Skip to content

fix: use raw enum values for cross-enum conflict detection - #2390

Merged
mromaszewicz merged 9 commits into
oapi-codegen:mainfrom
natalie-o-perret:fix/enum-conflict-detection-order-dependent
Jun 20, 2026
Merged

mromaszewicz merged 9 commits into
oapi-codegen:mainfrom
natalie-o-perret:fix/enum-conflict-detection-order-dependent

Conversation

@natalie-o-perret

@natalie-o-perret natalie-o-perret commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Fixes #2391.

Cross-enum conflict detection uses e1.GetValues() to check for shared values, but GetValues() returns prefixed names for enums already marked in an earlier loop iteration. Those prefixed names don't match the raw values of later enums, so some conflicts are silently missed, and whether an enum gets prefixed depends on iteration order rather than actual value overlap.

Fix: compare e1.Schema.EnumValues keys instead. These are always the unprefixed identifiers, regardless of whether the enum has been marked yet.

Regression test added (TestEnumConflictDetectionOrderIndependent): three string enums where A↔B and A↔C both conflict, but the A↔C conflict was previously missed once A was already prefixed.

Discovered via a downstream build failure after an upstream schema change removed a schema that had been the only direct conflict anchor for one of the enums.

@natalie-o-perret
natalie-o-perret force-pushed the fix/enum-conflict-detection-order-dependent branch 2 times, most recently from ddaed3d to 00d47d2 Compare June 1, 2026 16:22
@natalie-o-perret
natalie-o-perret marked this pull request as ready for review June 1, 2026 16:26
@natalie-o-perret
natalie-o-perret requested a review from a team as a code owner June 1, 2026 16:26
@greptile-apps

greptile-apps Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR makes enum conflict detection use stable enum value names. It changes:

  • Adds cross-enum conflict detection based on Schema.EnumValues.
  • Keeps a compatibility option to restore the old conflict-resolution behavior.
  • Adds config schema support for the new compatibility option.
  • Updates regression coverage for order-independent enum prefixing.
  • Regenerates affected issue test output with prefixed enum constants.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
pkg/codegen/codegen.go Updates enum conflict detection to iterate to a stable set of prefixed enum constants.
pkg/codegen/configuration.go Adds the compatibility flag for disabling the new enum value conflict resolution.
configuration-schema.json Exposes the new compatibility flag in the published configuration schema.
pkg/codegen/codegen_test.go Adds tests for order-independent enum conflict detection.

Reviews (3): Last reviewed commit: "Simplify enum value de-dup" | Re-trigger Greptile

@natalie-o-perret

Copy link
Copy Markdown
Contributor Author

Summoning @jamietanna for review

@mromaszewicz mromaszewicz added the bug Something isn't working label Jun 1, 2026
GetValues() returns prefixed identifier names for enums already marked
as conflicting. Using it in the cross-enum comparison loop makes the
result order-dependent: an enum that was prefixed in an earlier iteration
produces keys like FooStateReady, which no longer match the unprefixed
key Ready in a later enum, so the later enum misses the conflict and
stays unprefixed.

Fix by comparing Schema.EnumValues keys directly, which are always the
raw (unprefixed) Go-safe identifiers regardless of PrefixTypeName state.
@natalie-o-perret
natalie-o-perret force-pushed the fix/enum-conflict-detection-order-dependent branch from 7fa6bd9 to 930146a Compare June 1, 2026 18:08
@mromaszewicz

Copy link
Copy Markdown
Member

CI failed because your change affects existing generated code. I pulled your PR locally and I'm investigating, but we might need to feature-flag this to avoid breaking existing users.

@mromaszewicz

mromaszewicz commented Jun 1, 2026

Copy link
Copy Markdown
Member

I had Claude dig into this locally:

Why the files change

This PR changes conflict detection from comparing generated constant names (GetValues(), which reflects prefixing) to comparing raw schema values (Schema.EnumValues). That fixes the order-dependence in #2391, but it also drops a second class of conflict the old code happened to catch: an enum whose raw value equals another enum's prefixed constant name.

internal/test/issues/issue-1189 — correct improvement ✅

Three enums all share raw values [bar, foo]: TestFieldA1, TestFieldB, TestFieldC1. All three should be prefixed. The old order-dependent code marked A1 and B, then — because GetValues() returned their already-prefixed names — failed to see that C1 still collided, leaving it unprefixed (Bar/Foo). The new code compares raw values, so C1 is now correctly prefixed to TestFieldC1Bar/TestFieldC1Foo. This is exactly the bug #2391 describes. Compiles fine, output is now consistent.

internal/test/components — regression ❌ (won't compile)

This spec was deliberately built to test the other collision class. From components.yaml:

  • Enum1: [One, Two, Three] — conflicts with Enum2, so it's prefixed → constant Enum1One = "One"
  • Enum3: [Enum1One, Foo, Bar] — its raw value is literally Enum1One (the comment even says "Enum values conflict with Enums above, need to be prefixed")

The old GetValues() code compared post-prefix names, so it saw Enum1's prefixed constant Enum1One collide with Enum3's value Enum1One and prefixed Enum3 → Enum3Enum1One. The new raw-value code only sees Enum3's raw values [Enum1One, Foo, Bar] vs Enum1's raw [One, Two, Three] — no overlap — so Enum3 is left unprefixed. Result:

Enum1One Enum1 = "One"        // line 16
Enum1One Enum3 = "Enum1One"   // line 59  ← duplicate
components/components.gen.go:59:2: Enum1One redeclared in this block
components/components.gen.go:68:7: invalid case Enum1One in switch ... (mismatched types Enum1 and Enum3)

A correct fix needs to detect both classes — raw-value overlap and the case where one enum's value matches another enum's would-be prefixed constant name —
while remaining order-independent. The current PR can't land as-is because internal/test/components no longer compiles.

@mromaszewicz

Copy link
Copy Markdown
Member

@natalie-o-perret - This code is all extremely sensitive.

We can make new behavior default-on, but there definitely needs to be a flag to revert to current behavior, since I don't see a way of fixing this properly without breaking some old boilerplate.

@natalie-o-perret
natalie-o-perret force-pushed the fix/enum-conflict-detection-order-dependent branch from 1fb4e36 to be163f0 Compare June 1, 2026 18:31
@natalie-o-perret

natalie-o-perret commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

@mromaszewicz

CI failed because your change affects existing generated code. I pulled your PR locally and I'm investigating, but we might need to feature-flag this to avoid breaking existing users.

Thanks for the thorough write-up. I think you may have pulled before the second force push.

The current commit uses a 2-stage algorithm that handles both conflict classes.

internal/test/issues/issue-1189 -- correct improvement ✅

Agreed, that's exactly the bug this PR is fixing.

internal/test/components -- regression ❌ (won't compile)

This is handled by Stage 2. After Stage 1 marks Enum1 as prefixed (it shares raw values with Enum2), Stage 2 runs an iterative GetValues() comparison. At that point Enum1.GetValues() returns {"Enum1One": "One", ...} while Enum3.GetValues() (still unprefixed) returns {"Enum1One": "Enum1One", ...}. The key Enum1One appears in both, so Enum3 gets marked for prefixing too.

go test ./... passes and regenerating internal/test/components produces Enum3Bar, Enum3Enum1One, Enum3Foo with no compile errors. Could you try pulling again? Happy to debug further if you're still seeing the issue.


@natalie-o-perret - This code is all extremely sensitive.

We can make new behavior default-on, but there definitely needs to be a flag to revert to current behavior, since I don't see a way of fixing this properly without breaking some old boilerplate.

Fair point, I've added a compatibility.old-enum-conflict-detection: true flag (latest commit e13b895) that restores the pre-v2.7.1 GetValues()-based behavior. New behavior stays default-on; users who need to preserve existing generated output can opt out.

@mromaszewicz

Copy link
Copy Markdown
Member

@greptileai re-review.

Comment thread pkg/codegen/codegen.go
// already-applied prefixes, so enums processed later may miss conflicts
// with enums that were prefixed in an earlier iteration. Preserved here
// as an escape hatch for users who need to keep existing generated output.
for i := range enums {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If I understand this right, the next loop completely supersedes this one, so this one doesn't even need to run.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not quite, Stage 2 alone misses CState in the AState/BState/CState case.

Once Stage 2's first pass prefixes AState because it shares "running" with BState, AState.GetValues() returns {AStateRunning, AStateMigrating}.

When it then checks AState vs CState, "migrating" in CState no longer matches any key in AState's now-prefixed values, so CState is left unprefixed.

I verified this by removing Stage 1 and running the tests, both TestEnumConflictDetectionOrderIndependent and TestEnumConflictDetectionBothOrders fail with Migrating CState = "migrating" in the output.

Comment thread pkg/codegen/configuration.go Outdated
Comment thread pkg/codegen/configuration.go Outdated
@mromaszewicz

Copy link
Copy Markdown
Member

Sorry for the delay, I am revisting this now.

We can't change the meaning of old flags if we can help it.

I will push a change to default-on your change, but allow disabling it via disable-enum-value-conflict-resolution

mromaszewicz and others added 2 commits June 19, 2026 22:22
Gate enum value conflict resolution behind a new
`disable-enum-value-conflict-resolution` option (default on) instead of
overloading `old-enum-conflicts`, and revert the unrelated configuration
comment churn.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The conflict-resolution path had two loops: a "Stage 1" pass comparing raw
enum values, followed by a "Stage 2" fixed-point loop comparing effective
(possibly prefixed) constant names.

Stage 1 was redundant. For an unprefixed enum, GetValues() returns the raw
EnumValues, so Stage 2's first iteration already performs exactly the raw
comparison Stage 1 did. The two passes only diverge when a single
GenerateEnums call mixes already-prefixed and unprefixed enums, which never
happens at either call site: server-URL enums are all force-prefixed (and
generated in isolation), the main call contains none of them, and
always-prefix-enum-values is global. So dropping Stage 1 leaves the
generated output unchanged.

Collapse to the single fixed-point loop, which is genuinely required: a
prefixed name (e.g. "Enum1One") can collide with another enum's raw value,
so resolution must iterate until stable. Also operate on the slice elements
directly instead of the copy/write-back dance, and factor the overlap test
into enumsShareConstantName (which now builds each side's value map once per
pair rather than rebuilding the second one per key).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mromaszewicz mromaszewicz added the notable changes Used for release notes to highlight these more highly label Jun 20, 2026
@mromaszewicz
mromaszewicz merged commit b93bf8c into oapi-codegen:main Jun 20, 2026
15 checks passed
lwc pushed a commit to lwc/oapi-codegen that referenced this pull request Jun 23, 2026
…gen#2390)

* fix: use raw enum values for cross-enum conflict detection

GetValues() returns prefixed identifier names for enums already marked
as conflicting. Using it in the cross-enum comparison loop makes the
result order-dependent: an enum that was prefixed in an earlier iteration
produces keys like FooStateReady, which no longer match the unprefixed
key Ready in a later enum, so the later enum misses the conflict and
stays unprefixed.

Fix by comparing Schema.EnumValues keys directly, which are always the
raw (unprefixed) Go-safe identifiers regardless of PrefixTypeName state.

* test: add order-independence test for both schema orderings

* feat: add old-enum-conflict-detection compatibility flag

* fix: reuse old-enum-conflicts flag for legacy conflict detection path

* clean up configuration

Gate enum value conflict resolution behind a new
`disable-enum-value-conflict-resolution` option (default on) instead of
overloading `old-enum-conflicts`, and revert the unrelated configuration
comment churn.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Simplify enum value de-dup

The conflict-resolution path had two loops: a "Stage 1" pass comparing raw
enum values, followed by a "Stage 2" fixed-point loop comparing effective
(possibly prefixed) constant names.

Stage 1 was redundant. For an unprefixed enum, GetValues() returns the raw
EnumValues, so Stage 2's first iteration already performs exactly the raw
comparison Stage 1 did. The two passes only diverge when a single
GenerateEnums call mixes already-prefixed and unprefixed enums, which never
happens at either call site: server-URL enums are all force-prefixed (and
generated in isolation), the main call contains none of them, and
always-prefix-enum-values is global. So dropping Stage 1 leaves the
generated output unchanged.

Collapse to the single fixed-point loop, which is genuinely required: a
prefixed name (e.g. "Enum1One") can collide with another enum's raw value,
so resolution must iterate until stable. Also operate on the slice elements
directly instead of the copy/write-back dance, and factor the overlap test
into enumsShareConstantName (which now builds each side's value map once per
pair rather than rebuilding the second one per key).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Marcin Romaszewicz <marcinr@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mromaszewicz mromaszewicz removed the notable changes Used for release notes to highlight these more highly label Jul 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auto:no-mentions bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: cross-enum conflict detection is order-dependent due to GetValues() returning prefixed names

3 participants