Skip to content

feat: add opt-in field.name strategies and JavaBeans accessor naming - #1494

Merged
tangcent merged 1 commit into
masterfrom
fix/1492-dedupe-public-field-and-getter
Nov 9, 2026
Merged

tangcent merged 1 commit into
masterfrom
fix/1492-dedupe-public-field-and-getter

Conversation

@tangcent

@tangcent tangcent commented Nov 7, 2026 •

Copy link
Copy Markdown
Owner

Fixes #1492

Summary

Thrift-generated models declare a public field (public String Barcode) alongside getBarcode(). easy-yapi exports both, as Barcode and barcode. This PR adds nine opt-in field.name.* extensions for picking a naming strategy, and aligns both the strategy helpers and accessor property-name derivation with the mainstream convention. Enabling field.name.uncapitalize still makes that model export a single barcode property.

What "mainstream" means here

A cross-ecosystem survey (Jackson, Gson, System.Text.Json, Newtonsoft.Json, lodash, change-case, Pydantic, Rails, heck, serde, Symfony, protobuf, plus the Google / Microsoft / Go style guides) shows one convergent rule at the converter layer: a run of capitals is one word, except that its last letter starts the next word when a lower-case letter follows. URLValue therefore becomes url_value / url-value / urlValue / UrlValue / url.value. This is literally the same boundary rule as .NET's JsonSeparatorNamingPolicy, Newtonsoft's ToSeparatedCase, Rails' underscore, Pydantic's to_snake, lodash's words() and Jackson's KebabCaseStrategy (upperCount).

Jackson's SnakeCaseStrategy is the outlier (urlvalue). The legacy tool.camel2Underline keeps that behaviour for rules that call it by name, but built-in extensions now use the mainstream splitter. Style guides disagree on how to write the source identifier (XmlHttpRequest vs ServeHTTP vs IPAddress), but every converter normalizes it to the same word sequence — the divergence is at the naming layer, not the conversion layer.

For accessor names, java.beans.Introspector.decapitalize is the platform rule: two leading capitals are preserved, so getURLValue() -> URLValue. This is also Jackson 3's default (Jackson 2 needs USE_STD_BEAN_NAMING). It is the only choice that lets a URLValue field and its getter collide and de-duplicate; the .NET/JS urlValue spelling would still export two properties.

Changes

  • Nine default-enabled: false extensions, all named after the rule key they configure: field.name.uncapitalize, capitalize, camel-case, snake-case, kebab-case, pascal-case, lower-dot-case, upper-snake-case, upper-kebab-case. Each holds one field.name Groovy rule calling a single tool helper with it.fieldName(). The existing camel-case already produces lowerCamelCase, so no duplicate lower-camel-case extension is added.
  • RuleToolUtils.kt: adds toSnakeCase, toPascalCase and toLowerDotCase alongside toCamelCase / toKebabCase; all five share the nameWords() splitter. camel2Underline is unchanged but is now documented as the Jackson SnakeCaseStrategy legacy and is no longer used by an extension.
  • field.name.snake-case now yields url_value instead of urlvalue. The previously documented intentional snake/kebab acronym difference is removed.
  • BeanPropertyNames.kt: a shared decapitalize / fromAccessor implementing Introspector.decapitalize. DefaultPsiClassHelper uses it for getter and setter collection, and ScriptPsiMethodContext.fieldName() uses it for rules. getURLValue() now resolves to URLValue instead of uRLValue, so an acronym field and its accessor export one property ([BUG] A public field and its getter are exported as two duplicate fields (regression from #1443) #1492 class of bug).
  • Regenerated skills/easy-yapi-assistant/rule-contexts.json / .md via ./gradlew syncRuleContexts (three new tool signatures).
  • Coverage: nine extension scenarios, a 39-extension ledger, RuleToolUtils unit tests per helper, a script-context fieldName() test covering field / getter / setter / is / acronym / plain methods, and an integration test asserting exactly one property for URLValue + getURLValue().

User-facing result

Enable a strategy in Settings -> Extensions. For #1492 with field.name.uncapitalize:

default extension enabled
properties 2 1
exported names Barcode, barcode barcode

For a URLValue field with getURLValue() (no extension needed):

before after
exported names URLValue, uRLValue URLValue

Nothing changes for anyone who leaves the extensions disabled.

Boundaries

  • field.name uses SINGLE aggregation: a project-level .easyapi/*.rules entry (priority 4) overrides a built-in extension (priority 3), and jackson's @JsonProperty rule still wins for explicitly annotated members.
  • Field-first is the existing design: annotations on the getter are not merged when the declared field is exported first. Merging both sources would be a separate feature.
  • capitalize / uncapitalize stay character-level v2 helpers (URLValue -> uRLValue), not naming strategies; use camel-case / pascal-case for acronym-aware results. They are kept because uncapitalize is the [BUG] A public field and its getter are exported as two duplicate fields (regression from #1443) #1492 fix.
  • Jackson 2's default legacy bean mangling (getURL() -> url) is intentionally not reproduced; this PR follows the standard bean naming that Jackson 3 defaults to.
  • Thrift's isSetBarcode() is still treated as a boolean getter and exported as a separate setBarcode property — an independent getter-detection issue (Jackson does the same), out of scope here.

Verification

  • Nine extension scenario tests (including the [BUG] A public field and its getter are exported as two duplicate fields (regression from #1443) #1492 field + getter case, a setter-only property and an acronym field), RuleToolUtils tests pinning both the mainstream and the legacy Jackson outputs, a script-context fieldName() unit test, an acronym accessor integration test, and the 39-extension ledger test pass.
  • ./gradlew syncRuleContexts keeps the generated script-context catalog in sync with the reflected context API (EasyYapiAssistantSkillTest freshness guard passes).
  • ./gradlew test: 747 test classes, 7642 tests, 0 failures, 0 errors (10 skipped).

@github-actions github-actions Bot added the type: bug Something isn't working label Nov 7, 2026
@github-actions

github-actions Bot commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

📦 Plugin has been packaged for this PR. You can download it from the GitHub Actions workflow run by clicking on the "Artifacts" dropdown:

  • easy-yapi-3.2.6.252.0.zip — IntelliJ IDEA 2025.2+ (officially supported)
  • easy-yapi-3.2.6.221.251.zip — IntelliJ IDEA 2022.1 – 2025.1 (best-effort, not verified on older IDEs)

Each artifact is a ready-to-install plugin zip — pick the one matching your IDEA version and install it via Settings → Plugins → ⚙ → Install Plugin from Disk. No extraction needed.

@codecov

codecov Bot commented Nov 7, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 76.36364% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.102%. Comparing base (96939e7) to head (09526d8).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
...n/com/itangcent/easyapi/core/util/RuleToolUtils.kt 80.000% 0 Missing and 8 partials ⚠️
...m/itangcent/easyapi/core/util/BeanPropertyNames.kt 54.545% 0 Missing and 5 partials ⚠️
Additional details and impacted files

Impacted file tree graph

@@              Coverage Diff              @@
##            master     #1494       +/-   ##
=============================================
+ Coverage   70.037%   70.102%   +0.065%     
=============================================
  Files          523       525        +2     
  Lines        30421     30484       +63     
  Branches      7149      7173       +24     
=============================================
+ Hits         21306     21370       +64     
+ Misses        6363      6350       -13     
- Partials      2752      2764       +12     
Flag Coverage Δ
unittests 70.102% <76.364%> (+0.065%) ⬆️

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

Files with missing lines Coverage Δ
...tangcent/easyapi/core/psi/DefaultPsiClassHelper.kt 66.417% <100.000%> (+0.861%) ⬆️
...ent/easyapi/core/rule/context/ScriptPsiContexts.kt 56.471% <100.000%> (+1.077%) ⬆️
...m/itangcent/easyapi/core/util/BeanPropertyNames.kt 54.545% <54.545%> (ø)
...n/com/itangcent/easyapi/core/util/RuleToolUtils.kt 87.770% <80.000%> (-0.886%) ⬇️

... and 6 files with indirect coverage changes


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 96939e7...09526d8. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

github-actions Bot commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

📦 Plugin has been packaged for this PR. You can download it from the GitHub Actions workflow run by clicking on the "Artifacts" dropdown:

  • easy-yapi-3.2.6.252.0.zip — IntelliJ IDEA 2025.2+ (officially supported)
  • easy-yapi-3.2.6.221.251.zip — IntelliJ IDEA 2022.1 – 2025.1 (best-effort, not verified on older IDEs)

Each artifact is a ready-to-install plugin zip — pick the one matching your IDEA version and install it via Settings → Plugins → ⚙ → Install Plugin from Disk. No extraction needed.

@tangcent tangcent changed the title fix: stop exporting a public field and its getter as duplicate properties fix: stop duplicate property exports and restore v2 name transforms Nov 7, 2026
@tangcent
tangcent force-pushed the fix/1492-dedupe-public-field-and-getter branch from 581722b to b312218 Compare November 7, 2026 07:03
@github-actions github-actions Bot added type: new feature Add New Feature and removed type: bug Something isn't working labels Nov 7, 2026
@tangcent tangcent changed the title fix: stop duplicate property exports and restore v2 name transforms feat: restore v2 name transforms as opt-in field.name.* extensions Nov 7, 2026
@github-actions

github-actions Bot commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

📦 Plugin has been packaged for this PR. You can download it from the GitHub Actions workflow run by clicking on the "Artifacts" dropdown:

  • easy-yapi-3.2.6.252.0.zip — IntelliJ IDEA 2025.2+ (officially supported)
  • easy-yapi-3.2.6.221.251.zip — IntelliJ IDEA 2022.1 – 2025.1 (best-effort, not verified on older IDEs)

Each artifact is a ready-to-install plugin zip — pick the one matching your IDEA version and install it via Settings → Plugins → ⚙ → Install Plugin from Disk. No extraction needed.

@tangcent
tangcent force-pushed the fix/1492-dedupe-public-field-and-getter branch from b312218 to e886305 Compare November 7, 2026 07:39
@github-actions

github-actions Bot commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

📦 Plugin has been packaged for this PR. You can download it from the GitHub Actions workflow run by clicking on the "Artifacts" dropdown:

  • easy-yapi-3.2.6.252.0.zip — IntelliJ IDEA 2025.2+ (officially supported)
  • easy-yapi-3.2.6.221.251.zip — IntelliJ IDEA 2022.1 – 2025.1 (best-effort, not verified on older IDEs)

Each artifact is a ready-to-install plugin zip — pick the one matching your IDEA version and install it via Settings → Plugins → ⚙ → Install Plugin from Disk. No extraction needed.

@tangcent
tangcent force-pushed the fix/1492-dedupe-public-field-and-getter branch from e886305 to 7ff2a8d Compare November 7, 2026 08:08
@github-actions

github-actions Bot commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

📦 Plugin has been packaged for this PR. You can download it from the GitHub Actions workflow run by clicking on the "Artifacts" dropdown:

  • easy-yapi-3.2.6.252.0.zip — IntelliJ IDEA 2025.2+ (officially supported)
  • easy-yapi-3.2.6.221.251.zip — IntelliJ IDEA 2022.1 – 2025.1 (best-effort, not verified on older IDEs)

Each artifact is a ready-to-install plugin zip — pick the one matching your IDEA version and install it via Settings → Plugins → ⚙ → Install Plugin from Disk. No extraction needed.

@tangcent
tangcent force-pushed the fix/1492-dedupe-public-field-and-getter branch from 7ff2a8d to d70bded Compare November 8, 2026 12:42
@github-actions

github-actions Bot commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

📦 Plugin has been packaged for this PR. You can download it from the GitHub Actions workflow run by clicking on the "Artifacts" dropdown:

  • easy-yapi-3.2.6.252.0.zip — IntelliJ IDEA 2025.2+ (officially supported)
  • easy-yapi-3.2.6.221.251.zip — IntelliJ IDEA 2022.1 – 2025.1 (best-effort, not verified on older IDEs)

Each artifact is a ready-to-install plugin zip — pick the one matching your IDEA version and install it via Settings → Plugins → ⚙ → Install Plugin from Disk. No extraction needed.

@tangcent
tangcent force-pushed the fix/1492-dedupe-public-field-and-getter branch from d70bded to a36295a Compare November 8, 2026 13:31
@github-actions

github-actions Bot commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

📦 Plugin has been packaged for this PR. You can download it from the GitHub Actions workflow run by clicking on the "Artifacts" dropdown:

  • easy-yapi-3.2.6.252.0.zip — IntelliJ IDEA 2025.2+ (officially supported)
  • easy-yapi-3.2.6.221.251.zip — IntelliJ IDEA 2022.1 – 2025.1 (best-effort, not verified on older IDEs)

Each artifact is a ready-to-install plugin zip — pick the one matching your IDEA version and install it via Settings → Plugins → ⚙ → Install Plugin from Disk. No extraction needed.

v2 shipped selectable naming strategy blocks (#[Jackson_SnakeCaseStrategy],
KebabCaseStrategy, ...) in its recommended config. The v3 rewrite replaced
those blocks with built-in extensions but did not port the strategies, even
though helpers such as capitalize, uncapitalize, toCamelCase and
camel2Underline are already exposed as `tool` in Groovy rules.

Add nine default-disabled field.name.* extensions: uncapitalize, capitalize,
camel-case, snake-case, kebab-case, pascal-case, lower-dot-case,
upper-snake-case and upper-kebab-case. Each is a single field.name Groovy rule
calling one `tool` helper with `it.fieldName()` as input. The existing
camel-case extension already produces lowerCamelCase, so no duplicate
lower-camel-case extension is added.

A cross-ecosystem survey shows one convergent converter rule: a run of
capitals is one word, except that its last letter starts the next word when a
lower-case letter follows (URLValue -> url_value / url-value / urlValue).
This is the boundary used by System.Text.Json, Newtonsoft.Json, Rails,
Pydantic, lodash, heck and Jackson's KebabCaseStrategy. Jackson's
SnakeCaseStrategy (urlvalue) is the outlier; camel2Underline keeps that legacy
behaviour for rules that call it by name, but the extensions use the
mainstream splitter. Style guides disagree on how to write the source
identifier, while converters normalize it to the same word sequence.

Add toSnakeCase, toPascalCase and toLowerDotCase to RuleToolUtils, all sharing
the nameWords() splitter with toCamelCase / toKebabCase. field.name.snake-case
now yields url_value; the previously documented intentional snake/kebab
acronym difference is removed. Three new tool signatures are reflected in the
regenerated script-context catalog.

Add BeanPropertyNames implementing java.beans.Introspector.decapitalize and
use it in DefaultPsiClassHelper (getter/setter collection) and
ScriptPsiMethodContext.fieldName(). getURLValue() now resolves to URLValue
instead of uRLValue, so a URLValue field and its accessor collide and export
one property. This is also the standard bean naming Jackson 3 defaults to.

No new rule key, parser, setting, or config syntax is added. For a generated
class with `public String Barcode` and `getBarcode()`, enabling uncapitalize
maps the field to `barcode`; the getter collides and is skipped by the model
builder's existing name-based de-duplication (#1492).

Fixes: #1492
@tangcent
tangcent force-pushed the fix/1492-dedupe-public-field-and-getter branch from a36295a to 09526d8 Compare November 8, 2026 14:39
@tangcent tangcent changed the title feat: restore v2 name transforms as opt-in field.name.* extensions feat: add opt-in field.name strategies and JavaBeans accessor naming Nov 8, 2026
@github-actions

github-actions Bot commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

📦 Plugin has been packaged for this PR. You can download it from the GitHub Actions workflow run by clicking on the "Artifacts" dropdown:

  • easy-yapi-3.2.6.252.0.zip — IntelliJ IDEA 2025.2+ (officially supported)
  • easy-yapi-3.2.6.221.251.zip — IntelliJ IDEA 2022.1 – 2025.1 (best-effort, not verified on older IDEs)

Each artifact is a ready-to-install plugin zip — pick the one matching your IDEA version and install it via Settings → Plugins → ⚙ → Install Plugin from Disk. No extraction needed.

@tangcent
tangcent merged commit 3be6249 into master Nov 9, 2026
14 checks passed
@tangcent
tangcent deleted the fix/1492-dedupe-public-field-and-getter branch November 9, 2026 23:55
@github-actions

Copy link
Copy Markdown
Contributor

A PR (#1496) has been opened to fix this issue. You can download the packaged plugin from the GitHub Actions workflow run by clicking on the "Artifacts" dropdown:

  • easy-yapi-3.2.7.252.0.zip — IntelliJ IDEA 2025.2+ (officially supported)
  • easy-yapi-3.2.7.221.251.zip — IntelliJ IDEA 2022.1 – 2025.1 (best-effort, not verified on older IDEs)

Each artifact is a ready-to-install plugin zip — pick the one matching your IDEA version and install it via Settings → Plugins → ⚙ → Install Plugin from Disk. No extraction needed.

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

Labels

type: new feature Add New Feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] A public field and its getter are exported as two duplicate fields (regression from #1443)

1 participant