Skip to content

fix: keep final instance fields during type resolution - #155

Open
tangcent wants to merge 1 commit into
mainfrom
feature/issue-141-final-fields
Open

tangcent wants to merge 1 commit into
mainfrom
feature/issue-141-final-fields

Conversation

@tangcent

@tangcent tangcent commented Oct 1, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #141.

Type resolution skipped every field marked IsStatic || IsFinal, so DTOs whose members are declared final exported with an empty field map and no warning. The intent was presumably to skip compile-time constants, but final is also how immutable DTOs are written, and it is what Lombok generates:

@Value
public class CreateUserRequest {
	String name;   // Lombok makes these final
	String email;
}
public class Product {
	private final String sku;
	private final BigDecimal price;
}

Both resolve to an object model with no fields at all. The distinction that matters is not final but "is this a compile-time constant": a static final field with an initializer is a constant, an instance final field is data. Since statics are skipped regardless, the correct rule is to skip only IsStatic — final instance fields then resolve and export like any other field.

The same filter existed in the maven dependency resolver, so a Lombok @Value DTO coming from a dependency JAR hit the identical bug; it gets the same rule.

This PR also follows up on the coupling left when #139 landed: record components are implicitly private final (JLS 8.10.3) and were deliberately left unmarked only because the resolver used to drop final fields — they are now marked accurately.

Changes

File Change
api-collector-java/resolver/resolver.go The field loop in Resolve drops the IsFinal condition and skips only IsStatic, which already covers static final constants with or without an initializer. A comment records the constant-vs-data distinction so the old rule is not reintroduced
api-collector-java/maven/dep_resolver.go MavenDependencyResolver.ResolveType applies the same static-only filter when flattening dependency classes into ResolvedType fields
api-collector-java/parser/types.go Field gains HasInitializer — whether the declaration carries an = value part — so a static final field with an initializer is recognizable as a compile-time constant
api-collector-java/parser/extractor.go extractField reads initializer presence off the tree-sitter declarator's labeled value field (ChildByFieldName("value"); the grammar has no initializer node kind — the value is an array_initializer or an expression). extractRecordComponents sets IsFinal: true on record components and its comment no longer describes working around the old resolver rule
api-collector-java/testdata/ImmutableProduct.java Lombok @Value-style immutable DTO fixture (sku/price as final instance fields) that also carries a real public static final String TYPE = "product" constant
api-collector-java/resolver/resolver_test.go TestResolve_ImmutableFinalFields: the all-final DTO exports sku (string) and price (BigDecimal → double) while TYPE stays excluded. The inline record tests now set IsFinal: true on their fields, matching what the parser emits since #139's follow-up in this PR
api-collector-java/parser/parser_test.go TestParser_ParseImmutableProduct over the fixture: TYPE is static + final + with initializer, sku/price are final instance fields without one. TestParser_ParseRecord additionally asserts record components are marked final
api-collector-java/maven/dep_resolver_test.go TestMavenDependencyResolver_ResolveTypeFieldFiltering: dependency classes go through the same rule — statics dropped, final instance fields and plain fields kept

Verification

  • go vet ./... clean and go test ./... passes for api-collector-java; go test ./... passes for apilot-cli (the only dependent module), golden files unaffected — this change is Java-collector-only and the CLI golden project is Go.
  • Acceptance criteria from the issue:
    • A class whose instance fields are final exports all of them (TestResolve_ImmutableFinalFields, TestParser_ParseImmutableProduct).
    • static final constants remain excluded (the same tests assert TYPE stays out of the exported field map).
    • No change to classes that have neither modifier (full suite green untouched).

Type resolution skipped every IsStatic || IsFinal field, so DTOs whose
members are declared final exported with an empty field map and no
warning. The intent was to skip compile-time constants, but final is
also how immutable DTOs are written and what Lombok @value generates;
what distinguishes a constant from data is staticness, not finality.

Fields are now skipped only when static (which already covers static
final constants regardless of initializer); final instance fields
resolve and export like any other field. The same rule is applied to
dependency-resolved classes, which went through an identical filter.

Per-module changes:

- api-collector-java/resolver: the field loop in Resolve drops the
  IsFinal condition and only skips IsStatic, with a comment recording
  the constant-vs-data distinction so it is not reintroduced.
- api-collector-java/maven: MavenDependencyResolver.ResolveType applies
  the same static-only filter when flattening dependency classes into
  ResolvedType fields.
- api-collector-java/parser: record components are marked IsFinal (they
  are implicitly private final fields per JLS 8.10.3 and were left
  unset only because the resolver used to drop final fields), and
  Field gains HasInitializer, read off the tree-sitter declarator's
  labeled `value` field, so a static final field with an initializer
  is recognizable as a compile-time constant.

Acceptance criteria verified:

- A class whose instance fields are final exports all of them
  (TestResolve_ImmutableFinalFields, TestParser_ParseImmutableProduct).
- static final constants remain excluded (same tests assert 'TYPE'
  stays out of the exported field map).
- Classes with neither modifier are unchanged (existing resolver,
  springmvc, and apilot-cli golden tests pass untouched).

Tests: TestResolve_ImmutableFinalFields and record-field IsFinal
assertions in resolver_test.go; TestParser_ParseImmutableProduct plus
record-component finality in parser_test.go with the ImmutableProduct
testdata fixture; TestMavenDependencyResolver_ResolveTypeFieldFiltering
in maven. go vet and go test pass for api-collector-java and
apilot-cli (golden files unaffected).
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown

📦 Build artifact for this PR is available in the GitHub Actions workflow run under Artifacts.

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 71.816%. Comparing base (367e792) to head (f04515b).

Additional details and impacted files

Impacted file tree graph

@@              Coverage Diff              @@
##              main      #155       +/-   ##
=============================================
+ Coverage   71.594%   71.816%   +0.223%     
=============================================
  Files           66        66               
  Lines        12381     12383        +2     
=============================================
+ Hits          8864      8893       +29     
+ Misses        2932      2905       -27     
  Partials       585       585               
Flag Coverage Δ
unittests 71.816% <100.000%> (+0.223%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
api-collector-java/maven/dep_resolver.go 58.333% <100.000%> (+19.444%) ⬆️
api-collector-java/parser/extractor.go 86.981% <100.000%> (+1.187%) ⬆️
api-collector-java/resolver/resolver.go 77.510% <100.000%> (+0.803%) ⬆️

Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 367e792...f04515b. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] [Java] static and final fields are skipped during type resolution

2 participants