Skip to content

Commit f687e8a

Browse files
authored
Add Haskell Scotty analyzer (#1686)
Extends Noir's Haskell coverage (Servant, Yesod) with Scotty — the Sinatra-inspired micro web framework popular for small Haskell APIs. - New analyzer parses `get|post|put|patch|delete|options|head "path"` route blocks and `addroute METHOD "path"` declarations. - `:name` path captures become `:name` path params, matching the convention used by Servant/Yesod analyzers. - Inside each handler block, `param`/`queryParam`/`pathParam`/ `formParam`/`header` calls and `jsonData`/`files` references are surfaced as query, path, body, and header parameters. - Callee resolution handles both inline `do` handlers and named handler functions referenced as `get "/path" listAll`. - Detector recognises Scotty via `package.yaml`/`.cabal` dependencies and `import Web.Scotty` / `scotty N $ ...` usage. Closes #1656
1 parent 8b4fa3f commit f687e8a

13 files changed

Lines changed: 559 additions & 3 deletions

File tree

docs/content/usage/supported/callee_coverage/index.ko.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ JSON, JSONL, YAML, TOML 같은 모델 기반 포맷과 plain 모델 직렬화는
3333
| F# | Giraffe |
3434
| Go | Beego, Chi, Echo, fasthttp, Fiber, Gin, GoFrame, Goyave, go-zero, Hertz, httprouter, Iris, Gorilla Mux |
3535
| Groovy | Grails |
36-
| Haskell | Servant, Yesod |
36+
| Haskell | Scotty, Servant, Yesod |
3737
| Java | Armeria, Dropwizard, Javalin, JAX-RS, Micronaut, Play, Quarkus, Spark, Spring, Vert.x |
3838
| JavaScript | Express, Fastify, Hono, Koa, NestJS, Next.js, Nitro, Nuxt, Remix, Restify, SvelteKit |
3939
| Kotlin | http4k, Ktor, Spring |

docs/content/usage/supported/callee_coverage/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ This matrix lists frameworks with functional test coverage for callee extraction
3333
| F# | Giraffe |
3434
| Go | Beego, Chi, Echo, fasthttp, Fiber, Gin, GoFrame, Goyave, go-zero, Hertz, httprouter, Iris, Gorilla Mux |
3535
| Groovy | Grails |
36-
| Haskell | Servant, Yesod |
36+
| Haskell | Scotty, Servant, Yesod |
3737
| Java | Armeria, Dropwizard, Javalin, JAX-RS, Micronaut, Play, Quarkus, Spark, Spring, Vert.x |
3838
| JavaScript | Express, Fastify, Hono, Koa, NestJS, Next.js, Nitro, Nuxt, Remix, Restify, SvelteKit |
3939
| Kotlin | http4k, Ktor, Spring |

docs/content/usage/supported/language_and_frameworks/index.ko.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -900,6 +900,33 @@ sort_by = "weight"
900900
## Haskell
901901

902902
<div class="tech-grid">
903+
<article class="tech-card">
904+
<h3 class="tech-card-title">Scotty</h3>
905+
<div class="tech-card-row">
906+
<span class="tech-card-label">Route</span>
907+
<div class="tech-card-chips">
908+
<span class="tech-chip on">endpoint</span>
909+
<span class="tech-chip on">method</span>
910+
<span class="tech-chip on">query</span>
911+
<span class="tech-chip on">path</span>
912+
<span class="tech-chip on">body</span>
913+
<span class="tech-chip on">header</span>
914+
<span class="tech-chip off">cookie</span>
915+
<span class="tech-chip off">static</span>
916+
<span class="tech-chip off">websocket</span>
917+
<span class="tech-chip on">callee</span>
918+
</div>
919+
</div>
920+
<div class="tech-card-row">
921+
<span class="tech-card-label">AI Context</span>
922+
<div class="tech-card-chips">
923+
<span class="tech-chip off">guards</span>
924+
<span class="tech-chip on">sinks</span>
925+
<span class="tech-chip on">validators</span>
926+
<span class="tech-chip on">signals</span>
927+
</div>
928+
</div>
929+
</article>
903930
<article class="tech-card">
904931
<h3 class="tech-card-title">Servant</h3>
905932
<div class="tech-card-row">

docs/content/usage/supported/language_and_frameworks/index.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -900,6 +900,33 @@ Each card below lists a framework's supported features. **Route** covers endpoin
900900
## Haskell
901901

902902
<div class="tech-grid">
903+
<article class="tech-card">
904+
<h3 class="tech-card-title">Scotty</h3>
905+
<div class="tech-card-row">
906+
<span class="tech-card-label">Route</span>
907+
<div class="tech-card-chips">
908+
<span class="tech-chip on">endpoint</span>
909+
<span class="tech-chip on">method</span>
910+
<span class="tech-chip on">query</span>
911+
<span class="tech-chip on">path</span>
912+
<span class="tech-chip on">body</span>
913+
<span class="tech-chip on">header</span>
914+
<span class="tech-chip off">cookie</span>
915+
<span class="tech-chip off">static</span>
916+
<span class="tech-chip off">websocket</span>
917+
<span class="tech-chip on">callee</span>
918+
</div>
919+
</div>
920+
<div class="tech-card-row">
921+
<span class="tech-card-label">AI Context</span>
922+
<div class="tech-card-chips">
923+
<span class="tech-chip off">guards</span>
924+
<span class="tech-chip on">sinks</span>
925+
<span class="tech-chip on">validators</span>
926+
<span class="tech-chip on">signals</span>
927+
</div>
928+
</div>
929+
</article>
903930
<article class="tech-card">
904931
<h3 class="tech-card-title">Servant</h3>
905932
<div class="tech-card-row">
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
name: scotty-fixture
2+
version: 0.1.0.0
3+
dependencies:
4+
- base
5+
- scotty
6+
- warp
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{-# LANGUAGE OverloadedStrings #-}
2+
3+
module Main where
4+
5+
import Web.Scotty
6+
import Data.Text.Lazy (Text)
7+
8+
main :: IO ()
9+
main = scotty 3000 $ do
10+
get "/" $ do
11+
text "ok"
12+
13+
get "/users" $ do
14+
q <- queryParam "q"
15+
json q
16+
17+
post "/users" $ do
18+
u <- jsonData
19+
json u
20+
21+
get "/users/:id" $ do
22+
i <- pathParam "id"
23+
json i
24+
25+
put "/users/:id" $ do
26+
name <- formParam "name"
27+
text name
28+
29+
delete "/users/:id" $ do
30+
text "deleted"
31+
32+
patch "/users/:id" $ do
33+
token <- header "X-Token"
34+
text "patched"
35+
36+
options "/users" $ text "opt"
37+
38+
addroute GET "/health" $ text "healthy"
39+
40+
get "/search" listMatches
41+
42+
listMatches :: ActionM ()
43+
listMatches = do
44+
term <- queryParam "term"
45+
json term
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
require "../../func_spec.cr"
2+
3+
expected_endpoints = [
4+
Endpoint.new("/", "GET"),
5+
Endpoint.new("/users", "GET", [
6+
Param.new("q", "", "query"),
7+
]),
8+
Endpoint.new("/users", "POST", [
9+
Param.new("body", "JSON", "body"),
10+
]),
11+
Endpoint.new("/users/:id", "GET", [
12+
Param.new("id", "", "path"),
13+
]),
14+
Endpoint.new("/users/:id", "PUT", [
15+
Param.new("id", "", "path"),
16+
Param.new("name", "", "body"),
17+
]),
18+
Endpoint.new("/users/:id", "DELETE", [
19+
Param.new("id", "", "path"),
20+
]),
21+
Endpoint.new("/users/:id", "PATCH", [
22+
Param.new("id", "", "path"),
23+
Param.new("X-Token", "", "header"),
24+
]),
25+
Endpoint.new("/users", "OPTIONS"),
26+
Endpoint.new("/health", "GET"),
27+
Endpoint.new("/search", "GET"),
28+
]
29+
30+
FunctionalTester.new("fixtures/haskell/scotty/", {
31+
:techs => 1,
32+
:endpoints => expected_endpoints.size,
33+
}, expected_endpoints).perform_tests
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
require "../../../spec_helper"
2+
require "../../../../src/detector/detectors/haskell/*"
3+
4+
describe "Detect Haskell Scotty" do
5+
options = create_test_options
6+
instance = Detector::Haskell::Scotty.new options
7+
8+
it "package_yaml/dependency" do
9+
instance.detect("package.yaml", "dependencies:\n - scotty\n - warp").should be_true
10+
end
11+
12+
it "cabal/dependency" do
13+
instance.detect("sample.cabal", "build-depends: base, scotty, warp").should be_true
14+
end
15+
16+
it "haskell/import_scotty" do
17+
instance.detect("src/Main.hs", "import Web.Scotty\nmain = pure ()").should be_true
18+
end
19+
20+
it "haskell/scotty_call" do
21+
instance.detect("src/Main.hs", "main = scotty 3000 $ do { get \"/\" $ text \"hi\" }").should be_true
22+
end
23+
24+
it "haskell/unrelated" do
25+
instance.detect("src/Main.hs", "main = putStrLn \"hello\"").should be_false
26+
end
27+
end

src/analyzer/analyzer.cr

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ def initialize_analyzers(logger : NoirLogger)
4848
{"go_mux", Go::Mux},
4949
{"go_pocketbase", Go::Pocketbase},
5050
{"groovy_grails", Groovy::Grails},
51+
{"haskell_scotty", Haskell::Scotty},
5152
{"haskell_servant", Haskell::Servant},
5253
{"haskell_yesod", Haskell::Yesod},
5354
{"asyncapi", Specification::AsyncApi},

0 commit comments

Comments
 (0)