class Analyzer::Go::GoEngine

Direct Known Subclasses

Defined in:

analyzer/engines/go_engine.cr

Constant Summary

GO_HTTP_ROUTE_CALL_RE = /\.(?:GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|Get|Post|Put|Delete|Patch|Head|Options|ANY|Any|All)\s*\(/

Class Method Summary

Instance Method Summary

Instance methods inherited from class Analyzer

analyze analyze, base_path : String base_path, base_paths : Array(String) base_paths, callees_needed? : Bool callees_needed?, logger : NoirLogger logger, parallel_analyze(files : Array(String), &block : String -> Nil) parallel_analyze, read_file_content(path : String) : String read_file_content, result : Array(Endpoint) result, url : String url

Constructor methods inherited from class Analyzer

new(options : Hash(String, YAML::Any)) new

Macros inherited from class Analyzer

define_getter_methods(names) define_getter_methods

Instance methods inherited from module FileHelper

all_files : Array(String) all_files, get_files_by_extension(extension : String) : Array(String) get_files_by_extension, get_files_by_prefix(prefix : String) : Array(String) get_files_by_prefix, get_files_by_prefix_and_extension(prefix : String, extension : String) : Array(String) get_files_by_prefix_and_extension, get_public_dir_files(base_path : String, folder : String) : Array(String) get_public_dir_files, get_public_files(base_path : String, anchors : Array(String) = ["shard.yml", "Gemfile"]) : Array(String) get_public_files

Class Method Detail

def self.go_test_file?(path : String) : Bool #

*_test.go is Go's hard-wired build-tag for test-only source. go build excludes these files entirely; only go test pulls them in. Real route handlers never live there, but framework repos (echo, chi, gin) park hundreds of e.GET("/...", ...) calls in *_test.go to exercise the router. Skip both in the cross-file pre-passes (so test-file groups don't pollute the production group map) and in each analyzer's per-file loop.

Also skip Go's toolchain-ignored directory prefixes: any path component starting with _ (_examples/, _assets/, _testdata/) is excluded by go build/go list outright. kataras/iris alone parks 392 phantom endpoints under _examples/...; they're documented apps, not production routes the framework ships.


[View source]

Instance Method Detail

def add_param_to_endpoint(param : Param, endpoint : Endpoint) #

[View source]
def add_static_path_if_valid(static_path : Hash(String, String), public_dirs : Array(Hash(String, String))) #

[View source]
def collect_import_path_function_bodies(package_function_bodies : Hash(String, Hash(String, Noir::GoCalleeExtractor::FunctionBody))) : Hash(String, Hash(String, Noir::GoCalleeExtractor::FunctionBody)) #

Builds {import_path => {function_name => body}} for local Go packages under the configured base paths. This is gated on callee demand and reuses package_function_bodies, so default route scans do not pay for go.mod discovery. It lets generated routers like Hertz resolve feed.Feed when feed imports a sibling package from the same module.


[View source]
def collect_import_path_method_bodies(package_method_bodies : Hash(String, Hash(String, Array(Noir::GoCalleeExtractor::FunctionBody)))) : Hash(String, Hash(String, Array(Noir::GoCalleeExtractor::FunctionBody))) #

Builds {import_path => {method_name => [body, ...]}} for local Go packages under the configured base paths. This complements #collect_import_path_function_bodies for route handlers referenced as method values on imported package instances, e.g. handler := api.NewHandler(svc); app.Get("/x", handler.Show).


[View source]
def collect_package_controller_method_bodies(file_contents : Hash(String, String)) : Hash(String, Hash(String, Array(Noir::GoCalleeExtractor::FunctionBody))) #

Per-directory {method_name => [FunctionBody, ...]} map for controller-style routes whose handler is referenced by method name (Beego's web.Router("/x", &Ctrl{}, "get:Method")). Lets the analyzer walk the controller method's body for callees even though the registration call doesn't pass the handler as an argument. Gated on callees_needed? so default scans pay nothing.


[View source]
def collect_package_controller_methods(file_contents : Hash(String, String)) : Hash(String, Hash(String, Array(String))) #

--- Beego controller-method pre-pass --------------------------------

Builds a per-directory {controller_type => [http_verb_methods]} map so mapping-less web.Router("/x", &Ctrl{}) registrations resolve to the exact methods the controller implements. Keyed by directory because Beego controllers and their router registrations share a Go package (== directory); a controller defined in a sibling file is still found.


[View source]
def collect_package_function_bodies(file_contents : Hash(String, String)) : Hash(String, Hash(String, Noir::GoCalleeExtractor::FunctionBody)) #

--- Cross-file function body pre-pass --------------------------------

Walks every cached .go source in file_contents and collects top-level function_declaration nodes into a per-directory map so cross-file identifier-handler resolution in Noir::GoCalleeExtractor is O(1) at lookup time. The map is keyed by directory because Go's name resolution rules are scoped to a single package (== single directory), so we never have to worry about cross-package leakage.


[View source]
def collect_package_groups_ts(group_method : String = "Group", import_marker : String | Nil = nil) : Tuple(Hash(String, Hash(String, String)), Hash(String, String)) #

--- Tree-sitter group/route pre-pass --------------------------------

Does a cross-file fixpoint over every .go file in each package directory, extracting group declarations (x := r.Group("/x") and friends). Returns:

  • package_groups — per-directory {group_name => resolved_prefix}
  • file_contents — cached source strings keyed by path, so the per-file second pass doesn't read files twice

group_method is the method name used for grouping — "Group" for Gin/Echo/Fiber/Hertz (default), "Party" for Iris, "Subrouter" for Mux.

The fixpoint handles the routes.go calling v1.GET(...) under a v1 := r.Group("/v1") declared in main.go case — iterate until no new entries land. In-file declarations win over external ones. import_marker, when given, restricts the cross-file group/engine pre-pass to files that actually import the framework. Group variables (v1 := r.Group("/v1")) and root-engine bindings only ever appear in framework-importing files, so skipping the rest avoids parsing unrelated .go files — a large saving in mixed repos (e.g. an example monorepo where most dirs use other routers). file_contents is still populated for every file so the per-file route pass and callee pre-pass keep their read cache.


[View source]
def framework_package_dirs(file_contents : Hash(String, String), import_marker : String) : Set(String) #

Per-package directories that import a target framework. Some real projects hide the concrete framework type behind a local interface, so the file that calls router.GET(...) may not import Gin/Echo itself.


[View source]
def framework_route_source_candidate?(content : String, dir : String, framework_dirs : Set(String), import_marker : String, extra_methods : Array(String)) : Bool #

[View source]
def go_route_source_candidate?(content : String, extra_methods : Array(String)) : Bool #

Cheap gate before the tree-sitter route pass. Handler-only files often import Gin/Echo for *gin.Context / echo.Context, but they cannot emit endpoints unless they contain a route/static registration call.


[View source]
def read_package_file_contents : Hash(String, String) #

Read every .go file once into a {path => source} hash. Used by analyzers (Goyave) that don't otherwise call #collect_package_groups_ts but still need the file_contents hash as input to #collect_package_function_bodies.


[View source]
def resolve_public_dirs(public_dirs : Array(Hash(String, String))) #

[View source]
def resolve_public_dirs_with_glob(public_dirs : Array(Hash(String, String))) #

[View source]
def static_dir_entry(source_path : String, static_path : String, file_path : String) : Hash(String, String) #

[View source]
def ts_controller_method_bodies_for_directory(package_controller_method_bodies : Hash(String, Hash(String, Array(Noir::GoCalleeExtractor::FunctionBody))), dir : String) : Hash(String, Array(Noir::GoCalleeExtractor::FunctionBody)) #

[View source]
def ts_controller_methods_for_directory(package_controller_methods : Hash(String, Hash(String, Array(String))), dir : String) : Hash(String, Array(String)) #

Returns the cross-file controller-method map for the given directory, or an empty map.


[View source]
def ts_function_bodies_for_directory(package_function_bodies : Hash(String, Hash(String, Noir::GoCalleeExtractor::FunctionBody)), dir : String) : Hash(String, Noir::GoCalleeExtractor::FunctionBody) #

Returns the cross-file function-body map for the given directory, or an empty map when the directory has no captured functions.


[View source]
def ts_groups_for_directory(package_groups : Hash(String, Hash(String, String)), dir : String) : Hash(String, String) #

Returns the cross-file group map for the given directory, or an empty map when the directory has no registered groups.


[View source]