Keep the meaning of enum and static constants compared in conditions - #319
Merged
Merged
Conversation
Comparing a value with a constant accessed through a type (an enum
constant or a static field such as Format.JPG or Limits.max) directly in
an if condition crashed with an NPE: the field read was looked up as an
instance field (this#X), which is not in the context, and the if path
did not fall back to the field read refinement.
Such field reads now use the value from their refinement (e.g.
Format.JPG, or the resolved literal of a static final constant), so the
branches keep the meaning of the comparison, and an unconstrained fresh
value when the constant has no known value.
Enum constants are only represented as Type.CONST when the enum is
declared in the analyzed sources; constants of other enums (e.g.
java.time.DayOfWeek) could not be translated to SMT ("Variable not
found"), so they now carry no information instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add else-if chains over enum constants, arithmetic with a static final constant, a null check or-ed with a constant, and a comparison between two different constants. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rcosta358
approved these changes
Oct 2, 2026
Collaborator
Author
|
Follow-ups from the review, filed as separate bugs: #321 (a local with a constant's name shadows the constant; with this PR that clash inside an 🤖 Generated with Claude Code |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was wrong
Comparing an enum constant or a static constant of another class directly in an
if(if (format == Format.JPG)) crashed the verifier with an NPE (elemRefis null).OperationsChecker.getOperationRefinementsalways looks a field read up asthis#X; such constants are not in context under that name, and inside anifthere was no fallback.What changed
OperationsChecker.getOperationRefinements: when thethis#Xlookup finds nothing for a field read, use the value from the field read's own refinement via the existingvalueFromRefinementhelper: an enum declared in the sources becomesFormat.JPG, astatic finalliteral becomes its value, and anything else becomes an unconstrained fresh value (sound, but carries no information).RefinementTypeChecker.visitCtFieldRead:getDeclaringType().isEnum()is nowgetDeclaringType().getDeclaration() instanceof CtEnum, so only enums declared in the analysed sources are translated to SMT. A JDK enum constant (DayOfWeek.MONDAY) used to give a false "Variable 'DayOfWeek.MONDAY' could not be found" error (e.g.boolean b = d == DayOfWeek.MONDAY;); now it carries no information.Tests
testSuite/CorrectEnumConstantInCondition.javaandtestSuite/ErrorEnumConstantInCondition.java, one violation per method in the error file. Both crash with the NPE onmain.!=, constant on the left,&&and||, else-if chains (the finalelseis known to beGIF), a userstatic final(alone and in arithmetic), a JDKstatic final, a JDK enum, and a non-final static field.!=,||, else-if chain,null || constant, two different constants (Format.JPG == Format.PNG, so the else-branch is checked), a wrongstatic finalvalue, arithmetic, and the else-branch of a JDK enum comparison (still checked).mvn test: 345/345 onmain, 347/347 on this branch.Review
An adversarial review ran 40 small programs on
mainand on this branch:mainaccepted is now rejected, and no new crashes. Of the 9 programs that crashed onmain, 8 (enum constants inif, else-if chains, constant on both sides, enum from another package, a field compared to a constant, JDK enum) now verify or report the expected error. The ninth is the local-name clash described under follow-ups. One false error onmainis gone (JDK enum in a boolean local).n + Limits.max > 15), ternaries,!,&&/||,return, boolean locals, a parameter named like the constant, and a static (final or not) field of the current class all behave the same as other comparisons.elemRef == null && elemVar instanceof CtFieldRead) only changes field reads that used to crash in anif. Outside anif, the old path created an instance ofthis#Xfrom the same refinement, so the result is equivalent. The only difference is that it no longer adds a straythis#Xto the context. Keying on static-ness instead would not fix the name-clash problems below, because they come fromvisitCtFieldRead.null ||, two constants).Known limitations / follow-ups (pre-existing on
main, not changed here)visitCtFieldRead, first branch:_ == fieldNamewhen the location does not match). WithFormat JPG = Format.PNG;as a local,Format.JPGmeans the local, andonlyPng(Format.JPG)-style violations are accepted. Onmainthis already happens outside anif(Format g = Format.JPG; onlyPng(g);is accepted). Inside anif,maincrashed and this branch accepts. Candidate fix: only use the context variable when its location matches the target, and otherwise fall through. That removes 5 lines, keeps all 347 tests green, and fixes the 3 cases. It is left out because it changes field reads in general, not just in conditions.X.fresolves tothis#fwhen the current class has a fieldf(both invisitCtFieldReadand in thethis#Xlookup here). SoFormat.JPGis read asthis.JPGwhen the class has a field namedJPG,other.valis read asthis.val, and a field shadowed by a nestedLimits.maxloses its refinement. All of these are accepted unsoundly, onmainas well.int mode = 0;withvoid set() { mode = 3; }, thenif (n == mode)provesn == 0). This holds for instance fields and non-final statics.static final int X = compute();gives a false "Variable '#compute_1' could not be found" error.static final int MAX: "Invalid ghost declaration" (static finalfield plus a refinement gives a spurious "Invalid ghost declaration" #307). Open PR Exclude Static Final Constants from Instance Field Ghosts #313 does not conflict with this branch (they touch different files). With both merged,if (n == Limits.MAX)verifies the correct case and reports the wrong one. The tests use a lowercasemaxuntilstatic finalfield plus a refinement gives a spurious "Invalid ghost declaration" #307 is fixed.Fixes #302
🤖 Generated with Claude Code