Skip to content

feat(extractor/gin): Gin extractor enhancement — response tracing, param types, tags, route exclusion#69

Merged
spencercjh merged 10 commits intomainfrom
feat/real-gin-project
Apr 3, 2026
Merged

feat(extractor/gin): Gin extractor enhancement — response tracing, param types, tags, route exclusion#69
spencercjh merged 10 commits intomainfrom
feat/real-gin-project

Conversation

@spencercjh
Copy link
Copy Markdown
Owner

@spencercjh spencercjh commented Apr 2, 2026

Summary

  • Cross-function response tracing: auto-discover helper funcs taking *gin.Context, trace into their bodies to find c.JSON/c.XML calls for response type extraction (covers non-standard helpers beyond done/respond)
  • Parameter type inference: detect integer/boolean/number from strconv.Atoi/ParseBool/ParseFloat, c.GetInt()/c.GetBool() methods, and == "true" boolean comparisons
  • Tags inference: infer OpenAPI tags from route path prefix (e.g. /api/v1/projects/{id} → tag Projects)
  • operationId cleanup: strip package prefix from operationId (e.g. apis.CreateProjectCreateProject)
  • Configurable route exclusion: add --exclude-route and --exclude-route-prefix CLI flags for custom route filtering
  • Query/header param typing: use inferred GoType for parameter schemas instead of hardcoded string
  • Helper function detection: response helper (done/respond) call detection, cross-package handler name collision resolution, path param extraction from route path

Test plan

  • All existing unit tests pass (go test ./...)
  • All e2e tests pass (make test-e2e)
  • Lint passes with 0 issues (make lint)
  • Golden snapshot tests updated and passing
  • New tests for cross-function call tracing, parameter type inference (strconv, GetInt/GetBool, bool comparison)
  • New tests for custom route exclusion rules

🤖 Generated with Claude Code

…m() detection

The Gin extractor's buildOperation only added path parameters when
c.Param() calls were detected in handler code analysis. Handlers that
didn't explicitly call c.Param() for every path segment would produce
invalid OpenAPI specs missing required path parameters.

Now path parameters are always extracted from the route path definition
(the authoritative source of truth), matching the existing behavior for
handlers without analysis results.

Signed-off-by: spencercjh <spencercjh@gmail.com>
…ort aliases

findHandlerDecl now resolves package references by examining imports
in the route registration file, correctly handling both direct package
names and import aliases (e.g., customapis "gin-crosspkg/apis").
It also filters out methods (fn.Recv != nil) to avoid matching
non-handler functions with receivers. Adds resolvePkgRef and
pkgNameForImport helpers, plus an integration test with a multi-package
fixture that exercises name collisions across apis, adapter, and adapter
method declarations.

Signed-off-by: spencercjh <spencercjh@gmail.com>
…ase 2

 implements detection of standalone response helper functions like done(c, data, err) and extracts the Go type from arguments using varTypeMap. For response types from known helpers names (done, respond, response, writeJSON, sendJSON, reply). and the error argument patterns.

Signed-off-by: spencercjh <spencercjh@gmail.com>
Signed-off-by: spencercjh <spencercjh@gmail.com>
…rence, tags, route exclusion

- Cross-function call tracking: auto-discover helper funcs taking *gin.Context,
  trace into their bodies to find c.JSON/c.XML calls for response type extraction
- Parameter type inference: detect integer/boolean/number from strconv.Atoi/ParseBool,
  c.GetInt/c.GetBool methods, and == "true" boolean comparisons
- Tags inference from route path prefix (e.g. /api/v1/projects → tag: Projects)
- Clean operationId by stripping package prefix (apis.CreateProject → CreateProject)
- Add --exclude-route and --exclude-route-prefix CLI flags for configurable route filtering
- Use inferred GoType for query/header param schemas instead of hardcoded string

Signed-off-by: spencercjh <spencercjh@gmail.com>
Regenerated golden files to reflect the new features:
- Tags inferred from route path (e.g. /api/v1/users → tag "Users")
- Query params page/size now correctly typed as integer (from struct field types)

Signed-off-by: spencercjh <spencercjh@gmail.com>
Copilot AI review requested due to automatic review settings April 2, 2026 06:32
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 2, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • New CLI flags to exclude exact routes or route-prefixes; configurable via extract settings.
    • Automatic tag inference from route paths.
    • Improved response extraction that traces helper calls and unwraps wrapper payloads.
    • Better parameter type inference from strconv usages and boolean comparisons.
  • Bug Fixes

    • Promote embedded struct fields into parent schemas.
    • Improved path parameter extraction for Gin-style routes.
  • Docs

    • Added a roadmap detailing extractor improvements and phased goals.
  • Tests

    • New cross-package and extensive unit tests for extraction, tracing, inference, and filtering.

Walkthrough

This PR adds route exclusion flags/config, implements route filtering, introduces cross-function helper tracing and response extraction, promotes embedded struct fields into schemas, infers parameter types from code patterns, and updates generator integration and tests/fixtures accordingly.

Changes

Cohort / File(s) Summary
CLI & Config
cmd/generate.go, internal/config/config.go, internal/extractor/types.go
Added --exclude-route and --exclude-route-prefix flags; new ExcludeRoutes / ExcludeRoutePrefixes config and GenerateOptions fields; CLI falls back to config when flags are empty.
Route Extraction / Filtering
internal/extractor/gin/ast_parser.go, internal/extractor/gin/ast_parser_test.go
ExtractRoutes now accepts exclude lists; filters default noisy prefixes and custom exact/prefix rules; adds path normalization and logging; tests updated/expanded.
Generator Integration
internal/extractor/gin/generator.go
Passes exclusion options to parser; resolves cross-package handlers via import inspection; strips package prefixes from operationId; infers tags from paths; derives path params from routes and uses inferred Go types for param schemas; maps response status 0 → OpenAPI default.
Handler Analysis Core
internal/extractor/gin/handler_analyzer.go, internal/extractor/gin/handler_analyzer_test.go
Refactored handler analyzer: added helperFuncs and callerPkg, reorganized call dispatch into param/binding/response dispatchers, removed older extraction helpers; tests adjusted and some removed.
Helper Tracing
internal/extractor/gin/helper_tracer.go, internal/extractor/gin/helper_tracer_test.go
New tracer discovering package-level helper funcs (first param *gin.Context), mapping params→args, tracing helper bodies to resolve response payloads when calling c.JSON/c.XML/c.YAML; unit tests added.
Response Analysis
internal/extractor/gin/response_analyzer.go, internal/extractor/gin/response_analyzer_test.go
New response extraction logic: extract status codes (literals and http.Status*), unwrap wrapper Data for 2xx, normalize gin.H/map types, handle done(...)-style helper args, and emit ResponseInfo entries; tests added.
Parameter Type Inference
internal/extractor/gin/type_inference.go, internal/extractor/gin/type_inference_test.go
New inference of param Go types from strconv usages and boolean comparisons; updates HandlerInfo param types accordingly; tests added.
Schema Extraction
internal/extractor/gin/schema_extractor.go, internal/extractor/gin/schema_extractor_test.go
Promote anonymous embedded fields into parent schema (with visited tracking to avoid recursion); support embedded pointer promotion; update GoTypeToSchema mappings; tests added for promotion behavior.
Integration Tests & Fixtures
integration-tests/gin/fixtures/gin-crosspkg/*, integration-tests/gin/gin_test.go
Added gin-crosspkg fixture (apis, adapter, models, main, go.mod) and integration test TestCrossPackageNameCollision validating cross-package resolution, path params, request/response schemas.
Golden Fixtures
integration-tests/gin/fixtures/golden/paths/api-v1-users-*.json
Updated OpenAPI golden files: page/size param types to integer and added tags: ["Users"] to operations.
Docs / Plan
docs/plans/2026-04-02-gin-extractor-enhancement.md
Added planning doc summarizing gaps vs swaggo and phased roadmap for extractor improvements.
Removed / Moved Tests
internal/extractor/gin/handler_analyzer_test.go
Removed older status-code/response tests (migrated to response_analyzer tests); added fail-fast guards.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as CLI (generate.go)
    participant Config as Config (config.go)
    participant Generator as Generator (generator.go)
    participant Parser as ASTParser (ast_parser.go)
    participant HandlerAnalyzer as HandlerAnalyzer (handler_analyzer.go)
    participant HelperTracer as HelperTracer (helper_tracer.go)
    participant ResponseAnalyzer as ResponseAnalyzer (response_analyzer.go)
    participant SchemaExtractor as SchemaExtractor (schema_extractor.go)

    CLI->>Config: read exclusion flags / config
    Config-->>CLI: merged exclude lists
    CLI->>Generator: run Generate with options (incl. excludes)

    Generator->>Parser: ExtractRoutes(excludeRoutes, excludePrefixes)
    Parser->>Parser: normalize paths & filter defaults/custom prefixes
    Parser-->>Generator: filtered routes

    Generator->>HandlerAnalyzer: AnalyzeHandler(fn)
    HandlerAnalyzer->>HelperTracer: detect helper calls & lookup helpers
    HelperTracer->>ResponseAnalyzer: trace helper -> extract c.JSON/XML/YAML responses
    ResponseAnalyzer-->>HelperTracer: ResponseInfo{Status, GoType}
    HelperTracer-->>HandlerAnalyzer: attach Responses to HandlerInfo

    HandlerAnalyzer->>HandlerAnalyzer: inferParamTypes (strconv, comparisons)
    HandlerAnalyzer-->>Generator: HandlerInfo (params, responses)

    Generator->>SchemaExtractor: ExtractSchema(type)
    SchemaExtractor->>SchemaExtractor: promote embedded fields, avoid cycles
    SchemaExtractor-->>Generator: OpenAPI schema

    Generator-->>CLI: assembled OpenAPI spec (with tags, normalized operationIds)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Poem

🐰 I nibbled through routes, and filtered out the noise,
I hopped through helpers, traced returns with nimble poise,
Embedded fields I pulled so schemas now can bloom,
I sniffed out types from code — hooray! the docs resume.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 59.79% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main enhancements to the Gin extractor with specific focus areas: response tracing, parameter types, tags, and route exclusion.
Description check ✅ Passed The description provides detailed explanation of the changes, including the key features added (cross-function response tracing, parameter type inference, tags inference, etc.) and confirms test coverage.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/real-gin-project

Comment @coderabbitai help to get the list of available commands and usage tips.

@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

Gin extractor enhancement — response tracing, parameter types, tags, route exclusion, and cross-package resolution

✨ Enhancement 🧪 Tests

Grey Divider

Walkthroughs

Description
• **Cross-function response tracing**: Auto-discover helper functions accepting *gin.Context and
  trace into their bodies to find c.JSON/c.XML calls for response type extraction, covering
  non-standard helpers beyond done/respond
• **Parameter type inference**: Detect integer/boolean/number types from strconv conversions
  (Atoi, ParseInt, ParseBool, ParseFloat), c.GetInt()/c.GetBool() methods, and boolean
  comparisons (== "true")
• **Tags inference**: Infer OpenAPI tags from route path prefixes (e.g., /api/v1/users → tag
  Users)
• **Cross-package handler resolution**: Resolve handler references across packages using import
  analysis and handle name collisions with proper package matching
• **Configurable route exclusion**: Add --exclude-route and --exclude-route-prefix CLI flags for
  custom route filtering with default exclusions (/swagger, /docs, /debug, /static, /public)
• **Embedded struct field promotion**: Implement field promotion for embedded structs following Go's
  promotion rules with cycle detection
• **operationId cleanup**: Strip package prefix from operationId (e.g., apis.CreateProjectCreateProject)
• **Comprehensive test coverage**: New tests for helper analysis, parameter type inference, route
  filtering, embedded struct schemas, and cross-package handler resolution with integration tests
Diagram
flowchart LR
  A["Route Handler"] -->|"cross-package resolution"| B["Handler Declaration"]
  B -->|"trace helper calls"| C["Response Type Extraction"]
  A -->|"parameter analysis"| D["Type Inference"]
  D -->|"strconv/GetInt/GetBool"| E["Typed Parameters"]
  A -->|"path prefix"| F["Tag Inference"]
  A -->|"exclusion rules"| G["Filtered Routes"]
  B -->|"embedded structs"| H["Schema with Promoted Fields"]
  C --> I["OpenAPI Spec"]
  E --> I
  F --> I
  H --> I
Loading

Grey Divider

File Changes

1. internal/extractor/gin/handler_analyzer.go ✨ Enhancement +377/-233

Cross-function response tracing and parameter type inference

• Added cross-function response tracing by discovering helper functions accepting *gin.Context and
 tracing into their bodies for c.JSON/c.XML calls
• Implemented parameter type inference from strconv conversions (Atoi, ParseInt, ParseBool,
 ParseFloat), c.GetInt()/c.GetBool() methods, and boolean comparisons (== "true")
• Refactored method dispatch into separate handlers (dispatchParamMethods,
 dispatchBindingMethods, dispatchResponseMethods) for better code organization
• Added extractTypedParam to handle typed parameter extraction from GetInt/GetBool/GetInt64
 calls

internal/extractor/gin/handler_analyzer.go


2. internal/extractor/gin/helper_analyzer_test.go 🧪 Tests +477/-0

New tests for helper analysis and parameter type inference

• New comprehensive test suite for cross-function call tracking, parameter type inference, and
 helper function detection
• Tests cover strconv conversions, boolean comparisons, GetInt/GetBool methods, and known
 response helpers
• Validates that helper functions are correctly discovered and traced for response extraction
• Tests distinguish between known helpers, discovered helpers, and non-helper function calls

internal/extractor/gin/helper_analyzer_test.go


3. internal/extractor/gin/generator.go ✨ Enhancement +139/-27

Cross-package handler resolution and tag inference

• Enhanced findHandlerDecl to resolve cross-package handler references using import analysis and
 package name matching
• Added resolvePkgRef and pkgNameForImport functions to handle import aliases and cross-package
 collisions
• Implemented inferTag function to extract OpenAPI tags from route path prefixes
• Updated parameter schema generation to use inferred GoType instead of hardcoded "string"
• Modified extractPathParamsFromRoute to extract path parameters directly from route paths

internal/extractor/gin/generator.go


View more (19)
4. internal/extractor/gin/helper_analyzer.go Refactoring +310/-0

Helper analysis utilities and response type extraction

• Extracted helper analysis utilities into dedicated file including isFormRelatedWord,
 inferTypeFromVarName, isKnownResponseHelper
• Added isErrorType function to detect error values in helper calls
• Consolidated response type extraction functions: extractTypeFromResponseWithStatus,
 extractTypeFromResponseInternal, extractTypeFromCompositeLit
• Includes status code extraction and HTTP status constant mapping

internal/extractor/gin/helper_analyzer.go


5. internal/extractor/gin/ast_parser_test.go 🧪 Tests +170/-3

Tests for route filtering and exclusion rules

• Added tests for route filtering with default exclude prefixes (/swagger, /docs, /debug,
 /static, /public)
• Tests validate custom route exclusion rules via --exclude-route and --exclude-route-prefix
 flags
• Comprehensive test coverage for shouldExcludeRoute and filterRoutes functions with various
 path patterns

internal/extractor/gin/ast_parser_test.go


6. internal/extractor/gin/schema_extractor_test.go 🧪 Tests +141/-0

Tests for embedded struct schema extraction

• Added tests for embedded struct field promotion following Go's field promotion rules
• Tests cover simple embedded structs, nested embeddings, and pointer-embedded fields
• Validates that promoted fields are correctly included in parent schema properties

internal/extractor/gin/schema_extractor_test.go


7. cmd/generate.go ✨ Enhancement +35/-19

CLI flags for route exclusion configuration

• Added --exclude-route and --exclude-route-prefix CLI flags for custom route filtering
• Integrated exclusion options into GenerateOptions passed to extractor
• Updated flag variable declarations to include new exclusion parameters

cmd/generate.go


8. integration-tests/gin/fixtures/gin-crosspkg/apis/handlers.go 🧪 Tests +68/-0

Cross-package fixture handlers for integration tests

• New fixture file with Gin handlers in apis package demonstrating cross-package handler
 resolution
• Implements handlers for project CRUD operations with proper request/response types
• Uses ShouldBindJSON for request binding and c.Param/c.Query for parameter extraction

integration-tests/gin/fixtures/gin-crosspkg/apis/handlers.go


9. internal/extractor/gin/schema_extractor.go ✨ Enhancement +81/-2

Embedded struct field promotion in schema extraction

• Implemented embedded struct field promotion in extractStructSchema with cycle detection
• Added promoteEmbeddedFields to resolve and promote embedded struct properties into parent schema
• Added resolveEmbeddedTypeName to extract type names from embedded field expressions
• Supports simple identifiers, qualified names, and pointer types in embedded fields

internal/extractor/gin/schema_extractor.go


10. internal/extractor/gin/ast_parser.go ✨ Enhancement +50/-1

Route filtering with default and custom exclusion rules

• Added ExtractRoutes signature change to accept excludeRoutes and excludePrefixes parameters
• Implemented filterRoutes function to remove non-API routes based on default and custom exclusion
 rules
• Added shouldExcludeRoute to check paths against default prefixes (/swagger, /docs, /debug,
 /static, /public) and custom rules
• Integrated route filtering into the extraction pipeline

internal/extractor/gin/ast_parser.go


11. integration-tests/gin/fixtures/gin-crosspkg/adapter/project.go 🧪 Tests +27/-0

Adapter fixture for cross-package collision testing

• New fixture file with adapter functions having same names as handlers but different signatures (no
 *gin.Context)
• Demonstrates cross-package name collision scenario for testing handler resolution

integration-tests/gin/fixtures/gin-crosspkg/adapter/project.go


12. integration-tests/gin/fixtures/gin-crosspkg/adapter/service.go 🧪 Tests +27/-0

Service fixture with methods for collision testing

• New fixture file with service methods having same names as handlers and adapter functions
• Methods have receivers, distinguishing them from package-level handler functions
• Tests that method receivers are correctly filtered out during handler resolution

integration-tests/gin/fixtures/gin-crosspkg/adapter/service.go


13. integration-tests/gin/fixtures/gin-crosspkg/models/types.go 🧪 Tests +18/-0

Model types for cross-package fixture

• New fixture file defining request/response types for cross-package test project
• Includes Project, CreateProjectReq, and UpdateProjectReq types with JSON tags

integration-tests/gin/fixtures/gin-crosspkg/models/types.go


14. integration-tests/gin/fixtures/gin-crosspkg/main.go 🧪 Tests +18/-0

Main entry point for cross-package fixture

• New fixture file demonstrating route registration with cross-package handler references using
 import alias
• Routes use customapis alias for handlers from apis package

integration-tests/gin/fixtures/gin-crosspkg/main.go


15. integration-tests/gin/fixtures/gin-crosspkg/go.sum ⚙️ Configuration changes +89/-0

Go module dependencies for cross-package fixture

• New dependency file for cross-package fixture project with Gin and related dependencies

integration-tests/gin/fixtures/gin-crosspkg/go.sum


16. integration-tests/gin/gin_test.go 🧪 Tests +122/-0

Integration test for cross-package handler resolution

• Added TestCrossPackageNameCollision integration test validating cross-package handler resolution
 with import aliases
• Tests that handlers are correctly resolved despite name collisions across packages
• Validates request/response types, path parameters, and request body schemas are extracted
 correctly

integration-tests/gin/gin_test.go


17. internal/extractor/types.go ✨ Enhancement +8/-6

GenerateOptions struct extension for route exclusion

• Added ExcludeRoutes and ExcludeRoutePrefixes fields to GenerateOptions struct
• Allows configuration of custom route exclusion rules during spec generation

internal/extractor/types.go


18. docs/plans/2026-04-02-gin-extractor-enhancement.md 📝 Documentation +338/-0

Gin extractor enhancement plan with implementation roadmap

• Comprehensive enhancement plan for Gin extractor with 9 identified gaps compared to swaggo
• Proposes P0-P3 priority phases covering response schema extraction, embedded struct expansion,
 route filtering, and parameter type inference
• Details implementation strategies for cross-function response tracing, helper function detection,
 and type inference from strconv calls and binding structures
• Includes validation approach using real project comparison and known AST limitations

docs/plans/2026-04-02-gin-extractor-enhancement.md


19. integration-tests/gin/fixtures/gin-crosspkg/go.mod 🧪 Tests +37/-0

Cross-package test fixture Go module setup

• New Go module file for cross-package test fixture
• Declares dependency on github.com/gin-gonic/gin v1.12.0 with Go 1.26
• Includes transitive dependencies for Gin framework and related libraries

integration-tests/gin/fixtures/gin-crosspkg/go.mod


20. integration-tests/gin/fixtures/golden/paths/api-v1-users-get.json ✨ Enhancement +8/-3

Parameter type inference and tags extraction for list users endpoint

• Updated page query parameter type from string to integer with format: int32
• Updated size query parameter type from string to integer with format: int32
• Added tags field with value ["Users"] inferred from route path prefix

integration-tests/gin/fixtures/golden/paths/api-v1-users-get.json


21. integration-tests/gin/fixtures/golden/paths/api-v1-users-id-get.json ✨ Enhancement +4/-1

Tags inference for get user by ID endpoint

• Added tags field with value ["Users"] inferred from route path prefix /api/v1/users

integration-tests/gin/fixtures/golden/paths/api-v1-users-id-get.json


22. integration-tests/gin/fixtures/golden/paths/api-v1-users-post.json ✨ Enhancement +4/-1

Tags inference for create user endpoint

• Added tags field with value ["Users"] inferred from route path prefix /api/v1/users

integration-tests/gin/fixtures/golden/paths/api-v1-users-post.json


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented Apr 2, 2026

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX Issues (0)

Grey Divider


Action required

1. exclude-route bypasses config precedence📘 Rule violation ⚙ Maintainability
Description
The new --exclude-route/--exclude-route-prefix options are read only from CLI flags and are not
merged through the centralized config resolver, so env/config-file values cannot participate in the
required precedence order. This violates the standardized configuration precedence requirement and
makes behavior inconsistent with other options like outputDir/outputFormat which do honor config
precedence.
Code

cmd/generate.go[R85-94]

noStream, _ := cmd.Flags().GetBool("no-stream")
//nolint:errcheck
concurrency, _ := cmd.Flags().GetInt("concurrency")
+	//nolint:errcheck
+	excludeRoutes, _ := cmd.Flags().GetStringSlice("exclude-route")
+	//nolint:errcheck
+	excludeRoutePrefixes, _ := cmd.Flags().GetStringSlice("exclude-route-prefix")
// Step 1: Detect framework - try all registered extractors
extractorImpl, info, err := builtin.DetectFramework(path)
Evidence
PR Compliance ID 178479 requires config to be resolved with precedence `flag > env > config file >
default for each added/changed option. In cmd/generate.go`, output options are merged with config,
but the newly added route-exclusion options are taken directly from flags and passed through without
any central config/env/config-file overlay; internal/config/config.go also defines no config
fields for these options, preventing config/env participation.

Rule 178479: Respect standardized configuration precedence order
cmd/generate.go[85-167]
internal/config/config.go[10-58]
internal/config/config.go[78-102]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New route-exclusion settings (`--exclude-route`, `--exclude-route-prefix`) are not wired into the centralized configuration system, so they cannot follow the required precedence order (`flag > env > config file > default`).
## Issue Context
The project already uses Viper-backed config (`.spec-forge.yaml` + env) and applies precedence for some options (e.g., output dir/format). The new route exclusion options should follow the same precedence model.
## Fix Focus Areas
- cmd/generate.go[85-167]
- internal/config/config.go[10-58]
- internal/config/config.go[78-102]
- .spec-forge.example.yaml[1-35]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Typed params become object🐞 Bug ≡ Correctness
Description
HandlerAnalyzer assigns inferred parameter GoType values like "integer"/"boolean"/"number", but
Generator passes these to GoTypeToSchema which only recognizes Go type names (e.g., "int", "bool").
As a result, inferred typed query/header params are emitted as schema type=object instead of
primitive types.
Code

internal/extractor/gin/generator.go[R365-366]

// Query parameters
for _, param := range handlerInfo.QueryParams {
Evidence
Generator now uses GoTypeToSchema(param.GoType) for query/header parameters, but inferred types
are set to the OpenAPI strings "integer"/"boolean"/"number" in HandlerAnalyzer. GoTypeToSchema
doesn’t map those strings and falls back to object, breaking strconv/GetInt/GetBool type inference
in the generated spec.

internal/extractor/gin/generator.go[365-387]
internal/extractor/gin/handler_analyzer.go[10-16]
internal/extractor/gin/handler_analyzer.go[785-819]
internal/extractor/gin/schema_extractor.go[468-489]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Parameter type inference sets `ParamInfo.GoType` to OpenAPI-type strings (e.g., `"integer"`), but `GoTypeToSchema` only understands Go type names (e.g., `"int"`, `"bool"`). This causes inferred typed query/header params to render as `type: object`.
### Issue Context
- `Generator.buildOperation()` now emits parameter schemas via `GoTypeToSchema(param.GoType)`.
- `HandlerAnalyzer.inferTypeFromStrconv()` and `extractTypedParam()` assign `GoType` as `"integer"`/`"boolean"`/`"number"`.
### Fix Focus Areas
- internal/extractor/gin/schema_extractor.go[468-489]
- internal/extractor/gin/handler_analyzer.go[10-16]
- internal/extractor/gin/handler_analyzer.go[785-819]
- internal/extractor/gin/generator.go[365-387]
### Suggested fix
Update `GoTypeToSchema` to explicitly handle `"integer"`, `"boolean"`, `"number"` (and optionally `"array"`) and return the appropriate OpenAPI schema types/formats; or change inference constants to Go type names (`"int"`/`"bool"`/`"float64"`) so the existing mapping works.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Invalid response code "0"🐞 Bug ≡ Correctness
Description
Helper response extraction uses StatusCode=0 to represent a default response, but Generator
serializes responses using strconv.Itoa, producing an invalid OpenAPI response key "0".
Additionally, nil is classified as an error argument, which can produce a default response with
GoType="nil".
Code

internal/extractor/gin/handler_analyzer.go[R624-640]

+		// Check if this looks like an error type (err variable, *gin.Error, etc.)
+		if isErrorType(arg, varTypeMap) {
+			// Generate default error response
+			info.Responses = append(info.Responses, ResponseInfo{
+				StatusCode: 0, // 0 = default
+				GoType:     goType,
+			})
+			slog.Debug("Extracted helper error response", "type", goType)
+		} else {
+			// Generate 200 success response
+			info.Responses = append(info.Responses, ResponseInfo{
+				StatusCode: 200,
+				GoType:     goType,
+			})
+			slog.Debug("Extracted helper success response", "type", goType)
}
}
Evidence
extractHelperResponse appends ResponseInfo entries with StatusCode: 0 for helper error
responses, and isErrorType returns true when the argument is the identifier nil. Generator later
uses strconv.Itoa(resp.StatusCode) as the response map key, which will emit "0" instead of
OpenAPI’s required default key for non-status responses.

internal/extractor/gin/handler_analyzer.go[609-640]
internal/extractor/gin/helper_analyzer.go[87-105]
internal/extractor/gin/generator.go[394-431]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Helper-derived responses encode “default” as `StatusCode=0`, but the OpenAPI generator writes response keys using `strconv.Itoa(StatusCode)`, yielding a response key of `"0"` (invalid in OpenAPI). Also, `nil` is treated as an error argument, which can create a meaningless default response with GoType `"nil"`.
### Issue Context
- `extractHelperResponse()` uses `StatusCode: 0` for default responses.
- `isErrorType()` returns true for `nil`.
- `Generator.buildOperation()` converts status codes to strings with `strconv.Itoa` without special-casing 0.
### Fix Focus Areas
- internal/extractor/gin/handler_analyzer.go[609-640]
- internal/extractor/gin/helper_analyzer.go[87-105]
- internal/extractor/gin/generator.go[394-431]
### Suggested fix
1) Change response keying logic:
- In `Generator.buildOperation`, if `resp.StatusCode == 0`, set the response under key `"default"` (and avoid `strconv.Itoa(0)`), or change `ResponseInfo` to represent default explicitly.
2) Prevent `nil` from generating a default response:
- Update `isErrorType()` to *not* treat `nil` as an error value for the purpose of emitting an error response; or in `extractHelperResponse()` explicitly skip emitting an error response when the error argument is literal `nil`.
3) (Optional but likely needed)
- Don’t derive the error-response schema from the error argument’s type; instead either trace the helper body to discover the actual response payload type or emit a generic schema for `default` when unknown.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. Helper tracing misses substitution🐞 Bug ≡ Correctness
Description
Cross-function response tracing only substitutes helper parameters when the response expression is a
single identifier. If the helper responds with a wrapper/composite literal containing parameter
identifiers (e.g., Rsp{Data: data}), the nested identifier won’t be replaced and the extracted
response type can be wrong.
Code

internal/extractor/gin/handler_analyzer.go[R748-758]

+// resolveThroughParamMap resolves an expression by substituting helper parameters
+// with their call-site arguments. If the expression is an Ident matching a helper
+// parameter name, the corresponding call-site argument is returned instead.
+func (a *HandlerAnalyzer) resolveThroughParamMap(expr ast.Expr, paramArgMap map[string]ast.Expr) ast.Expr {
+	if ident, ok := expr.(*ast.Ident); ok {
+		if arg, exists := paramArgMap[ident.Name]; exists {
+			return arg
+		}
}
-	return 200
+	return expr
}
Evidence
traceHelperResponse attempts to map helper parameters to call-site arguments, but
resolveThroughParamMap only swaps *ast.Ident at the top level and does not recursively rewrite
identifiers inside composite literals, call expressions, or key-value expressions. This causes
traced response extraction to miss the call-site types when helpers wrap data before calling
c.JSON.

internal/extractor/gin/handler_analyzer.go[674-719]
internal/extractor/gin/handler_analyzer.go[748-758]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Helper tracing builds a param→argument map, but only substitutes when the response expression is a top-level identifier. Wrapper responses like `Rsp{Data: data}` won’t resolve `data` to the call-site argument, producing incorrect response type extraction.
### Issue Context
- `traceHelperResponse()` calls `resolveThroughParamMap(call.Args[1], ...)`.
- `resolveThroughParamMap()` only handles `*ast.Ident` and returns `expr` unchanged otherwise.
### Fix Focus Areas
- internal/extractor/gin/handler_analyzer.go[674-719]
- internal/extractor/gin/handler_analyzer.go[748-758]
### Suggested fix
Implement a recursive expression rewriter (or resolver) that walks `ast.Expr` trees and replaces any identifier matching a helper parameter with its call-site argument, including inside:
- `*ast.CompositeLit` elements (`KeyValueExpr` values)
- `*ast.KeyValueExpr`
- `*ast.UnaryExpr`
- `*ast.CallExpr` args
Then feed the rewritten expression into `extractTypeFromResponseWithStatus`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Path params always string🐞 Bug ≡ Correctness
Description
Path parameters are now extracted from the route pattern and emitted with schema type string
unconditionally. This bypasses HandlerInfo.PathParams entirely, so inferred path param types
(e.g., from strconv.Atoi(c.Param("id"))) can never affect the OpenAPI output.
Code

internal/extractor/gin/generator.go[R360-363]

+	// Path parameters: extract from the route path (source of truth)
+	// rather than relying solely on c.Param() calls in the handler,
+	// since handlers may not explicitly call c.Param() for every path segment.
+	operation.Parameters = append(operation.Parameters, g.extractPathParamsFromRoute(route.FullPath)...)
Evidence
buildOperation appends path parameters from extractPathParamsFromRoute(route.FullPath) and that
function hardcodes the schema type to string. Meanwhile the analyzer can update
info.PathParams[i].GoType, but those values are unused for path params after this change.

internal/extractor/gin/generator.go[357-364]
internal/extractor/gin/generator.go[594-623]
internal/extractor/gin/handler_analyzer.go[900-924]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Path params are sourced from the route pattern (good for completeness), but their types are always emitted as string. Any type inference done in the handler analyzer for `PathParams` is now ignored.
### Issue Context
- `buildOperation()` uses `extractPathParamsFromRoute(route.FullPath)`.
- `extractPathParamsFromRoute()` hardcodes `Type: string`.
- `HandlerAnalyzer.updateParamType()` can set `info.PathParams[i].GoType`.
### Fix Focus Areas
- internal/extractor/gin/generator.go[357-364]
- internal/extractor/gin/generator.go[594-623]
- internal/extractor/gin/handler_analyzer.go[900-924]
### Suggested fix
Keep extracting param *names* from the route path, but when building each `openapi3.Parameter`, look up the same param name in `handlerInfo.PathParams` and, if found, set schema via `GoTypeToSchema(found.GoType)`; otherwise default to string. This preserves completeness while allowing inferred typing.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@spencercjh spencercjh self-assigned this Apr 2, 2026
@spencercjh spencercjh added enhancement New feature or request go Pull requests that update go code labels Apr 2, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances the Gin extractor’s AST-based OpenAPI generation to better match real-world Gin code patterns, especially around response discovery, parameter typing, tags/operationId formatting, and route filtering.

Changes:

  • Add cross-function response helper tracing and improve response/type inference utilities.
  • Improve parameter typing (query/header) and enrich operations with inferred tags + cleaned operationId.
  • Add configurable route exclusion (--exclude-route, --exclude-route-prefix) plus default non-API route filtering; extend schema extraction to promote embedded struct fields.

Reviewed changes

Copilot reviewed 21 out of 22 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
internal/extractor/types.go Extend GenerateOptions with exclude-route settings.
internal/extractor/gin/schema_extractor.go Promote embedded struct fields into parent schemas with cycle detection.
internal/extractor/gin/schema_extractor_test.go Add tests covering embedded/pointer/nested embedded struct promotion.
internal/extractor/gin/helper_analyzer.go Introduce helper/type/status-code inference helpers shared by handler analysis.
internal/extractor/gin/helper_analyzer_test.go Add tests for helper tracing and parameter type inference.
internal/extractor/gin/handler_analyzer.go Discover helper funcs, trace helper responses, infer param types, and typed parameter extraction.
internal/extractor/gin/generator.go Pass exclude rules into route extraction; improve cross-package handler resolution; infer tags; use typed schemas for query/header params.
internal/extractor/gin/ast_parser.go Add default + user-configurable route filtering during extraction.
internal/extractor/gin/ast_parser_test.go Add tests for default/custom route exclusion logic.
cmd/generate.go Add CLI flags and plumb exclude rules into GenerateOptions.
integration-tests/gin/gin_test.go Add e2e test for cross-package handler name collision resolution.
integration-tests/gin/fixtures/golden/* Update golden snapshots for tags and typed query params.
integration-tests/gin/fixtures/gin-crosspkg/* Add new fixture project for cross-package handler resolution testing.
docs/plans/2026-04-02-gin-extractor-enhancement.md Add enhancement plan document describing goals/approach.
Comments suppressed due to low confidence (1)

internal/extractor/gin/ast_parser.go:125

  • Route filtering now always applies the hard-coded defaultExcludePrefixes (e.g., /docs, /debug, /static). This is a behavior change that can silently drop legitimate APIs living under these prefixes, and there’s currently no opt-out. Consider making the defaults configurable (e.g., only apply when user hasn’t provided any exclude rules, or add a flag/config to disable default excludes).
	// Filter out non-API routes (swagger docs, static files, debug endpoints, etc.)
	routes = filterRoutes(routes, excludeRoutes, excludePrefixes)

	slog.Info("Extracted routes", "count", len(routes))
	return routes, nil
}

// extractGroups extracts router group definitions from a file.
func (p *ASTParser) extractGroups(file *ast.File) map[string]*GroupInfo {
	groups := make(map[string]*GroupInfo)

	ast.Inspect(file, func(n ast.Node) bool {
		if node, ok := n.(*ast.AssignStmt); ok {
			if len(node.Lhs) == 1 && len(node.Rhs) == 1 {
				if group := p.parseGroupAssignment(node.Rhs[0]); group != nil {

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/extractor/gin/handler_analyzer.go Outdated
Comment thread internal/extractor/gin/helper_analyzer.go Outdated
Comment thread internal/extractor/gin/generator.go
Comment thread internal/extractor/gin/schema_extractor.go
Comment thread cmd/generate.go
Comment thread internal/extractor/gin/generator.go
Comment thread internal/extractor/gin/handler_analyzer.go Outdated
Comment thread internal/extractor/gin/handler_analyzer.go Outdated
Comment thread internal/extractor/gin/generator.go Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (1)
internal/extractor/gin/schema_extractor_test.go (1)

835-851: Consider consolidating the duplicate helper functions.

getSchemaPropKeys and getSchemaKeys (lines 836-842) have identical implementations since openapi3.Schemas is the same type used for both component schemas and properties. Consider using a single helper.

♻️ Suggested consolidation
 // Helper function to get schema keys for error messages
 func getSchemaKeys(schemas openapi3.Schemas) []string {
 	keys := make([]string, 0, len(schemas))
 	for k := range schemas {
 		keys = append(keys, k)
 	}
 	return keys
 }
-
-// Helper function to get property keys for error messages
-func getSchemaPropKeys(props openapi3.Schemas) []string {
-	keys := make([]string, 0, len(props))
-	for k := range props {
-		keys = append(keys, k)
-	}
-	return keys
-}

Then update the call sites (lines 740, 744, 788, 830) to use getSchemaKeys instead of getSchemaPropKeys.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/extractor/gin/schema_extractor_test.go` around lines 835 - 851,
Consolidate the duplicate helpers by removing getSchemaPropKeys and using
getSchemaKeys everywhere; update all call sites that currently call
getSchemaPropKeys to call getSchemaKeys instead (the functions operate on the
same openapi3.Schemas type), and delete the redundant getSchemaPropKeys
declaration so only getSchemaKeys remains.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/plans/2026-04-02-gin-extractor-enhancement.md`:
- Around line 9-10: The plan uses a workstation-specific absolute path
`/home/caijh/codes/open/openapi` which prevents reproducibility; update the
docs/plans/2026-04-02-gin-extractor-enhancement.md content to replace that
hardcoded path (and any other occurrences around lines 314-319) with a neutral
placeholder like `<openapi-repo>` or a relative path (e.g., `./openapi`) so
examples and comparison steps are reproducible for other contributors.

In `@internal/extractor/gin/ast_parser.go`:
- Around line 344-358: The shouldExcludeRoute function is matching filters
verbatim and with raw HasPrefix which misses parameter syntax differences and
mis-matches boundaries; normalize both the input path and all filters
(defaultExcludePrefixes, extraPrefixes, extraExcludes) to the same OpenAPI style
(e.g., convert ":param" to "{param}" and collapse duplicate slashes) before
comparing, perform prefix checks on segment boundaries (ensure a prefix match
only when the next character is '/' or end-of-string to avoid "/public" matching
"/publications"), and then do the exact exclude check against the normalized
extraExcludes; update the logic around defaultExcludePrefixes, extraPrefixes and
extraExcludes in shouldExcludeRoute to use these normalized values.

In `@internal/extractor/gin/generator.go`:
- Around line 360-363: The current change switches path param sourcing to
route.FullPath but extractPathParamsFromRoute hardcodes every param schema to
string, losing earlier analyzer-inferred types (e.g. int for id); update the
merge logic so extractPathParamsFromRoute (or its caller) preserves any existing
inferred schema/type for a parameter instead of overriding with string: when
appending to operation.Parameters, check for an existing parameter by name (from
analyzer results or prior operation.Parameters) and reuse its Schema/Type; only
create a new string-typed schema for parameters that are truly absent. Ensure
the logic references extractPathParamsFromRoute, operation.Parameters and
route.FullPath so name matching and schema preservation occur during the
append/merge.

In `@internal/extractor/gin/handler_analyzer.go`:
- Around line 23-24: The helper discovery currently uses a single helperFuncs
map keyed only by function name causing collisions across packages; update
discoverHelperFuncs to store helpers scoped by package (e.g.
map[string]map[string]*ast.FuncDecl or map[pkgName+"#"+funcName]*ast.FuncDecl)
using the package identifier from each parsed ast.File, and change
parseHelperCall to resolve bare helper calls by first looking up the helper in
the caller's package scope (and still support selector/resolved-package lookups)
so functions with the same name in different packages do not overwrite each
other; update any code that reads helperFuncs (references in
discoverHelperFuncs, parseHelperCall and other locations around lines ~41-52 and
~669-670) to use the new scoped key or nested map lookup.
- Around line 705-708: The response resolution must recursively replace
identifiers inside helper AST nodes before extracting the type: update the call
site where resolvedArg is computed (currently using
resolveThroughParamMap(call.Args[1], paramArgMap) before calling
extractTypeFromResponseWithStatus) to run a recursive resolution that walks
composite literals, unary/paren/selector/index/field expressions and replaces
identifier nodes from paramArgMap (and apply the same fix at the other
occurrence around lines 751-757). Concretely, extend or replace
resolveThroughParamMap to traverse the AST node tree for call.Args elements,
resolving identifiers nested inside composite literal fields, slices, maps,
selector expressions, etc., then pass the fully-resolved AST node into
extractTypeFromResponseWithStatus so extractTypeFromResponseWithStatus and
varTypeMap receive the actual concrete argument nodes rather than top-level
wrappers.
- Around line 624-630: The code currently uses StatusCode: 0 to represent an
OpenAPI "default" response which gets serialized as the numeric "0"; instead
introduce an explicit sentinel and serialization path: either add a boolean
field (e.g., ResponseInfo.IsDefault) and set IsDefault: true in the isErrorType
branch, or define a clearly-named constant (e.g., DefaultStatusCode = -1) and
set StatusCode: DefaultStatusCode in that branch; then update the response
generator that consumes []ResponseInfo (the serialization path that reads
ResponseInfo.StatusCode) to emit the string "default" when IsDefault is true or
when StatusCode == DefaultStatusCode. Ensure you modify the isErrorType handling
(where info.Responses is appended) and the ResponseInfo type/serialization code
so no numeric 0 is produced.
- Around line 287-290: Remove the case branches that treat Gin's Context.GetInt,
GetInt64, and GetBool as query parameters: delete or stop calling
a.extractTypedParam for the "GetInt", "GetInt64", and "GetBool" cases (the
switch handling of call → a.extractTypedParam(call, info, goTypeInteger,
"query") and a.extractTypedParam(call, info, goTypeBoolean, "query")). These
methods read Context.Keys (middleware-populated state) and must not be
documented as client query inputs; ensure only query-string accessors (e.g.,
c.Query variants) are handled as "query" parameters in the same switch so that
query extraction remains correct.

In `@internal/extractor/gin/helper_analyzer.go`:
- Around line 87-105: The isErrorType heuristic is too permissive: remove the
hard-coded checks that treat an identifier named "e" and the literal "nil" as
errors; instead only treat an identifier as an error when varTypeMap indicates
its static type is "error" or "*errors.errorString". Update the isErrorType
function to drop the ident.Name == "e" branch and the nil-special-case branch
and rely on the varTypeMap lookup (and you may also remove or optionally keep
the ident.Name == "err" special-case in favor of the varTypeMap) so only
statically-typed errors are recognized.

In `@internal/extractor/gin/schema_extractor.go`:
- Around line 301-314: The embedded selector lookup currently loses package
context because resolveEmbeddedTypeName returns a pkg-qualified name but
e.findTypeSpec falls back to a short-name global scan, causing wrong-type
matches when two packages export the same type; update the lookup flow so that
the full pkg-qualified name from resolveEmbeddedTypeName is used to locate the
TypeSpec within the importing file's package/import context (for example by
adding a new finder like findTypeSpecWithPkgContext or extending findTypeSpec to
accept the originating file/package/import map) and use that to resolve the
embedded type; apply the same change to the identical path at the other
occurrence (around lines 350-357) so promotion/inlining always uses the
import-aware lookup rather than the global short-name scan.
- Around line 271-274: The embedded-field branch currently promotes fields
without checking struct tags; before calling e.promoteEmbeddedFields(field,
schema, visitedMap) invoke the same tag-processing used for named fields
(applyTags) on the embedded field and act on its result: if applyTags indicates
the field is skipped (e.g., json:"-") then continue and do not promote; if
applyTags returns an explicit name/rename (e.g., json:"name") treat the field as
a named field instead of promoting it (i.e., skip promotion and let the
named-field path handle it). Ensure you reference the same helpers used
elsewhere (applyTags, promoteEmbeddedFields, visitedMap, schema, field) so
tagged embedded fields are excluded or renamed consistently.

---

Nitpick comments:
In `@internal/extractor/gin/schema_extractor_test.go`:
- Around line 835-851: Consolidate the duplicate helpers by removing
getSchemaPropKeys and using getSchemaKeys everywhere; update all call sites that
currently call getSchemaPropKeys to call getSchemaKeys instead (the functions
operate on the same openapi3.Schemas type), and delete the redundant
getSchemaPropKeys declaration so only getSchemaKeys remains.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: defc8b49-c027-4ae6-a756-dff069fbdb63

📥 Commits

Reviewing files that changed from the base of the PR and between 63460fc and 22a523b.

⛔ Files ignored due to path filters (1)
  • integration-tests/gin/fixtures/gin-crosspkg/go.sum is excluded by !**/*.sum
📒 Files selected for processing (21)
  • cmd/generate.go
  • docs/plans/2026-04-02-gin-extractor-enhancement.md
  • integration-tests/gin/fixtures/gin-crosspkg/adapter/project.go
  • integration-tests/gin/fixtures/gin-crosspkg/adapter/service.go
  • integration-tests/gin/fixtures/gin-crosspkg/apis/handlers.go
  • integration-tests/gin/fixtures/gin-crosspkg/go.mod
  • integration-tests/gin/fixtures/gin-crosspkg/main.go
  • integration-tests/gin/fixtures/gin-crosspkg/models/types.go
  • integration-tests/gin/fixtures/golden/paths/api-v1-users-get.json
  • integration-tests/gin/fixtures/golden/paths/api-v1-users-id-get.json
  • integration-tests/gin/fixtures/golden/paths/api-v1-users-post.json
  • integration-tests/gin/gin_test.go
  • internal/extractor/gin/ast_parser.go
  • internal/extractor/gin/ast_parser_test.go
  • internal/extractor/gin/generator.go
  • internal/extractor/gin/handler_analyzer.go
  • internal/extractor/gin/helper_analyzer.go
  • internal/extractor/gin/helper_analyzer_test.go
  • internal/extractor/gin/schema_extractor.go
  • internal/extractor/gin/schema_extractor_test.go
  • internal/extractor/types.go

Comment thread docs/plans/2026-04-02-gin-extractor-enhancement.md Outdated
Comment thread internal/extractor/gin/ast_parser.go Outdated
Comment thread internal/extractor/gin/generator.go Outdated
Comment thread internal/extractor/gin/handler_analyzer.go Outdated
Comment thread internal/extractor/gin/handler_analyzer.go Outdated
Comment thread internal/extractor/gin/handler_analyzer.go Outdated
Comment thread internal/extractor/gin/handler_analyzer.go Outdated
Comment thread internal/extractor/gin/response_analyzer.go
Comment thread internal/extractor/gin/schema_extractor.go
Comment thread internal/extractor/gin/schema_extractor.go
Signed-off-by: spencercjh <spencercjh@gmail.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/plans/2026-04-02-gin-extractor-enhancement.md`:
- Around line 56-60: The fenced code blocks showing the handler trace (e.g.,
"handler CreateProject" with "done(c, data, err)" and the "done() calls
c.JSON(200, Rsp{Data: fields[0]})" snippet) are missing language specifiers and
trigger MD040; update those fenced blocks to include an appropriate language
(e.g., ```go or ```text) so they get syntax highlighting; apply the same change
to the other similar block later in the file (the block around the second
occurrence of the handler trace).
- Around line 182-185: The CLI docs show exclusion examples that don't match
actual semantics: update the examples to use exact-path vs prefix flags and the
real config keys—explain that the flag/option exclude-route (and config key
extract.excludeRoutes) performs exact-path matching, while exclude-route-prefix
(or the config key extract.excludeRoutePrefixes) is for prefix-based filtering;
replace `gin.excludePrefixes` with `extract.excludeRoutes` and
`extract.excludeRoutePrefixes` and provide example commands demonstrating both
exact match (e.g., --exclude-route /swagger) and prefix match (e.g.,
--exclude-route-prefix /rjob) consistent with the logic implemented in
internal/config/config.go.

In `@internal/extractor/gin/handler_analyzer.go`:
- Around line 807-809: The type-inference misses header-based parameters because
findContextQueryParam only matches query/path accessors; detect header accessors
(e.g., c.GetHeader("X-Limit") and c.Request.Header.Get("X-Limit")) and feed
their param name into updateParamType. Concretely, extend findContextQueryParam
(or add a new helper like findContextHeaderParam) to recognize
GetHeader/Request.Header.Get call expressions and return the header key, then
call that helper at the same call sites where findContextQueryParam is used (the
invocation preceding updateParamType) so updateParamType can update HeaderParams
for GetHeader-based usages.
- Around line 667-693: traceHelperResponse currently only passes the caller's
varTypeMap into extractTypeFromResponseWithStatus, so helper-local wrapper types
(e.g., a local "rsp" constructed inside helperDecl) are unknown and resolve to
the identifier name; fix by merging the helper's local variable type mappings
into the map used for extraction: after building paramArgMap (using
buildParamArgMap) and before calling extractTypeFromResponseWithStatus,
construct/compute a helperLocalVarMap from helperDecl (scan helperDecl.Body for
local variable declarations/assignments or reuse the existing analyzer logic
that builds var type maps) and merge it into a new combinedVarTypeMap
(combinedVarTypeMap should prefer helper-local types for names declared in
helperDecl and fall back to the caller varTypeMap), then pass combinedVarTypeMap
into extractTypeFromResponseWithStatus so resolveThroughParamMap +
extractTypeFromResponseWithStatus can recognize helper-local wrapper types like
"rsp".
- Around line 736-749: resolveThroughParamMap currently mutates parsed AST nodes
in place (notably *ast.CompositeLit and *ast.KeyValueExpr), which corrupts the
shared helper body across handlers; instead, return new AST nodes and avoid
modifying the original nodes: when you see a *ast.CompositeLit, construct and
return a new &ast.CompositeLit{Type: e.Type, Elts: newElts} where each elt is
the recursively resolved result; when you see a *ast.KeyValueExpr, return a new
&ast.KeyValueExpr{Key: e.Key, Value: resolvedValue}; for *ast.Ident returning
paramToArg[e.Name] return a copy (or a freshly constructed node) rather than the
original if that node can be shared; ensure the default case returns the
original expr unchanged. Update resolveThroughParamMap to build and return new
nodes so HandlerAnalyzer (and its shared helper body) is never modified in
place.

In `@internal/extractor/gin/helper_analyzer.go`:
- Around line 136-156: The extractStatusCode function currently returns 200 for
any unrecognized status expressions, causing unknown http.Status* constants or
local variables to be treated as successful 200 responses; change
extractStatusCode (in helper_analyzer.go) to return a sentinel (e.g., -1) for
unknown/unparsable expressions instead of 200, and update callers such as
extractTypeFromResponseWithStatus to check for this sentinel (status < 0) and
treat it as "unknown" (i.e., do not unwrap as a success payload or assume
success/error branching) so real error-status codes remain on the error-wrapper
path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2ff8e63e-6759-4224-a32f-658d2d246908

📥 Commits

Reviewing files that changed from the base of the PR and between 22a523b and 88bf2d3.

📒 Files selected for processing (10)
  • cmd/generate.go
  • docs/plans/2026-04-02-gin-extractor-enhancement.md
  • internal/config/config.go
  • internal/extractor/gin/ast_parser.go
  • internal/extractor/gin/generator.go
  • internal/extractor/gin/handler_analyzer.go
  • internal/extractor/gin/helper_analyzer.go
  • internal/extractor/gin/helper_analyzer_test.go
  • internal/extractor/gin/schema_extractor.go
  • internal/extractor/gin/schema_extractor_test.go
✅ Files skipped from review due to trivial changes (1)
  • internal/extractor/gin/helper_analyzer_test.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • internal/extractor/gin/schema_extractor_test.go
  • internal/extractor/gin/schema_extractor.go
  • internal/extractor/gin/ast_parser.go
  • internal/extractor/gin/generator.go

Comment thread docs/plans/2026-04-02-gin-extractor-enhancement.md
Comment thread docs/plans/2026-04-02-gin-extractor-enhancement.md
Comment thread internal/extractor/gin/handler_analyzer.go Outdated
Comment thread internal/extractor/gin/handler_analyzer.go Outdated
Comment thread internal/extractor/gin/handler_analyzer.go Outdated
Comment thread internal/extractor/gin/response_analyzer.go
Signed-off-by: spencercjh <spencercjh@gmail.com>
Copilot AI review requested due to automatic review settings April 3, 2026 01:19
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 23 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/extractor/gin/schema_extractor.go
Comment thread internal/extractor/gin/handler_analyzer.go Outdated
Comment thread internal/extractor/gin/helper_analyzer.go Outdated
Comment thread internal/extractor/gin/handler_analyzer.go Outdated
Comment thread internal/extractor/gin/handler_analyzer.go Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (3)
internal/extractor/gin/handler_analyzer.go (3)

712-735: ⚠️ Potential issue | 🟠 Major

Include helper-local variables in traced type resolution.

traceHelperResponse() still passes only the caller's varTypeMap into extractTypeFromResponseWithStatus(). If the helper does rsp := Envelope{Data: data}; c.JSON(..., rsp), extraction degrades to rsp instead of Envelope. Build a second map from helperDecl.Body and merge it before the type lookup.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/extractor/gin/handler_analyzer.go` around lines 712 - 735, The
current scan uses only the caller's varTypeMap when calling
extractTypeFromResponseWithStatus, so helper-local variable types (declared in
helperDecl.Body) are not considered; update traceHelperResponse (or the
surrounding logic where paramArgMap is built and
extractTypeFromResponseWithStatus is invoked) to construct a helper-local var
type map by analyzing helperDecl.Body (similar to how varTypeMap is built),
merge that helper map with the existing varTypeMap (helper map taking precedence
for names local to the helper), and pass the merged map into
extractTypeFromResponseWithStatus so responses like rsp := Envelope{Data: data};
c.JSON(..., rsp) resolve to Envelope instead of rsp.

849-851: ⚠️ Potential issue | 🟠 Major

Header params still never participate in the retyping pass.

Both inference sites call findContextQueryParam(), but that helper only matches Query, DefaultQuery, and Param. strconv.Atoi(c.GetHeader("X-Limit")) and c.GetHeader("X-Debug") == "true" therefore stay documented as string.

#!/bin/bash
# Expected: inspect any matches; they currently won't be retyped by this pass.
python - <<'PY'
from pathlib import Path
import re

patterns = {
    "strconv+GetHeader": re.compile(r'strconv\.(?:Atoi|ParseInt|ParseBool|ParseFloat)\s*\(\s*.*?\.GetHeader\(', re.S),
    "bool-compare+GetHeader": re.compile(r'\.GetHeader\([^)]*\)\s*==\s*"(?:true|false)"', re.S),
}

for path in Path(".").rglob("*.go"):
    try:
        text = path.read_text()
    except Exception:
        continue
    for label, pattern in patterns.items():
        for match in pattern.finditer(text):
            line = text.count("\n", 0, match.start()) + 1
            print(f"{path}:{line}: {label}")
PY

Also applies to: 890-892, 910-927

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/extractor/gin/handler_analyzer.go` around lines 849 - 851, The
retyping pass misses header-based inferences because findContextQueryParam()
only recognizes Query/DefaultQuery/Param; update the analyzer to also detect
headers (e.g., c.GetHeader("X-...") and c.Request.Header.Get or Header["X-..."])
and feed those names into updateParamType just like query params. Concretely,
extend or add a helper (e.g., findContextHeaderParam) and call it alongside
findContextQueryParam in the sites around paramName :=
a.findContextQueryParam(call.Args[0]) (and similar blocks at the other locations
you noted) so that header-derived expressions like
strconv.Atoi(c.GetHeader(...)) and c.GetHeader(...) == "true" produce
appropriate retyping via updateParamType.

778-791: ⚠️ Potential issue | 🔴 Critical

Make parameter substitution a pure deep-clone walk.

resolveThroughParamMap() mutates nodes from the shared parsed helper body and only descends into CompositeLit/KeyValueExpr. That lets one handler's substitutions leak into later analyses, and wrappers like &data still won't resolve to the caller argument. Return cloned wrapper nodes and keep descending instead of editing e in place.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/extractor/gin/handler_analyzer.go` around lines 778 - 791,
resolveThroughParamMap currently mutates nodes from the parsed helper body and
only edits CompositeLit/KeyValueExpr in place, allowing substitutions to leak
and failing to resolve wrappers like &data; change it to perform a pure
deep-clone walk: when encountering an *ast.Ident that maps in paramToArg, return
a cloned copy of the mapped arg (do not overwrite the original Ident), and for
composite nodes (e.g. *ast.CompositeLit, *ast.KeyValueExpr, *ast.UnaryExpr for
address operators, and other container nodes you use) allocate and return new
node instances whose child fields are set to the result of recursively calling
resolveThroughParamMap, instead of mutating e in place; ensure you preserve node
positions/metadata as needed or zero them if not required.
🧹 Nitpick comments (1)
internal/extractor/gin/ast_parser.go (1)

329-337: Precompute normalized excludes/prefixes once per filtering pass.

shouldExcludeRoute re-normalizes extraExcludes for every route, which adds repeated allocations and work. Move normalization outside the loop in filterRoutes and pass precomputed values.

Also applies to: 360-364

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/extractor/gin/ast_parser.go` around lines 329 - 337, The loop in
filterRoutes re-normalizes extraExcludes/extraPrefixes per route via
shouldExcludeRoute, causing repeated allocations; move normalization out of the
loop by computing normalizedExtraExcludes and normalizedExtraPrefixes once at
the start of filterRoutes (using the existing normalization helper), then call
shouldExcludeRoute with those precomputed normalized slices (or add a new
shouldExcludeRouteNormalized that accepts already-normalized values) and remove
per-call normalization inside shouldExcludeRoute; make the same change for the
other occurrence that currently normalizes in-loop (the block around the other
filter usage).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/extractor/gin/ast_parser.go`:
- Around line 373-375: normalizeRoutePath only replaces Gin ':param' syntax via
routeParamReplacer but misses Gin wildcard syntax ('*param'), so filters like
'/files/*path' won't normalize to the same format as convertPathFormat; update
normalizeRoutePath to also replace wildcard segments (e.g., use a second regexp
or extend routeParamReplacer to match `\*([A-Za-z0-9_]+)` and replace with
`{$1}`) so that `*name` is normalized to `{name}`, keeping references to
normalizeRoutePath and routeParamReplacer (or a new routeWildcardReplacer) and
ensuring behavior matches convertPathFormat.

In `@internal/extractor/gin/handler_analyzer.go`:
- Around line 690-698: The current logic returns to a.extractHelperResponse
whenever ident.Name matches the isKnownResponseHelper heuristic (e.g.,
"done"/"respond"/"reply"), preventing lookupHelper from preferring a discovered
helper declaration; change the flow so lookupHelper(a.callerPkg, ident.Name) is
attempted first and, if it finds a helper, call a.traceHelperResponse(helper,
call.Args, info, varTypeMap); only if no helper is found fall back to
isKnownResponseHelper and call a.extractHelperResponse. This keeps
a.lookupHelper, a.traceHelperResponse, a.extractHelperResponse, ident.Name and
call.Args as the referenced symbols to update and ensures wrappers with real
response bodies/statuses are traced from the helper body.
- Around line 43-58: The discoverHelperFuncs loop currently uses
isGinContextType which only recognizes the hardcoded "gin" name; update
discovery to resolve per-file import aliases and pass them to the type check so
imports like "g", "." or other aliases are recognized. Concretely, in
discoverHelperFuncs (and any caller of isGinContextType) build a map of import
aliases from the file's AST (examining file.Imports for alias or default names,
treating dot-imports as matching Context type and resolving package paths for
"github.com/gin-gonic/gin"), then update isGinContextType to accept that alias
map or replace it with a new helper (e.g., isContextTypeWithAliases) that checks
if the parameter type matches any alias (or dot-import) for gin.Context; use the
file-specific alias resolution when deciding to add fn into a.helperFuncs.

In `@internal/extractor/gin/schema_extractor.go`:
- Around line 299-306: The ref built for cross-package embedded types uses the
full qualified name from resolveEmbeddedTypeName (e.g., "models.User") which
yields refs like "#/components/schemas/models.User" that mismatch
fieldToSchemaRef's short-name refs; update the embed-handling in
schema_extractor.go (the block using resolveEmbeddedTypeName and assigning
schema.Properties[parts[0]]) to extract the short type name (substring after the
last '.') from embeddedTypeName before building the ref so it uses
"#/components/schemas/User", matching fieldToSchemaRef and avoiding broken
references.

---

Duplicate comments:
In `@internal/extractor/gin/handler_analyzer.go`:
- Around line 712-735: The current scan uses only the caller's varTypeMap when
calling extractTypeFromResponseWithStatus, so helper-local variable types
(declared in helperDecl.Body) are not considered; update traceHelperResponse (or
the surrounding logic where paramArgMap is built and
extractTypeFromResponseWithStatus is invoked) to construct a helper-local var
type map by analyzing helperDecl.Body (similar to how varTypeMap is built),
merge that helper map with the existing varTypeMap (helper map taking precedence
for names local to the helper), and pass the merged map into
extractTypeFromResponseWithStatus so responses like rsp := Envelope{Data: data};
c.JSON(..., rsp) resolve to Envelope instead of rsp.
- Around line 849-851: The retyping pass misses header-based inferences because
findContextQueryParam() only recognizes Query/DefaultQuery/Param; update the
analyzer to also detect headers (e.g., c.GetHeader("X-...") and
c.Request.Header.Get or Header["X-..."]) and feed those names into
updateParamType just like query params. Concretely, extend or add a helper
(e.g., findContextHeaderParam) and call it alongside findContextQueryParam in
the sites around paramName := a.findContextQueryParam(call.Args[0]) (and similar
blocks at the other locations you noted) so that header-derived expressions like
strconv.Atoi(c.GetHeader(...)) and c.GetHeader(...) == "true" produce
appropriate retyping via updateParamType.
- Around line 778-791: resolveThroughParamMap currently mutates nodes from the
parsed helper body and only edits CompositeLit/KeyValueExpr in place, allowing
substitutions to leak and failing to resolve wrappers like &data; change it to
perform a pure deep-clone walk: when encountering an *ast.Ident that maps in
paramToArg, return a cloned copy of the mapped arg (do not overwrite the
original Ident), and for composite nodes (e.g. *ast.CompositeLit,
*ast.KeyValueExpr, *ast.UnaryExpr for address operators, and other container
nodes you use) allocate and return new node instances whose child fields are set
to the result of recursively calling resolveThroughParamMap, instead of mutating
e in place; ensure you preserve node positions/metadata as needed or zero them
if not required.

---

Nitpick comments:
In `@internal/extractor/gin/ast_parser.go`:
- Around line 329-337: The loop in filterRoutes re-normalizes
extraExcludes/extraPrefixes per route via shouldExcludeRoute, causing repeated
allocations; move normalization out of the loop by computing
normalizedExtraExcludes and normalizedExtraPrefixes once at the start of
filterRoutes (using the existing normalization helper), then call
shouldExcludeRoute with those precomputed normalized slices (or add a new
shouldExcludeRouteNormalized that accepts already-normalized values) and remove
per-call normalization inside shouldExcludeRoute; make the same change for the
other occurrence that currently normalizes in-loop (the block around the other
filter usage).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ad3ea46d-f68c-42f7-9b08-8b1cfeb19700

📥 Commits

Reviewing files that changed from the base of the PR and between 88bf2d3 and a854cea.

📒 Files selected for processing (3)
  • internal/extractor/gin/ast_parser.go
  • internal/extractor/gin/handler_analyzer.go
  • internal/extractor/gin/schema_extractor.go

Comment thread internal/extractor/gin/ast_parser.go
Comment thread internal/extractor/gin/handler_analyzer.go Outdated
Comment thread internal/extractor/gin/handler_analyzer.go Outdated
Comment thread internal/extractor/gin/schema_extractor.go
Signed-off-by: spencercjh <spencercjh@gmail.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

♻️ Duplicate comments (1)
internal/extractor/gin/response_analyzer.go (1)

116-166: ⚠️ Potential issue | 🟠 Major

Unknown status expressions default to 200, altering response semantics.

When extractStatusCode encounters unrecognized http.Status* constants (e.g., http.StatusConflict, http.StatusUnprocessableEntity) or local variables, it returns 200. This causes extractTypeFromResponseWithStatus to treat these as success responses and unwrap Data fields instead of returning the wrapper type.

♻️ Proposed fix to return sentinel for unknown status
 // extractStatusCode extracts HTTP status code from expression.
 func extractStatusCode(expr ast.Expr) int {
 	// Integer literal
 	if lit, ok := expr.(*ast.BasicLit); ok {
 		if lit.Kind == token.INT {
 			var code int
 			if _, err := fmt.Sscanf(lit.Value, "%d", &code); err == nil {
 				return code
 			}
 		}
 	}

 	// http.StatusOK reference
 	if sel, ok := expr.(*ast.SelectorExpr); ok {
 		if x, ok := sel.X.(*ast.Ident); ok && x.Name == "http" {
-			return statusCodeFromName(sel.Sel.Name)
+			code := statusCodeFromName(sel.Sel.Name)
+			if code != -1 {
+				return code
+			}
 		}
 	}

-	return 200 // Default
+	return -1 // Unknown
 }

 // statusCodeFromName converts http.StatusXxx to code.
 func statusCodeFromName(name string) int {
 	switch name {
 	// ... existing cases ...
 	}
-	return 200
+	return -1 // Unknown constant
 }

Then in extractTypeFromResponseInternal, treat statusCode < 0 as "unknown" and skip success/error-specific unwrapping.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/extractor/gin/response_analyzer.go` around lines 116 - 166, The
function extractStatusCode currently returns 200 for any unknown/unsupported
expressions which misclassifies responses; modify extractStatusCode to return a
sentinel negative value (e.g., -1) for unrecognized expressions (including
unknown http.Status* names and non-literal identifiers) and update
statusCodeFromName to return that same sentinel for unmatched names; then update
extractTypeFromResponseInternal to treat statusCode < 0 as "unknown" and skip
the success/error-specific unwrapping logic so wrapper types are preserved for
unknown statuses.
🧹 Nitpick comments (3)
internal/extractor/gin/response_analyzer.go (1)

140-166: Consider adding common HTTP status codes.

statusCodeFromName is missing several frequently-used status codes like StatusConflict (409), StatusUnprocessableEntity (422), StatusTooManyRequests (429), and StatusGone (410). These will fall through to the default return value.

♻️ Suggested additions
 	case "StatusServiceUnavailable":
 		return 503
+	case "StatusConflict":
+		return 409
+	case "StatusGone":
+		return 410
+	case "StatusUnprocessableEntity":
+		return 422
+	case "StatusTooManyRequests":
+		return 429
+	case "StatusMethodNotAllowed":
+		return 405
+	case "StatusNotAcceptable":
+		return 406
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/extractor/gin/response_analyzer.go` around lines 140 - 166,
statusCodeFromName currently omits several common HTTP statuses and falls back
to 200; add explicit cases in the switch inside statusCodeFromName for the
missing symbols: "StatusConflict" => 409, "StatusGone" => 410,
"StatusUnprocessableEntity" => 422, and "StatusTooManyRequests" => 429 so they
return the correct numeric codes instead of defaulting to 200; keep the existing
default return behavior for unknown names.
internal/extractor/gin/type_inference.go (2)

112-124: Variable-to-param name matching may produce false positives.

The logic assumes the variable name equals the query param name. If a variable named enabled is compared to "true" but originates from c.Query("is_enabled"), this won't update the correct param. Conversely, if there's an unrelated enabled variable and a query param also named enabled, it would incorrectly mark that param as boolean.

This is a minor concern since it's a best-effort heuristic, but consider adding a comment documenting this limitation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/extractor/gin/type_inference.go` around lines 112 - 124, The
heuristic in the comparison branch that checks if exprSide is an *ast.Ident and
matches ident.Name against info.QueryParams (in the code around exprSide, ident,
and info.QueryParams) can produce false positives because variable names may not
map to the original query param name; add a concise comment near this block
explaining the limitation (that variable-to-param name matching is a best-effort
heuristic and can misidentify types if the variable was assigned from a
differently-named c.Query/c.Param or is an unrelated local variable) so future
readers understand why this approach is used and its potential inaccuracies.

208-237: inferTypeFromVarName capitalizes unknown variable names, which may not be valid types.

For unknown variable names like myData, the function returns MyData which may not correspond to any actual type. This could pollute the type map with non-existent types.

♻️ Suggested improvement
 func inferTypeFromVarName(varName string) string {
 	if varName == "" {
 		return ""
 	}

 	// Common variable name -> type mappings
 	mappings := map[string]string{
 		"user":   "User",
 		"users":  "User",
 		"req":    "",
 		"result": "",
 		"data":   "",
 		"item":   "",
 		"items":  "",
 	}

 	if typ, ok := mappings[varName]; ok {
 		return typ
 	}

-	// Try to capitalize first letter (heuristic)
-	// e.g., "pageResult" -> "PageResult"
-	if varName[0] >= 'a' && varName[0] <= 'z' {
-		return string(varName[0]-'a'+'A') + varName[1:]
-	}
-
-	return varName
+	// Don't guess types for unknown variable names
+	return ""
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/extractor/gin/type_inference.go` around lines 208 - 237,
inferTypeFromVarName currently capitalizes arbitrary unknown variable names
(e.g., "myData" -> "MyData"), which can introduce non-existent types; change the
logic in inferTypeFromVarName to only return types from the explicit mappings
map (user/users -> "User", etc.) and otherwise return an empty string instead of
applying the capitalization heuristic; update/remove the heuristic branch that
checks varName[0] and ensure callers handle an empty string result, referencing
the inferTypeFromVarName function and the mappings variable to locate and modify
the behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/extractor/gin/helper_tracer_test.go`:
- Around line 176-182: After locating the handler function with the loop that
assigns to fn (the ast.FuncDecl found by comparing f.Name.Name == "GetUsers"),
add a nil check immediately after the loop in each test
(TestParseHelperCall_DoneWithCompositeLit, TestParseHelperCall_NotHelper,
TestParseHelperCall_MethodCallIgnored) to prevent panics — e.g., if fn == nil
then fail the test with t.Fatalf or t.Fatal and a clear message that the
GetUsers handler was not found so subsequent use of fn is safe.

In `@internal/extractor/gin/helper_tracer.go`:
- Around line 115-161: traceHelperResponse currently only uses the caller's
varTypeMap so helper-local variables (e.g., rsp := Wrapper{...}) are invisible;
fix by collecting helper-local variable type mappings from helperDecl.Body (scan
for short/var assignments and composite literals/typed returns to build a
localVarMap), merge localVarMap with the incoming varTypeMap into a
combinedVarMap (local overrides or vice versa as appropriate), and pass
combinedVarMap into extractTypeFromResponseWithStatus instead of varTypeMap;
keep using paramArgMap for resolving call args (resolveThroughParamMap) and
preserve the existing logic that finds c.JSON/c.XML calls in
traceHelperResponse.
- Around line 192-206: resolveThroughParamMap currently mutates AST nodes in
place (modifying CompositeLit.Elts and KeyValueExpr.Value) which corrupts shared
helper declarations; change it to produce and return new AST nodes instead of
mutating originals: for *ast.CompositeLit, build a new slice of elements by
calling resolveThroughParamMap on each elt and return a new
&ast.CompositeLit{Type: e.Type, Elts: newElts} (preserving other fields), for
*ast.KeyValueExpr return a new &ast.KeyValueExpr{Key: e.Key, Value:
resolvedValue} where resolvedValue is the recursive call result, and for
*ast.Ident continue to return the mapped arg; ensure the function always returns
the newly constructed node when a replacement occurs so the caller receives a
non-mutated AST.
- Around line 104-113: The current branch prioritizes isKnownResponseHelper over
a.lookupHelper causing real helper bodies to be ignored; change the order so you
first call a.lookupHelper(a.callerPkg, ident.Name) and if it returns non-nil
invoke a.traceHelperResponse(helper, call.Args, info, varTypeMap) and return,
otherwise fall back to isKnownResponseHelper to call
a.extractHelperResponse(call.Args, info, varTypeMap); keep the same return
behavior and argument variables (ident.Name, call.Args, info, varTypeMap) when
moving the checks.

In `@internal/extractor/gin/type_inference_test.go`:
- Around line 102-127: The test locates the handler function 'getItems' into the
local variable fn but doesn't verify fn is non-nil before calling
analyzer.AnalyzeHandler(fn); add a nil-check after the loop (e.g., if fn == nil)
and call t.Fatalf with a clear message that the target function wasn't found so
AnalyzeHandler is never invoked with a nil 'fn' and the test fails cleanly.

In `@internal/extractor/gin/type_inference.go`:
- Around line 126-148: findContextQueryParam currently only recognizes Query,
DefaultQuery and Param calls so header parameters like c.GetHeader("X-Limit")
(possibly wrapped in strconv.Atoi) are missed; update findContextQueryParam
(method on HandlerAnalyzer) to also handle the "GetHeader" selector and return
the extracted string literal via extractStringLiteral, and consider renaming the
function to findContextParamName if you want broader semantics; ensure
updateParamType's existing HeaderParams lookup will then be reachable for
header-derived names.

---

Duplicate comments:
In `@internal/extractor/gin/response_analyzer.go`:
- Around line 116-166: The function extractStatusCode currently returns 200 for
any unknown/unsupported expressions which misclassifies responses; modify
extractStatusCode to return a sentinel negative value (e.g., -1) for
unrecognized expressions (including unknown http.Status* names and non-literal
identifiers) and update statusCodeFromName to return that same sentinel for
unmatched names; then update extractTypeFromResponseInternal to treat statusCode
< 0 as "unknown" and skip the success/error-specific unwrapping logic so wrapper
types are preserved for unknown statuses.

---

Nitpick comments:
In `@internal/extractor/gin/response_analyzer.go`:
- Around line 140-166: statusCodeFromName currently omits several common HTTP
statuses and falls back to 200; add explicit cases in the switch inside
statusCodeFromName for the missing symbols: "StatusConflict" => 409,
"StatusGone" => 410, "StatusUnprocessableEntity" => 422, and
"StatusTooManyRequests" => 429 so they return the correct numeric codes instead
of defaulting to 200; keep the existing default return behavior for unknown
names.

In `@internal/extractor/gin/type_inference.go`:
- Around line 112-124: The heuristic in the comparison branch that checks if
exprSide is an *ast.Ident and matches ident.Name against info.QueryParams (in
the code around exprSide, ident, and info.QueryParams) can produce false
positives because variable names may not map to the original query param name;
add a concise comment near this block explaining the limitation (that
variable-to-param name matching is a best-effort heuristic and can misidentify
types if the variable was assigned from a differently-named c.Query/c.Param or
is an unrelated local variable) so future readers understand why this approach
is used and its potential inaccuracies.
- Around line 208-237: inferTypeFromVarName currently capitalizes arbitrary
unknown variable names (e.g., "myData" -> "MyData"), which can introduce
non-existent types; change the logic in inferTypeFromVarName to only return
types from the explicit mappings map (user/users -> "User", etc.) and otherwise
return an empty string instead of applying the capitalization heuristic;
update/remove the heuristic branch that checks varName[0] and ensure callers
handle an empty string result, referencing the inferTypeFromVarName function and
the mappings variable to locate and modify the behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f2b1d68a-111c-4e7c-b889-7877461e5e1f

📥 Commits

Reviewing files that changed from the base of the PR and between a854cea and cbae4fd.

📒 Files selected for processing (8)
  • internal/extractor/gin/handler_analyzer.go
  • internal/extractor/gin/handler_analyzer_test.go
  • internal/extractor/gin/helper_tracer.go
  • internal/extractor/gin/helper_tracer_test.go
  • internal/extractor/gin/response_analyzer.go
  • internal/extractor/gin/response_analyzer_test.go
  • internal/extractor/gin/type_inference.go
  • internal/extractor/gin/type_inference_test.go
💤 Files with no reviewable changes (1)
  • internal/extractor/gin/handler_analyzer_test.go
✅ Files skipped from review due to trivial changes (1)
  • internal/extractor/gin/response_analyzer_test.go

Comment thread internal/extractor/gin/helper_tracer_test.go
Comment thread internal/extractor/gin/helper_tracer.go Outdated
Comment on lines +115 to +161
// traceHelperResponse traces into a helper function's body to find c.JSON/c.XML
// response calls and extracts response types by mapping call-site arguments to
// the helper's parameters.
func (a *HandlerAnalyzer) traceHelperResponse(helperDecl *ast.FuncDecl, callArgs []ast.Expr, info *HandlerInfo, varTypeMap map[string]string) {
if helperDecl.Body == nil {
return
}

// Build param-to-argument mapping for the helper function
// e.g., helper(c *gin.Context, data any, err error) called as helper(c, project, nil)
// maps: data → project (with type from varTypeMap), err → nil
paramArgMap := a.buildParamArgMap(helperDecl, callArgs, varTypeMap)

// Scan helper body for c.JSON/c.XML calls
ast.Inspect(helperDecl.Body, func(n ast.Node) bool {
call, ok := n.(*ast.CallExpr)
if !ok {
return true
}
sel, ok := call.Fun.(*ast.SelectorExpr)
if !ok {
return true
}
if _, ok = sel.X.(*ast.Ident); !ok {
return true
}

method := sel.Sel.Name
switch method {
case "JSON", "XML", "YAML":
if len(call.Args) >= 2 {
// Resolve the response argument through param mapping
resolvedArg := a.resolveThroughParamMap(call.Args[1], paramArgMap)
statusCode := extractStatusCode(call.Args[0])
goType := extractTypeFromResponseWithStatus(resolvedArg, varTypeMap, statusCode)
if goType != "" {
info.Responses = append(info.Responses, ResponseInfo{
StatusCode: statusCode,
GoType: goType,
})
slog.Debug("Traced helper response", "helper", helperDecl.Name.Name, "status", statusCode, "type", goType)
}
}
}
return true
})
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Helper-local variables are invisible to type extraction.

traceHelperResponse only passes the caller's varTypeMap. If the helper declares local variables like rsp := Wrapper{Data: data}; c.JSON(200, rsp), the variable rsp won't be found in varTypeMap, degrading extraction to the identifier name rather than the actual type.

♻️ Proposed fix to merge helper-local types
 func (a *HandlerAnalyzer) traceHelperResponse(helperDecl *ast.FuncDecl, callArgs []ast.Expr, info *HandlerInfo, varTypeMap map[string]string) {
 	if helperDecl.Body == nil {
 		return
 	}

+	// Build combined varTypeMap with helper-local variables
+	combinedVarMap := make(map[string]string)
+	for k, v := range varTypeMap {
+		combinedVarMap[k] = v
+	}
+	// Add helper-local variable types (caller types take precedence via param mapping)
+	helperLocalMap := a.buildVarTypeMap(helperDecl.Body)
+	for k, v := range helperLocalMap {
+		if _, exists := combinedVarMap[k]; !exists {
+			combinedVarMap[k] = v
+		}
+	}

 	// Build param-to-argument mapping for the helper function
 	paramArgMap := a.buildParamArgMap(helperDecl, callArgs, varTypeMap)

Then use combinedVarMap instead of varTypeMap in the extractTypeFromResponseWithStatus call.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/extractor/gin/helper_tracer.go` around lines 115 - 161,
traceHelperResponse currently only uses the caller's varTypeMap so helper-local
variables (e.g., rsp := Wrapper{...}) are invisible; fix by collecting
helper-local variable type mappings from helperDecl.Body (scan for short/var
assignments and composite literals/typed returns to build a localVarMap), merge
localVarMap with the incoming varTypeMap into a combinedVarMap (local overrides
or vice versa as appropriate), and pass combinedVarMap into
extractTypeFromResponseWithStatus instead of varTypeMap; keep using paramArgMap
for resolving call args (resolveThroughParamMap) and preserve the existing logic
that finds c.JSON/c.XML calls in traceHelperResponse.

Comment thread internal/extractor/gin/helper_tracer.go
Comment thread internal/extractor/gin/type_inference_test.go
Comment thread internal/extractor/gin/type_inference.go Outdated
…priority, type inference

- resolveThroughParamMap now returns new AST nodes instead of mutating
  shared helper declarations, preventing cross-handler corruption
- parseHelperCall prefers lookupHelper over isKnownResponseHelper so
  traced bodies take priority over name-based heuristics
- findContextQueryParam recognizes GetHeader for header param type inference
- Cross-package embedded type refs strip package prefix to match short-name
  schema component keys (e.g. models.User → User)
- normalizeRoutePath reuses convertPathFormat for wildcard *param handling
- Add nil checks in all test handler lookups to prevent silent panics

Signed-off-by: spencercjh <spencercjh@gmail.com>
Copilot AI review requested due to automatic review settings April 3, 2026 07:55
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 27 out of 28 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +175 to +185
params := helperDecl.Type.Params.List
for i, param := range params {
if i >= len(callArgs) {
break
}
// Skip the first param (*gin.Context) — it's always the context
if i == 0 {
continue
}
for _, name := range param.Names {
paramArgMap[name.Name] = callArgs[i]
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildParamArgMap indexes helperDecl.Type.Params.List by field index (i) and uses that to pick callArgs[i]. This breaks for grouped parameters in a single field (e.g., func helper(c *gin.Context, data, err any)), because both names in that field will be mapped to the same argument, shifting subsequent args and producing incorrect response type resolution. Consider iterating args with a separate parameter-position counter over each individual name (skipping the first *gin.Context param), rather than using the params.List index directly.

Suggested change
params := helperDecl.Type.Params.List
for i, param := range params {
if i >= len(callArgs) {
break
}
// Skip the first param (*gin.Context) — it's always the context
if i == 0 {
continue
}
for _, name := range param.Names {
paramArgMap[name.Name] = callArgs[i]
argPos := 0
skippedContext := false
for _, param := range helperDecl.Type.Params.List {
if len(param.Names) == 0 {
if !skippedContext {
skippedContext = true
} else {
argPos++
}
continue
}
for _, name := range param.Names {
// Skip the first param (*gin.Context) — it's always the context.
if !skippedContext {
skippedContext = true
argPos++
continue
}
if argPos >= len(callArgs) {
return paramArgMap
}
paramArgMap[name.Name] = callArgs[argPos]
argPos++

Copilot uses AI. Check for mistakes.
Comment on lines +136 to +142
sel, ok := call.Fun.(*ast.SelectorExpr)
if !ok {
return true
}
if _, ok = sel.X.(*ast.Ident); !ok {
return true
}
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

traceHelperResponse currently treats any selector call with method JSON/XML/YAML as a Gin response call as long as the receiver is an *ast.Ident. This can misattribute responses if the helper contains other identifiers with JSON/XML/YAML methods. Since the helper’s first parameter is known to be the *gin.Context, it would be more accurate to only match calls where the selector receiver name equals that first parameter name.

Copilot uses AI. Check for mistakes.
Comment on lines +233 to +237
// Phase 1: If we have a package prefix, only search files from that package
if pkgPrefix != "" {
for _, file := range e.files {
if file.Name.Name != pkgPrefix {
continue
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

findTypeSpec tries to constrain cross-package lookups by comparing the type’s package prefix (e.g., models.Usermodels) to file.Name.Name. This only works when the imported identifier matches the target package clause; it fails for aliased imports (e.g., alias.User) or packages whose declared name differs from the import name, and then falls back to a global short-name search (potentially reintroducing collisions). Consider resolving pkgPrefix via the importing file’s import table (alias → actual package name) or tracking types by import path/package directory instead of file.Name.Name.

Copilot uses AI. Check for mistakes.
Comment on lines +596 to +603
// Skip common API prefixes
lower := strings.ToLower(p)
if lower == "api" || strings.HasPrefix(lower, "v") && len(lower) > 1 {
// Check if it's a version like v1, v2, v3
if lower == "api" || (len(lower) == 2 && lower[1] >= '0' && lower[1] <= '9') {
continue
}
}
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inferTag’s version-prefix skipping only ignores v<single-digit> (len==2). Common patterns like /api/v10/... or /api/v1.2/... will not be treated as version segments and will instead become tags (e.g., V10). Consider expanding the check to skip v followed by one-or-more digits (and possibly v<digits>.<digits>), so tag inference remains stable across versioning schemes.

Copilot uses AI. Check for mistakes.
Comment on lines +251 to +275
// pkgNameForImport resolves an import path to its actual package name by finding
// parsed files whose directory matches the import path suffix.
func pkgNameForImport(importPath string, files map[string]*ast.File) string {
// Extract the directory suffix from the import path
// e.g., "gin-crosspkg/apis" → "apis"
lastSlash := strings.LastIndex(importPath, "/")
if lastSlash == -1 {
return ""
}
dirSuffix := "/" + importPath[lastSlash+1:] + "/"

for path, file := range files {
// Check if the file is in a directory matching the import path suffix
// e.g., "/home/user/project/apis/handlers.go" contains "/apis/"
lastDirSlash := strings.LastIndex(path, "/")
if lastDirSlash == -1 {
continue
}
dir := path[:lastDirSlash]
// Check if the directory ends with the expected suffix
if strings.HasSuffix(dir, dirSuffix[:len(dirSuffix)-1]) {
return file.Name.Name
}
}
return ""
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pkgNameForImport attempts to resolve an aliased import by matching only the last path segment (directory suffix) against parsed file paths and then returns the first match from iterating a map. If multiple parsed directories share that suffix (e.g., multiple /apis folders), the chosen package becomes nondeterministic across runs. Consider using the full import path (module-aware) to locate the correct directory, or deterministically selecting among matches (e.g., by sorting candidate paths).

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +20
// inferParamTypes scans the handler body for strconv conversions and conditional
// comparisons that reveal the true type of query/path/header parameters previously
// extracted as string.
//
// Detected patterns:
// - strconv.Atoi(c.Query("offset")) → integer
// - strconv.ParseInt(c.Query("id"), ...) → integer
// - strconv.ParseBool(c.Query("verbose")) → boolean
// - strconv.ParseFloat(c.Query("rate"), ...) → number
// - c.Query("x") == "true" → boolean
func (a *HandlerAnalyzer) inferParamTypes(body *ast.BlockStmt, info *HandlerInfo) {
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description mentions parameter type inference via c.GetInt() / c.GetBool() methods, but the implementation in this PR only infers types from strconv conversions and == "true"/"false" comparisons (no GetInt/GetBool handling found in internal/extractor/gin). Either update the description to match the implemented behavior, or add the missing inference patterns so the feature matches the stated scope.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
internal/extractor/gin/schema_extractor.go (1)

376-386: ⚠️ Potential issue | 🟡 Minor

Required fields from embedded schema may incorrectly include shadowed properties.

Line 385 unconditionally appends all required field names from the embedded schema, but line 379 skips property promotion for names that already exist in the parent. This mismatch can cause:

  1. Incorrect required marking: If parent has "id" (optional) and embedded has "id" (required), the parent's property remains but gets incorrectly marked required.
  2. Duplicate entries: If both schemas have the same required field, the name appears twice in the Required array.
🛠️ Proposed fix to filter required fields
 	// Promote properties from embedded schema into parent
 	if embeddedSchema.Properties != nil {
 		for name, propRef := range embeddedSchema.Properties {
 			if _, exists := parentSchema.Properties[name]; !exists {
 				parentSchema.Properties[name] = propRef
 			}
 		}
 	}

-	parentSchema.Required = append(parentSchema.Required, embeddedSchema.Required...)
+	// Only include required fields that were actually promoted (not shadowed)
+	for _, req := range embeddedSchema.Required {
+		if _, existsInParent := parentSchema.Properties[req]; existsInParent {
+			// Check if this property came from the embedded schema (was promoted)
+			if _, existedBefore := parentSchema.Properties[req]; !existedBefore {
+				continue // Skip - parent already had this property
+			}
+		}
+		// Avoid duplicates
+		alreadyRequired := false
+		for _, existing := range parentSchema.Required {
+			if existing == req {
+				alreadyRequired = true
+				break
+			}
+		}
+		if !alreadyRequired {
+			parentSchema.Required = append(parentSchema.Required, req)
+		}
+	}

A cleaner approach would track which properties were actually promoted:

 	// Promote properties from embedded schema into parent
+	promoted := make(map[string]bool)
 	if embeddedSchema.Properties != nil {
 		for name, propRef := range embeddedSchema.Properties {
 			if _, exists := parentSchema.Properties[name]; !exists {
 				parentSchema.Properties[name] = propRef
+				promoted[name] = true
 			}
 		}
 	}

-	parentSchema.Required = append(parentSchema.Required, embeddedSchema.Required...)
+	for _, req := range embeddedSchema.Required {
+		if promoted[req] {
+			parentSchema.Required = append(parentSchema.Required, req)
+		}
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/extractor/gin/schema_extractor.go` around lines 376 - 386, The code
appends all names from embeddedSchema.Required into parentSchema.Required even
when some properties were not promoted or already exist, causing incorrect
required markings and duplicates; modify the promotion block that iterates
embeddedSchema.Properties to record which property names were actually added
(e.g., collect promotedNames when parentSchema.Properties[name] did not exist
and you assign propRef) and then only append those promotedNames (filtering out
any that already exist in parentSchema.Required to avoid duplicates) to
parentSchema.Required; ensure you reference embeddedSchema.Properties,
parentSchema.Properties and parentSchema.Required when implementing the fix.
🧹 Nitpick comments (3)
internal/extractor/gin/ast_parser.go (2)

315-325: Consider semantic clarity: /favicon.ico is an exact path, not a prefix.

This works correctly due to hasPathSegmentPrefix handling exact matches, but mixing full paths in a "prefixes" list could be confusing for maintainers. Consider either renaming to defaultExcludePatterns or documenting that the list handles both prefixes and exact paths.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/extractor/gin/ast_parser.go` around lines 315 - 325, Rename or
clarify the meaning of defaultExcludePrefixes to avoid confusion between
prefixes and exact paths: either rename the symbol (e.g., defaultExcludePatterns
or defaultExcludePathsAndPrefixes) and update all references, or keep the name
but add a comment noting that hasPathSegmentPrefix handles both prefix and exact
path matches; ensure references to defaultExcludePrefixes and the helper
hasPathSegmentPrefix are updated/annotated so maintainers understand exact-path
entries like "/favicon.ico" are supported.

343-364: Consider hoisting normalizedExcludes computation to avoid repeated allocation.

The normalizedExcludes slice is created for every route. When filtering 100 routes with 10 custom excludes, this creates 100 slices. Moving the normalization to filterRoutes and passing pre-normalized slices would be more efficient.

♻️ Proposed optimization

In filterRoutes, pre-normalize the excludes:

 func filterRoutes(routes []Route, extraExcludes, extraPrefixes []string) []Route {
+	normalizedExcludes := make([]string, len(extraExcludes))
+	for i, exc := range extraExcludes {
+		normalizedExcludes[i] = normalizeRoutePath(exc)
+	}
+	normalizedPrefixes := make([]string, len(extraPrefixes))
+	for i, p := range extraPrefixes {
+		normalizedPrefixes[i] = normalizeRoutePath(p)
+	}
 	var filtered []Route
 	for _, route := range routes {
-		if shouldExcludeRoute(route.FullPath, extraExcludes, extraPrefixes) {
+		if shouldExcludeRoute(route.FullPath, normalizedExcludes, normalizedPrefixes) {

Then update shouldExcludeRoute to skip redundant normalization of the filter inputs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/extractor/gin/ast_parser.go` around lines 343 - 364, The per-route
allocation of normalizedExcludes in shouldExcludeRoute causes repeated work;
pre-normalize the exclude list once in filterRoutes and change
shouldExcludeRoute to accept the already-normalized excludes/prefixes (e.g.,
update its signature used by filterRoutes and other callers to take
normalizedExcludes and normalizedPrefixes or pass normalizedPrefix strings),
remove the normalization loop inside shouldExcludeRoute, and update all call
sites (notably filterRoutes) to compute normalizedExcludes/normalizedPrefixes
via normalizeRoutePath once and pass them in to avoid allocating a new slice per
route.
internal/extractor/gin/type_inference.go (1)

112-123: Variable-name-to-param-name matching is a heuristic with edge cases.

The logic at lines 115-123 assumes that if a variable name matches a query param name and is compared to "true"/"false", the param should be typed as boolean. This could produce false positives in unusual cases:

page := c.Query("page")  // Actually numeric
if page == "true" { ... }  // Bug in user code, but inference marks "page" as boolean

Since this is best-effort inference and such patterns are rare, this is acceptable, but consider adding a debug log when this heuristic triggers to aid troubleshooting:

slog.Debug("Inferred boolean from variable comparison heuristic", "var", ident.Name, "param", info.QueryParams[i].Name)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/extractor/gin/type_inference.go` around lines 112 - 123, The
heuristic in the exprSide == *ast.Ident branch (checking ident.Name against
info.QueryParams[*].Name) can yield false positives; add an explicit debug log
when this heuristic triggers to make such inferences visible for troubleshooting
by calling slog.Debug with a clear message like "Inferred boolean from variable
comparison heuristic" and include fields for the variable name (ident.Name) and
the matched param name (info.QueryParams[i].Name), then keep the existing
assignment info.QueryParams[i].GoType = goTypeBoolean and the existing return.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/extractor/gin/helper_tracer.go`:
- Around line 165-189: The buildParamArgMap function incorrectly uses the loop
index i for both params.List and callArgs, causing grouped parameter names
(param.Names) to map to the same callArg; update buildParamArgMap in
HandlerAnalyzer to use a separate argument index (e.g., argIdx) that increments
for each parameter group entry but is advanced per name within that group so
each name in param.Names consumes a distinct callArgs[argIdx], skip the first
context param as before, and stop when argIdx >= len(callArgs); ensure
param.Names are each mapped to the corresponding callArgs element rather than
reusing callArgs[i].

---

Duplicate comments:
In `@internal/extractor/gin/schema_extractor.go`:
- Around line 376-386: The code appends all names from embeddedSchema.Required
into parentSchema.Required even when some properties were not promoted or
already exist, causing incorrect required markings and duplicates; modify the
promotion block that iterates embeddedSchema.Properties to record which property
names were actually added (e.g., collect promotedNames when
parentSchema.Properties[name] did not exist and you assign propRef) and then
only append those promotedNames (filtering out any that already exist in
parentSchema.Required to avoid duplicates) to parentSchema.Required; ensure you
reference embeddedSchema.Properties, parentSchema.Properties and
parentSchema.Required when implementing the fix.

---

Nitpick comments:
In `@internal/extractor/gin/ast_parser.go`:
- Around line 315-325: Rename or clarify the meaning of defaultExcludePrefixes
to avoid confusion between prefixes and exact paths: either rename the symbol
(e.g., defaultExcludePatterns or defaultExcludePathsAndPrefixes) and update all
references, or keep the name but add a comment noting that hasPathSegmentPrefix
handles both prefix and exact path matches; ensure references to
defaultExcludePrefixes and the helper hasPathSegmentPrefix are updated/annotated
so maintainers understand exact-path entries like "/favicon.ico" are supported.
- Around line 343-364: The per-route allocation of normalizedExcludes in
shouldExcludeRoute causes repeated work; pre-normalize the exclude list once in
filterRoutes and change shouldExcludeRoute to accept the already-normalized
excludes/prefixes (e.g., update its signature used by filterRoutes and other
callers to take normalizedExcludes and normalizedPrefixes or pass
normalizedPrefix strings), remove the normalization loop inside
shouldExcludeRoute, and update all call sites (notably filterRoutes) to compute
normalizedExcludes/normalizedPrefixes via normalizeRoutePath once and pass them
in to avoid allocating a new slice per route.

In `@internal/extractor/gin/type_inference.go`:
- Around line 112-123: The heuristic in the exprSide == *ast.Ident branch
(checking ident.Name against info.QueryParams[*].Name) can yield false
positives; add an explicit debug log when this heuristic triggers to make such
inferences visible for troubleshooting by calling slog.Debug with a clear
message like "Inferred boolean from variable comparison heuristic" and include
fields for the variable name (ident.Name) and the matched param name
(info.QueryParams[i].Name), then keep the existing assignment
info.QueryParams[i].GoType = goTypeBoolean and the existing return.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 952bef27-6f7d-42d0-b4d6-c6bb4b08a31c

📥 Commits

Reviewing files that changed from the base of the PR and between cbae4fd and e9a6957.

📒 Files selected for processing (8)
  • internal/extractor/gin/ast_parser.go
  • internal/extractor/gin/handler_analyzer_test.go
  • internal/extractor/gin/helper_tracer.go
  • internal/extractor/gin/helper_tracer_test.go
  • internal/extractor/gin/response_analyzer_test.go
  • internal/extractor/gin/schema_extractor.go
  • internal/extractor/gin/type_inference.go
  • internal/extractor/gin/type_inference_test.go
✅ Files skipped from review due to trivial changes (2)
  • internal/extractor/gin/type_inference_test.go
  • internal/extractor/gin/response_analyzer_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/extractor/gin/handler_analyzer_test.go

Comment on lines +165 to +189
// buildParamArgMap maps helper function parameter names to their call-site arguments.
// e.g., for helper(c *gin.Context, data any, err error) called as helper(c, project, nil):
//
// "data" → *ast.Ident{Name: "project"}, "err" → *ast.Ident{Name: "nil"}
func (a *HandlerAnalyzer) buildParamArgMap(helperDecl *ast.FuncDecl, callArgs []ast.Expr, _ map[string]string) map[string]ast.Expr {
paramArgMap := make(map[string]ast.Expr)
if helperDecl.Type.Params == nil {
return paramArgMap
}

params := helperDecl.Type.Params.List
for i, param := range params {
if i >= len(callArgs) {
break
}
// Skip the first param (*gin.Context) — it's always the context
if i == 0 {
continue
}
for _, name := range param.Names {
paramArgMap[name.Name] = callArgs[i]
}
}
return paramArgMap
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Incorrect parameter-to-argument mapping when parameters are grouped.

When Go function parameters are grouped (e.g., func helper(c *gin.Context, data, err any)), the params.List entry at index 1 contains both data and err in its Names slice. The current loop uses i as the index into both params and callArgs, incorrectly mapping both names to the same argument.

For helper(c, proj, nil):

  • Expected: data → proj, err → nil
  • Actual: data → proj, err → proj (both map to callArgs[1])
🐛 Proposed fix to track argument index separately
 func (a *HandlerAnalyzer) buildParamArgMap(helperDecl *ast.FuncDecl, callArgs []ast.Expr, _ map[string]string) map[string]ast.Expr {
 	paramArgMap := make(map[string]ast.Expr)
 	if helperDecl.Type.Params == nil {
 		return paramArgMap
 	}

 	params := helperDecl.Type.Params.List
-	for i, param := range params {
-		if i >= len(callArgs) {
-			break
-		}
-		// Skip the first param (*gin.Context) — it's always the context
-		if i == 0 {
-			continue
-		}
-		for _, name := range param.Names {
-			paramArgMap[name.Name] = callArgs[i]
+	argIdx := 0 // Track actual argument position
+	for _, param := range params {
+		for _, name := range param.Names {
+			if argIdx >= len(callArgs) {
+				return paramArgMap
+			}
+			// Skip the first arg (*gin.Context) — it's always the context
+			if argIdx == 0 {
+				argIdx++
+				continue
+			}
+			paramArgMap[name.Name] = callArgs[argIdx]
+			argIdx++
 		}
 	}
 	return paramArgMap
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/extractor/gin/helper_tracer.go` around lines 165 - 189, The
buildParamArgMap function incorrectly uses the loop index i for both params.List
and callArgs, causing grouped parameter names (param.Names) to map to the same
callArg; update buildParamArgMap in HandlerAnalyzer to use a separate argument
index (e.g., argIdx) that increments for each parameter group entry but is
advanced per name within that group so each name in param.Names consumes a
distinct callArgs[argIdx], skip the first context param as before, and stop when
argIdx >= len(callArgs); ensure param.Names are each mapped to the corresponding
callArgs element rather than reusing callArgs[i].

@spencercjh spencercjh merged commit e086d67 into main Apr 3, 2026
9 checks passed
@spencercjh spencercjh deleted the feat/real-gin-project branch April 3, 2026 08:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request go Pull requests that update go code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants