diff --git a/checker/src/main/java/org/checkerframework/checker/calledmethods/builder/AutoValueSupport.java b/checker/src/main/java/org/checkerframework/checker/calledmethods/builder/AutoValueSupport.java index 4f44b30fac7a..2dee12a05cff 100644 --- a/checker/src/main/java/org/checkerframework/checker/calledmethods/builder/AutoValueSupport.java +++ b/checker/src/main/java/org/checkerframework/checker/calledmethods/builder/AutoValueSupport.java @@ -10,6 +10,7 @@ import org.checkerframework.framework.util.AnnotatedTypes; import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.InternalUtils; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; import org.checkerframework.javacutil.UserError; @@ -115,7 +116,7 @@ public void handleBuilderBuildMethod(AnnotatedExecutableType builderBuildType) { @Override public boolean isToBuilderMethod(ExecutableElement candidateToBuilderElement) { - if (!"toBuilder".equals(candidateToBuilderElement.getSimpleName().toString())) { + if (!InternalUtils.sameName(candidateToBuilderElement.getSimpleName(), "toBuilder")) { return false; } diff --git a/checker/src/main/java/org/checkerframework/checker/calledmethods/builder/LombokSupport.java b/checker/src/main/java/org/checkerframework/checker/calledmethods/builder/LombokSupport.java index 6719d7a9798f..cc80c9d9df9d 100644 --- a/checker/src/main/java/org/checkerframework/checker/calledmethods/builder/LombokSupport.java +++ b/checker/src/main/java/org/checkerframework/checker/calledmethods/builder/LombokSupport.java @@ -8,6 +8,7 @@ import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.InternalUtils; import java.beans.Introspector; import java.util.ArrayList; @@ -132,7 +133,7 @@ public void handleBuilderBuildMethod(AnnotatedExecutableType builderBuildType) { @Override public boolean isToBuilderMethod(ExecutableElement candidateToBuilderElement) { - return candidateToBuilderElement.getSimpleName().contentEquals("toBuilder") + return InternalUtils.sameName(candidateToBuilderElement.getSimpleName(), "toBuilder") && (ElementUtils.hasAnnotation(candidateToBuilderElement, "lombok.Generated") || ElementUtils.hasAnnotation( candidateToBuilderElement.getEnclosingElement(), diff --git a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationFieldAccessTreeAnnotator.java b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationFieldAccessTreeAnnotator.java index f715c3585bd0..b612a349263d 100644 --- a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationFieldAccessTreeAnnotator.java +++ b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationFieldAccessTreeAnnotator.java @@ -10,6 +10,7 @@ import org.checkerframework.framework.type.treeannotator.TreeAnnotator; import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.BugInCF; +import org.checkerframework.javacutil.InternalUtils; import org.checkerframework.javacutil.TreeUtils; import javax.lang.model.element.Element; @@ -76,8 +77,8 @@ private void computeFieldAccessType(ExpressionTree tree, AnnotatedTypeMirror typ // (e.g., constructor calls or uses of an outer this). if (tree instanceof IdentifierTree) { IdentifierTree identTree = (IdentifierTree) tree; - if (identTree.getName().contentEquals("this") - || identTree.getName().contentEquals("super")) { + if (InternalUtils.isThisName(identTree.getName()) + || InternalUtils.isSuperName(identTree.getName())) { return; } } diff --git a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationParentAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationParentAnnotatedTypeFactory.java index c66e93ee2066..7836820929da 100644 --- a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationParentAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationParentAnnotatedTypeFactory.java @@ -46,6 +46,7 @@ import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.InternalUtils; import org.checkerframework.javacutil.TreePathUtil; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; @@ -414,7 +415,7 @@ protected boolean isUnused( Name when = AnnotationUtils.getElementValueClassName(unused, unusedWhenElement); for (AnnotationMirror anno : receiverAnnos) { Name annoName = ((TypeElement) anno.getAnnotationType().asElement()).getQualifiedName(); - if (annoName.contentEquals(when)) { + if (InternalUtils.sameName(annoName, when)) { return true; } } diff --git a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationTransfer.java b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationTransfer.java index aeabe0260974..e91dab39ab0c 100644 --- a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationTransfer.java @@ -18,6 +18,7 @@ import org.checkerframework.dataflow.expression.JavaExpression; import org.checkerframework.framework.flow.CFAbstractTransfer; import org.checkerframework.framework.flow.CFValue; +import org.checkerframework.javacutil.InternalUtils; import org.checkerframework.javacutil.TreePathUtil; import org.checkerframework.javacutil.TreeUtils; @@ -75,7 +76,7 @@ protected List initializedFieldsAfterCall(MethodInvocationNode List result = new ArrayList<>(); MethodInvocationTree tree = node.getTree(); ExecutableElement method = TreeUtils.elementFromUse(tree); - boolean isConstructor = method.getSimpleName().contentEquals(""); + boolean isConstructor = InternalUtils.isInitName(method.getSimpleName()); Node receiver = node.getTarget().getReceiver(); if (isConstructor && receiver instanceof ThisNode) { @@ -84,14 +85,14 @@ protected List initializedFieldsAfterCall(MethodInvocationNode (methodSelect instanceof IdentifierTree) ? ((IdentifierTree) methodSelect).getName() : ((MemberSelectTree) methodSelect).getIdentifier(); - if (methodName.contentEquals("this")) { + if (InternalUtils.isThisName(methodName)) { // Case 1: After a call to the constructor of the same class, all // fields are guaranteed to be initialized. ClassTree clazz = TreePathUtil.enclosingClass(analysis.getTypeFactory().getPath(tree)); TypeElement clazzElem = TreeUtils.elementFromDeclaration(clazz); markFieldsAsInitialized(result, clazzElem); - } else if (methodName.contentEquals("super")) { + } else if (InternalUtils.isSuperName(methodName)) { // Case 4: After a call to the constructor of the super class, all // fields of any super class are guaranteed to be initialized. ClassTree clazz = diff --git a/checker/src/main/java/org/checkerframework/checker/interning/InterningVisitor.java b/checker/src/main/java/org/checkerframework/checker/interning/InterningVisitor.java index a76f00083cf4..48a43a0cb41a 100644 --- a/checker/src/main/java/org/checkerframework/checker/interning/InterningVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/interning/InterningVisitor.java @@ -37,6 +37,7 @@ import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationMirrorSet; import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.InternalUtils; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; @@ -335,8 +336,8 @@ private boolean isReferenceEqualityImplementation(MethodTree equalsMethod) { Name leftName = ((IdentifierTree) lhsTree).getName(); Name rightName = ((IdentifierTree) rhsTree).getName(); Name paramName = equalsMethod.getParameters().get(0).getName(); - if ((leftName.contentEquals("this") && rightName == paramName) - || (leftName == paramName && rightName.contentEquals("this"))) { + if ((InternalUtils.isThisName(leftName) && rightName == paramName) + || (leftName == paramName && InternalUtils.isThisName(rightName))) { return true; } } @@ -928,7 +929,7 @@ private boolean classIsAnnotated(AnnotatedTypeMirror type) { */ private @Nullable Element getThis(Scope scope) { for (Element e : scope.getLocalElements()) { - if (e.getSimpleName().contentEquals("this")) { + if (InternalUtils.isThisName(e.getSimpleName())) { return e; } } @@ -952,7 +953,8 @@ private boolean overrides(ExecutableElement e, Class clazz, String method) { // Check all of the methods in the class for name matches and overriding. for (ExecutableElement elt : ElementFilter.methodsIn(clazzElt.getEnclosedElements())) { - if (elt.getSimpleName().contentEquals(method) && elements.overrides(e, elt, clazzElt)) { + if (InternalUtils.sameName(elt.getSimpleName(), method) + && elements.overrides(e, elt, clazzElt)) { return true; } } diff --git a/checker/src/main/java/org/checkerframework/checker/lock/LockAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/lock/LockAnnotatedTypeFactory.java index 52510c335ac8..bb3e9852daed 100644 --- a/checker/src/main/java/org/checkerframework/checker/lock/LockAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/lock/LockAnnotatedTypeFactory.java @@ -45,6 +45,7 @@ import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.InternalUtils; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypeSystemError; import org.plumelib.util.CollectionsPlume; @@ -753,7 +754,7 @@ private void translateJcipAndJavaxAnnotations(Element element, AnnotatedTypeMirr Object value = null; for (Map.Entry entry : valmap.entrySet()) { - if (entry.getKey().getSimpleName().contentEquals("value")) { + if (InternalUtils.isValueName(entry.getKey().getSimpleName())) { value = entry.getValue().getValue(); break; } diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessNoInitVisitor.java b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessNoInitVisitor.java index 9f620de0ce42..8961e22e7581 100644 --- a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessNoInitVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessNoInitVisitor.java @@ -53,6 +53,7 @@ import org.checkerframework.javacutil.AnnotationMirrorSet; import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.InternalUtils; import org.checkerframework.javacutil.TreePathUtil; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TreeUtilsAfterJava11; @@ -238,7 +239,7 @@ protected boolean commonAssignmentCheck( // Note that this method should return non-null only for fields of this class, not // fields of any other class, including outer classes. if (!(receiver instanceof IdentifierTree) - || !((IdentifierTree) receiver).getName().contentEquals("this")) { + || !InternalUtils.isThisName(((IdentifierTree) receiver).getName())) { return null; } // fallthrough diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java index 0a02f52513ef..b7e249de3c6b 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -827,7 +827,9 @@ private boolean areSame(JavaExpression target, @Nullable JavaExpression enclosin return false; } if (enclosingTarget instanceof ThisReference && target instanceof ThisReference) { - return enclosingTarget.getType().toString().equals(target.getType().toString()); + return checker.getProcessingEnvironment() + .getTypeUtils() + .isSameType(enclosingTarget.getType(), target.getType()); } else { return enclosingTarget.equals(target); } diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/SetOfTypes.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/SetOfTypes.java index 185bbbf70e94..b9673d068306 100644 --- a/checker/src/main/java/org/checkerframework/checker/resourceleak/SetOfTypes.java +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/SetOfTypes.java @@ -5,6 +5,7 @@ import org.checkerframework.checker.signature.qual.CanonicalName; import org.checkerframework.dataflow.qual.Pure; +import org.checkerframework.javacutil.ElementUtils; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; @@ -69,7 +70,7 @@ static SetOfTypes allSubtypes(TypeMirror t) { @Pure static SetOfTypes anyOfTheseNames(ImmutableSet<@CanonicalName String> names) { return (typeUtils, u) -> - u instanceof Type && names.contains(((Type) u).tsym.getQualifiedName().toString()); + u instanceof Type && names.contains(ElementUtils.getQualifiedName(((Type) u).tsym)); } /** diff --git a/checker/src/main/java/org/checkerframework/checker/units/UnitsRelationsTools.java b/checker/src/main/java/org/checkerframework/checker/units/UnitsRelationsTools.java index 134af783d4ff..2fc73a56fa7d 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/UnitsRelationsTools.java +++ b/checker/src/main/java/org/checkerframework/checker/units/UnitsRelationsTools.java @@ -8,6 +8,7 @@ import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationMirrorSet; import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.InternalUtils; import java.lang.annotation.Annotation; import java.util.Map; @@ -155,7 +156,7 @@ private static boolean hasNoPrefix(AnnotationValue annotationValue) { for (Map.Entry entry : elementValues.entrySet()) { - if (entry.getKey().getSimpleName().contentEquals("value")) { + if (InternalUtils.isValueName(entry.getKey().getSimpleName())) { return entry.getValue(); } } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java index d6ed6c8f222e..a1be67c12675 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java @@ -144,6 +144,7 @@ import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.InternalUtils; import org.checkerframework.javacutil.SystemUtil; import org.checkerframework.javacutil.TreePathUtil; import org.checkerframework.javacutil.TreeUtils; @@ -3438,7 +3439,7 @@ public Node visitIdentifier(IdentifierTree tree, Void p) { switch (element.getKind()) { case FIELD: // Note that "this"/"super" is a field, but not a field access. - if (element.getSimpleName().contentEquals("this")) { + if (InternalUtils.isThisName(element.getSimpleName())) { node = new ExplicitThisNode(tree); } else { node = new SuperNode(tree); diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ExplicitThisNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ExplicitThisNode.java index 691c5c71fe84..8fffb0cb5a5e 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ExplicitThisNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ExplicitThisNode.java @@ -2,6 +2,7 @@ import com.sun.source.tree.IdentifierTree; +import org.checkerframework.javacutil.InternalUtils; import org.checkerframework.javacutil.TreeUtils; /** @@ -13,11 +14,17 @@ */ public class ExplicitThisNode extends ThisNode { + /** The identifier tree for "this". */ protected final IdentifierTree tree; + /** + * Creates a node for the given "this" identifier. + * + * @param t the identifier tree for "this" + */ public ExplicitThisNode(IdentifierTree t) { super(TreeUtils.typeOf(t)); - assert t.getName().contentEquals("this"); + assert InternalUtils.isThisName(t.getName()); tree = t; } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/SuperNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/SuperNode.java index 74cc10985bc7..438aa6b0790b 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/SuperNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/SuperNode.java @@ -4,6 +4,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.qual.SideEffectFree; +import org.checkerframework.javacutil.InternalUtils; import org.checkerframework.javacutil.TreeUtils; import java.util.Collection; @@ -21,11 +22,17 @@ */ public class SuperNode extends Node { + /** The identifier tree for "super". */ protected final IdentifierTree tree; + /** + * Creates a node for the given "super" identifier. + * + * @param t the identifier tree for "super" + */ public SuperNode(IdentifierTree t) { super(TreeUtils.typeOf(t)); - assert t.getName().contentEquals("super"); + assert InternalUtils.isSuperName(t.getName()); tree = t; } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpression.java b/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpression.java index 11f53bb9a6fc..5fa21a3c5371 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpression.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpression.java @@ -38,6 +38,7 @@ import org.checkerframework.javacutil.AnnotationProvider; import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.InternalUtils; import org.checkerframework.javacutil.TreePathUtil; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; @@ -550,10 +551,10 @@ public static JavaExpression fromTree(ExpressionTree tree) { IdentifierTree identifierTree = (IdentifierTree) tree; TypeMirror typeOfId = TreeUtils.typeOf(identifierTree); Name identifierName = identifierTree.getName(); - if (identifierName.contentEquals("this")) { + if (InternalUtils.isThisName(identifierName)) { result = new ThisReference(typeOfId); break; - } else if (identifierName.contentEquals("super")) { + } else if (InternalUtils.isSuperName(identifierName)) { result = new SuperReference(typeOfId); break; } diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index e5a9ac530ecb..cebe9441f372 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -81,6 +81,22 @@ list's iterator; the read-only iterator now walks the backing list by index. Thi single largest remaining source of `Iterator` allocation in type checking. No user-visible behavior change. +Performance: `InternalUtils` has new helpers (`isInitName`, `isThisName`, `isSuperName`, +`isValueName`, `isJavaLangObjectName`, `isJavaLangEnumName`) that compare a javac `Name` +against the table's pre-interned name by identity instead of `Name.contentEquals`, which +decodes the name's UTF-8 bytes into a fresh `String` on every call on byte-backed name +tables (javac's default before JDK 23, and what Gradle's `-XDuseUnsharedTable` forces on +all JDK versions). All call sites that compared a `Name` against these fixed literals +(`TreeUtils.isConstructor`, `TreeUtils.isEnumSuperCall`, identifier `this`/`super` checks +in dataflow and the checkers, annotation-element `value` checks) now use them. For +comparisons against dynamic-but-bounded strings (annotation element names, method names a +checker matches against), the new `InternalUtils.sameName(Name, CharSequence)` interns the +target into the name's own table through a table-validated cache and compares by identity +(~6x faster, allocation-free, on byte-backed tables); `sameName(Name, Name)` compares +same-table names by identity. `AnnotationUtils.getElementValue` and +`AnnotationBuilder.findElement` — the hottest `Name` comparisons in the Called Methods, +Must Call, and Resource Leak checkers — now use it. No user-visible behavior change. + Performance: the `AnnotatedTypeFactory` tree-type caches (`classAndMethodTreeCache`, `fromExpressionTreeCache`, `fromMemberTreeCache`, `fromTypeTreeCache`, and `elementToTreeCache`) are now unbounded `IdentityHashMap`s cleared per compilation unit, rather than LRU caches capped at 2048 diff --git a/docs/developer/performance-notes.md b/docs/developer/performance-notes.md index 3c75217a3b5c..06d32c23bc94 100644 --- a/docs/developer/performance-notes.md +++ b/docs/developer/performance-notes.md @@ -294,6 +294,63 @@ so small per-call wins paid back substantially. javac `Symbol.PackageSymbol`, reads the enclosing package directly from the `owner` field instead of calling `Elements#getPackageElement(String)`. Falls back to the original string-based lookup for non-javac implementations. +- **PR #1796** — *Interned-`Name` identity comparison for fixed name literals.* New + `InternalUtils` helpers (`isInitName`, `isThisName`, `isSuperName`, `isValueName`, + `isJavaLangObjectName`, `isJavaLangEnumName`) compare a javac `Name` against its own + table's pre-interned name (`n == n.table.names.init` etc. — uses the *name's own* table, so + no cross-context identity assumption) with a `contentEquals` fallback for non-javac + `Name`s. Converted the utf2chars-profiled sites — `TreeUtils.isConstructor`/ + `isEnumSuperCall` (the latter also reordered to check `` before the class name), the + `this`/`super` identifier checks in `TreeUtils`, + `TypeFromExpressionVisitor.visitIdentifier`/`visitMemberSelect`, `ParamApplier.isReceiver`, + `ElementUtils.isObject` — and then the remaining ~25 fixed-literal `contentEquals` sites + across dataflow (`CFGTranslationPhaseOne`, `JavaExpression`, `SuperNode`, + `ExplicitThisNode`), framework, and the checkers (initialization, interning, nullness, + lock, units). Full-build `checknullness` JFR: every converted site left the + `Convert.utf2chars`/`utf2string` attribution (was ~30 of 275 utf2* samples ≈ 0.23% of all + samples), so the end-to-end effect is real but sub-0.5% — not resolvable in wall clock. + Microbenchmark on a byte-backed name table: 12x faster and ~66 B/op allocation removed vs + `contentEquals`; neutral on `StringNameTable`. Only names with pre-interned `Names` fields + available on JDK 11+ are used (`init`, `_this`, `_super`, `value`, `java_lang_Object`, + `java_lang_Enum`); `names.yield` (JDK 13+) was deliberately not used. + + **General `sameName(Name, CharSequence)` with a table-validated static cache (also + PR #1796).** For arbitrary (non-pre-interned) target strings, the naive per-call form + `n == n.table.fromString(literal)` is a dud — measured (8.2M mixed hit/miss ops): ~12% + faster but 28% *more* allocation than `contentEquals` on a byte-backed table (it + re-encodes the literal per call), and 1.8x *slower* on `StringNameTable`. A naive static + `Map` is unsound in multi-compilation JVMs (the test suite, a language + server): a cached `Name` from a previous compilation's table compares `==`-false against + content-equal names from the new table. The applied design closes both holes: a single + `volatile` holder pinning `(Name.Table, ConcurrentHashMap)` that + `sameName` discards whenever it sees a name from a different table — stale answers are + impossible, the worst case is a cache rebuild on table switch. Measured: **5.8 ns/op, + zero allocation vs `contentEquals`'s 36.8 ns/op + ~66 B/op on byte-backed tables (6.4x)**; + on `StringNameTable` 5.7 vs 4.7 ns/op (~neutral). Converted the dynamic-but-bounded-target + sites: `AnnotationUtils.getElementValue`'s element-name loops (the **#1 utf2* consumer for + the Resource Leak Checker** — 66 of 201 utf2* samples on `checkResourceLeak`, invisible on + `checknullness`; hot-site profiles are checker-specific), `AnnotationBuilder.findElement`, + `ElementUtils`/`TreeUtils` method/field-name lookups, `JavaExpressionParseUtil` identifier + resolution, the stub parser's `findElement` family, and `SetOfTypes.anyOfTheseNames` (via + `ElementUtils.getQualifiedName`'s interned cache). Cardinality caveat: each distinct probe + string is interned into the compiler's name table and cached for the compilation, so + `sameName` is only for bounded target sets (annotation element names, configured method + names, source identifiers) — not arbitrary unbounded input. + + **Key environmental facts (verified June 2026):** (1) which `Name.Table` javac uses decides + whether `Name.toString()`/`contentEquals` decode UTF-8 per call: byte-backed + `SharedNameTable` is the default before JDK 23, `StringNameTable` (decode-free, cached + `toString`) since JDK 23. (2) **Gradle passes `-XDuseUnsharedTable` to every forked javac** + (verified in a `--debug` compile log), forcing the byte-backed table on *all* JDK versions — + so under Gradle (this project's own build, most users' builds) the decode cost is alive on + JDK 25/26 too, while plain-javac/Maven runs on JDK 23+ don't have it. Measure name-decode + changes with `-XDuseUnsharedTable`, or the A/B silently tests the wrong table. (3) Do NOT + compare a `Name` char-by-char (`charAt` loop): base `Name.length()`/`charAt()` call + `toString()` per invocation, so that is N+1 decodes instead of `contentEquals`'s one + (measured 2.8x slower interpreted, 545 MB extra allocation per 8M ops on JDK 21); the + raw-byte APIs (`getUtf8Length`/`getUtf8Bytes`/`map`) are version-specific and *re-encode* + on `StringNameTable` (measured 5x slower) — identity against an interned `Name` is the only + variant that wins on every table. ### Annotation-file (stub) parsing @@ -1368,8 +1425,12 @@ not single-leaf. Re-prioritized venues: - **Name decoding ~2.3%** (`Convert.utf2chars` 1.43% + `utf2string` 0.86%). The "annotation formatting in the hot path" question is *resolved, opposite to the prior guess*: 36% of `utf2chars` is under `DefaultAnnotationFormatter.isInvisibleQualified` (22%) + `AnnotationUtils.toStringSimple` (14%) — i.e. - `ATM.toString`/`CFAbstractValue.toString` invoked **during type-checking, not the stub parser**. Worth - a look for an unguarded `toString`, but small (~0.5%). + `ATM.toString`/`CFAbstractValue.toString` invoked **during type-checking, not the stub parser**. + *Update (PR #1796):* the name-*comparison* share of this slice is addressed (interned-`Name` + identity helpers + `sameName`); what remains is the *formatting/stringification* share — the + unguarded `toString` was found (see the eager-error-formatting bullet below), plus + `ProperType.computeHashCode` (hashes `toString()`) and `SourceChecker.shouldSkipUses` + (`Symbol.toString()` per call for a regex match). Stale pre-cache attribution kept below for history: @@ -1390,7 +1451,8 @@ not single-leaf. Re-prioritized venues: decoding; its nearest-CF callers split between legitimate stub work (`AnnotationFileParser.findElement`) and `TreeUtils.isConstructor`/`isEnumSuperCall`. Incremental, not architectural: audit each forcer for info it already has (e.g. a `TypeKind` it could read without completing the symbol, or an interned - `String` it could compare instead of decoding a `Name`). + `String` it could compare instead of decoding a `Name`). *Update: the name-comparison callers named + here (`isConstructor`, `isEnumSuperCall`, `findElement`) were addressed by PR #1796.* - **The defaulting walk is the largest *CF-controlled* leaf cluster — FEASIBILITY MEASURED (June 2026), verdict: highly memoizable, worth building.** `QualifierDefaults.DefaultApplierElementImpl.scan` @@ -1518,16 +1580,19 @@ not single-leaf. Re-prioritized venues: `getDeclAnnotations`/`isSupportedQualifier` calls per node), not the per-lookup cost. Low value as a direct target; better addressed indirectly if the defaulting/CF-into-javac venues reduce node visits. -- **Annotation *formatting* in the hot path — confirm before chasing (likely a non-issue).** - `AnnotationUtils.toStringSimple` and `DefaultAnnotationFormatter.isInvisibleQualified` appear under - `Convert.utf2chars`, which would be alarming (formatting should only run for diagnostics). But - `isInvisibleQualified` is called *only* from `DefaultAnnotationFormatter.formatAnnotationString` - (ATM `toString`), and `toStringSimple` from `CFAbstractValue.toString`/`toStringSimple` and the stub - parser — and the co-located nearest-CF frame is `AnnotationFileParser.findElement`, i.e. the **stub - parser** legitimately decoding/matching annotation names, not type-checking. Before optimizing, - confirm with a stack sample whether any `ATM.toString`/`CFAbstractValue.toString` is invoked from a - non-diagnostic path (an unguarded message build); if it is only the stub parser, there is nothing to - fix here. Sub-0.5% either way — low priority. +- **Annotation *formatting* in the hot path — CONFIRMED real (June 2026), mechanism found, fix + open: eager error formatting.** Stack samples on the full `checknullness` build settled the + "confirm before chasing" question: 148 samples (~1.1%) contain `AnnotatedTypeMirror.toString`, + and the callers are **not** diagnostics-only. The two paths: (1) + `BaseTypeVisitor.checkContainsSameToString` — a static `SimpleAnnotatedTypeScanner` whose lambda + calls `type.toString()` *and* `type.toString(true)` on **every component of every type** — invoked + via `containsSameToString` from `FoundRequired.of` and `shouldPrintVerbose`; (2) + `reportCommonAssignmentError`/`reportMethodInvocabilityError`, which build `FoundRequired` (i.e. + format both full types) **before** `checker.reportError`, so the formatting cost is paid even when + the warning is subsequently suppressed (`@SuppressWarnings`, `-AsuppressWarnings`) — and CF's own + sources suppress thousands. The fix shape is lazy formatting: defer the `FoundRequired` string + computation until the report is known to be emitted. This is the largest remaining CF-controlled + `utf2chars` consumer after PR #1796 (which removed the name-*comparison* share). - **CF driving javac internals — the biggest realistic CPU lever (~25% of total).** The wall-clock breakdown above attributes ~25% of all time to CF reaching into javac: @@ -1535,8 +1600,12 @@ not single-leaf. Re-prioritized venues: `CFAbstractValue.canBeMissingAnnotations`/`getErased`/`ElementUtils.isTypeElement`), `Name`/UTF-8 decoding (`Convert.utf2chars`/`utf2string`, `Utf8NameTable.equals` — every time CF compares or stringifies a `Name` that isn't yet decoded/interned), and repeated - `TreePath` construction/tree walks. PR #1763 (`getKind()` overrides) and PR #1673 - (interned-name caching) each chipped at one facet. This is bigger than dataflow + stubs + `TreePath` construction/tree walks. PR #1763 (`getKind()` overrides), PR #1673 + (interned-name caching), and PR #1796 (interned-`Name` identity comparison — removed the + name-*comparison* share of the decode facet; what is left of it is stringification: + the eager-error-formatting bullet above, `ProperType.computeHashCode`, + `SourceChecker.shouldSkipUses`, `LocalVariableNode.getName`, stub-parser + `annosInPackage`) each chipped at one facet. This is bigger than dataflow + stubs + visitor combined and is the highest-leverage remaining CPU target for realistic compiles; it is incremental, not architectural — audit the remaining forcers/decoders that already have (or could cache) the needed info. Confirmed real, not the diff --git a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java index ca901751370e..61d666ce1a26 100644 --- a/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java +++ b/framework/src/main/java/org/checkerframework/common/basetype/BaseTypeVisitor.java @@ -109,6 +109,7 @@ import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.InternalUtils; import org.checkerframework.javacutil.SwitchExpressionScanner; import org.checkerframework.javacutil.SwitchExpressionScanner.FunctionalSwitchExpressionScanner; import org.checkerframework.javacutil.SystemUtil; @@ -1862,7 +1863,7 @@ protected void validateVariablesTargetLocation(Tree tree, AnnotatedTypeMirror ty issueError = false; break; case PARAMETER: - if (((VariableTree) tree).getName().contentEquals("this")) { + if (InternalUtils.isThisName(((VariableTree) tree).getName())) { if (locations.contains(TypeUseLocation.RECEIVER)) { issueError = false; } @@ -2680,8 +2681,8 @@ public Void visitAnnotation(AnnotationTree tree, Void p) { TypeElement annoType = (TypeElement) TreeInfo.symbol((JCTree) tree.getAnnotationType()); Name annoName = annoType.getQualifiedName(); - if (annoName.contentEquals(DefaultQualifier.class.getName()) - || annoName.contentEquals(SuppressWarnings.class.getName())) { + if (InternalUtils.sameName(annoName, DefaultQualifier.class.getName()) + || InternalUtils.sameName(annoName, SuppressWarnings.class.getName())) { // Skip these two annotations, as we don't care about the arguments to them. return null; } diff --git a/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceImplementation.java b/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceImplementation.java index 8d7d10f1587d..ea485727837d 100644 --- a/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceImplementation.java +++ b/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceImplementation.java @@ -46,6 +46,7 @@ import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.InternalUtils; import org.checkerframework.javacutil.TreePathUtil; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypeSystemError; @@ -302,7 +303,7 @@ private void updateInferredExecutableParameterTypes( // have information inferred about their receivers. if (receiver != null && atypeFactory.wpiShouldInferTypesForReceivers() - && !methodElt.getSimpleName().contentEquals("")) { + && !InternalUtils.isInitName(methodElt.getSimpleName())) { AnnotatedTypeMirror receiverArgATM = atypeFactory.getReceiverType(invocationTree); AnnotatedExecutableType methodDeclType = atypeFactory.getAnnotatedType(methodElt); AnnotatedTypeMirror receiverParamATM = methodDeclType.getReceiverType(); diff --git a/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java b/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java index e99ce254c5e6..9f0337b563e6 100644 --- a/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java +++ b/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java @@ -62,6 +62,7 @@ import org.checkerframework.javacutil.AnnotationMirrorSet; import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.InternalUtils; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypeSystemError; import org.plumelib.util.ArraySet; @@ -784,7 +785,7 @@ private void addClass( throw new BugInCF( "Missing getFullyQualifiedName() for " + javaParserClass); } - if ("".contentEquals(tree.getSimpleName())) { + if (InternalUtils.sameName(tree.getSimpleName(), "")) { @SuppressWarnings( "signature:assignment" // computed from string concatenation ) diff --git a/framework/src/main/java/org/checkerframework/framework/ajava/ExpectedTreesVisitor.java b/framework/src/main/java/org/checkerframework/framework/ajava/ExpectedTreesVisitor.java index 8cd0e1a66c94..64d6a1aaf265 100644 --- a/framework/src/main/java/org/checkerframework/framework/ajava/ExpectedTreesVisitor.java +++ b/framework/src/main/java/org/checkerframework/framework/ajava/ExpectedTreesVisitor.java @@ -26,6 +26,7 @@ import com.sun.source.tree.VariableTree; import com.sun.source.tree.WhileLoopTree; +import org.checkerframework.javacutil.InternalUtils; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TreeUtilsAfterJava11.BindingPatternUtils; import org.checkerframework.javacutil.TreeUtilsAfterJava11.SwitchExpressionUtils; @@ -183,8 +184,8 @@ public Void visitExpressionStatement(ExpressionStatementTree tree, Void p) { MethodInvocationTree invocation = (MethodInvocationTree) tree.getExpression(); if (invocation.getMethodSelect() instanceof IdentifierTree) { IdentifierTree identifier = (IdentifierTree) invocation.getMethodSelect(); - if (identifier.getName().contentEquals("this") - || identifier.getName().contentEquals("super")) { + if (InternalUtils.isThisName(identifier.getName()) + || InternalUtils.isSuperName(identifier.getName())) { trees.remove(tree); trees.remove(identifier); } @@ -344,7 +345,7 @@ public Void visitNewClass(NewClassTree tree, Void p) { // Constructors cannot be declared in an anonymous class, so don't add them. if (member instanceof MethodTree) { MethodTree methodTree = (MethodTree) member; - if (methodTree.getName().contentEquals("")) { + if (InternalUtils.isInitName(methodTree.getName())) { continue; } } diff --git a/framework/src/main/java/org/checkerframework/framework/ajava/JointJavacJavaParserVisitor.java b/framework/src/main/java/org/checkerframework/framework/ajava/JointJavacJavaParserVisitor.java index 96ef451d27b7..64e96786ba6a 100644 --- a/framework/src/main/java/org/checkerframework/framework/ajava/JointJavacJavaParserVisitor.java +++ b/framework/src/main/java/org/checkerframework/framework/ajava/JointJavacJavaParserVisitor.java @@ -160,6 +160,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.javacutil.BugInCF; +import org.checkerframework.javacutil.InternalUtils; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TreeUtilsAfterJava11.BindingPatternUtils; import org.checkerframework.javacutil.TreeUtilsAfterJava11.CaseUtils; @@ -205,7 +206,7 @@ public Void visitAnnotation(AnnotationTree javacTree, Node javaParserNode) { assert value instanceof AssignmentTree; AssignmentTree assignment = (AssignmentTree) value; assert assignment.getVariable() instanceof IdentifierTree; - assert ((IdentifierTree) assignment.getVariable()).getName().contentEquals("value"); + assert InternalUtils.isValueName(((IdentifierTree) assignment.getVariable()).getName()); assignment.getExpression().accept(this, node.getMemberValue()); } else if (javaParserNode instanceof NormalAnnotationExpr) { NormalAnnotationExpr node = (NormalAnnotationExpr) javaParserNode; @@ -399,7 +400,7 @@ public static boolean isDefaultSuperConstructorCall(StatementTree statement) { return false; } - if (!((IdentifierTree) invocation.getMethodSelect()).getName().contentEquals("super")) { + if (!InternalUtils.isSuperName(((IdentifierTree) invocation.getMethodSelect()).getName())) { return false; } @@ -658,7 +659,7 @@ public void visitAnonymousClassBody( Tree member = javacMembers.get(0); if (member instanceof MethodTree) { MethodTree methodTree = (MethodTree) member; - if (methodTree.getName().contentEquals("")) { + if (InternalUtils.isInitName(methodTree.getName())) { javacMembers.remove(0); } } @@ -680,7 +681,8 @@ public static boolean isNoArgumentConstructor(Tree member) { } MethodTree methodTree = (MethodTree) member; - return methodTree.getName().contentEquals("") && methodTree.getParameters().isEmpty(); + return InternalUtils.isInitName(methodTree.getName()) + && methodTree.getParameters().isEmpty(); } /** @@ -837,7 +839,7 @@ private boolean isYieldAndYield(ExpressionStatementTree javacTree, Node javaPars if ((javacInvokArgs.isEmpty() && javacInvokTypeArgs.isEmpty() && javacInvokMethod instanceof IdentifierTree) - && ((IdentifierTree) javacInvokMethod).getName().toString().equals("yield")) { + && ((IdentifierTree) javacInvokMethod).getName().contentEquals("yield")) { YieldStmt javaParserYieldStmt = (YieldStmt) javaParserNode; Expression javaParserYieldExpression = javaParserYieldStmt.getExpression(); diff --git a/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileParser.java b/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileParser.java index fd5a2a6273f2..0b4e19e85577 100644 --- a/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileParser.java +++ b/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileParser.java @@ -81,6 +81,7 @@ import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.InternalUtils; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; import org.checkerframework.javacutil.UserError; @@ -643,7 +644,7 @@ private Map getImportedAnnotations() { for (VariableElement field : ElementUtils.getAllFieldsIn(enclType, elements)) { // field.getSimpleName() is a CharSequence, not a String - if (fieldName.equals(field.getSimpleName().toString())) { + if (InternalUtils.sameName(field.getSimpleName(), fieldName)) { importedConstants.add(imported); } } @@ -1785,7 +1786,7 @@ private void processField(FieldDeclaration decl, VariableElement elt) { VariableDeclarator fieldVarDecl = null; String eltName = elt.getSimpleName().toString(); for (VariableDeclarator var : decl.getVariables()) { - if (var.getName().toString().equals(eltName)) { + if (var.getName().getIdentifier().equals(eltName)) { fieldVarDecl = var; break; } @@ -2193,7 +2194,8 @@ private void putNewElement( continue; } ExecutableElement candidate = (ExecutableElement) elt; - if (!candidate.getSimpleName().contentEquals(methodDecl.getName().getIdentifier())) { + if (!InternalUtils.sameName( + candidate.getSimpleName(), methodDecl.getName().getIdentifier())) { continue; } List candidateParams = candidate.getParameters(); @@ -2290,7 +2292,7 @@ private boolean sameType(TypeMirror javacType, Type javaParserType) { Element javacElement = javacTypeInternal.asElement(); // Check both fully-qualified name and simple name. return javacElement.toString().equals(javaParserString) - || javacElement.getSimpleName().contentEquals(javaParserString); + || InternalUtils.sameName(javacElement.getSimpleName(), javaParserString); case ARRAY: return javaParserType.isArrayType() @@ -2351,11 +2353,8 @@ private void processFakeOverride( NodeWithRange astNode) { String typeString = type.getNameAsString(); for (AnnotatedDeclaredType supertype : types) { - if (supertype - .getUnderlyingType() - .asElement() - .getSimpleName() - .contentEquals(typeString)) { + if (InternalUtils.sameName( + supertype.getUnderlyingType().asElement().getSimpleName(), typeString)) { return supertype; } } @@ -2383,7 +2382,7 @@ private void processFakeOverride( private @Nullable Element findElement(TypeElement typeElt, ClassOrInterfaceDeclaration ciDecl) { String wantedClassOrInterfaceName = ciDecl.getNameAsString(); for (TypeElement typeElement : ElementUtils.getAllTypeElementsIn(typeElt)) { - if (wantedClassOrInterfaceName.equals(typeElement.getSimpleName().toString())) { + if (InternalUtils.sameName(typeElement.getSimpleName(), wantedClassOrInterfaceName)) { return typeElement; } } @@ -2413,7 +2412,7 @@ private void processFakeOverride( private @Nullable Element findElement(TypeElement typeElt, EnumDeclaration enumDecl) { String wantedEnumName = enumDecl.getNameAsString(); for (TypeElement typeElement : ElementUtils.getAllTypeElementsIn(typeElt)) { - if (wantedEnumName.equals(typeElement.getSimpleName().toString())) { + if (InternalUtils.sameName(typeElement.getSimpleName(), wantedEnumName)) { return typeElement; } } @@ -2486,7 +2485,7 @@ private List methodsInTypeElement(TypeElement typeElt) { String wantedMethodString = AnnotationFileUtil.toString(methodDecl); for (ExecutableElement method : methodsInTypeElement(typeElt)) { if (wantedMethodParams == method.getParameters().size() - && wantedMethodName.contentEquals(method.getSimpleName().toString()) + && InternalUtils.sameName(method.getSimpleName(), wantedMethodName) && ElementUtils.getSimpleSignature(method).equals(wantedMethodString)) { return method; } @@ -2586,7 +2585,7 @@ private VariableElement findElement(TypeElement typeElt, VariableDeclarator vari TypeElement typeElt, String fieldName, NodeWithRange astNode) { for (VariableElement field : ElementUtils.getAllFieldsIn(typeElt, elements)) { // field.getSimpleName() is a CharSequence, not a String - if (fieldName.equals(field.getSimpleName().toString())) { + if (InternalUtils.sameName(field.getSimpleName(), fieldName)) { return field; } } diff --git a/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileUtil.java b/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileUtil.java index 699d37dce9fc..b078bb013a0c 100644 --- a/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileUtil.java +++ b/framework/src/main/java/org/checkerframework/framework/stub/AnnotationFileUtil.java @@ -173,7 +173,7 @@ public boolean isCommandLine() { } FieldDeclaration decl = (FieldDeclaration) member; for (VariableDeclarator var : decl.getVariables()) { - if (toString(var).equals(field.getSimpleName().toString())) { + if (field.getSimpleName().contentEquals(toString(var))) { return decl; } } diff --git a/framework/src/main/java/org/checkerframework/framework/stub/StubGenerator.java b/framework/src/main/java/org/checkerframework/framework/stub/StubGenerator.java index 9000fb0029a5..abad16a360ea 100644 --- a/framework/src/main/java/org/checkerframework/framework/stub/StubGenerator.java +++ b/framework/src/main/java/org/checkerframework/framework/stub/StubGenerator.java @@ -286,9 +286,11 @@ private void printMember(Element member, List innerClass) { * Helper method that outputs the field declaration for the given field. * *

It indicates whether the field is {@code protected}. + * + * @param field the field to print */ private void printFieldDecl(VariableElement field) { - if ("class".equals(field.getSimpleName().toString())) { + if (field.getSimpleName().contentEquals("class")) { error("Cannot write class literals in stub files."); return; } diff --git a/framework/src/main/java/org/checkerframework/framework/type/TypeFromExpressionVisitor.java b/framework/src/main/java/org/checkerframework/framework/type/TypeFromExpressionVisitor.java index 016b4e7d2efd..47fc00e781e5 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/TypeFromExpressionVisitor.java +++ b/framework/src/main/java/org/checkerframework/framework/type/TypeFromExpressionVisitor.java @@ -36,6 +36,7 @@ import org.checkerframework.javacutil.AnnotationMirrorSet; import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.InternalUtils; import org.checkerframework.javacutil.SwitchExpressionScanner; import org.checkerframework.javacutil.SwitchExpressionScanner.FunctionalSwitchExpressionScanner; import org.checkerframework.javacutil.SystemUtil; @@ -226,7 +227,7 @@ public AnnotatedTypeMirror visitSwitchExpressionTree17( @Override public AnnotatedTypeMirror visitIdentifier(IdentifierTree tree, AnnotatedTypeFactory f) { - if (tree.getName().contentEquals("this") || tree.getName().contentEquals("super")) { + if (InternalUtils.isThisName(tree.getName()) || InternalUtils.isSuperName(tree.getName())) { AnnotatedDeclaredType res = f.getSelfType(tree); return res; } @@ -264,11 +265,11 @@ public AnnotatedTypeMirror visitMemberSelect(MemberSelectTree tree, AnnotatedTyp // Fall-through. } - if (tree.getIdentifier().contentEquals("this")) { + if (InternalUtils.isThisName(tree.getIdentifier())) { // Tree is "MyClass.this", where "MyClass" may be the innermost enclosing type or any // outer type. return f.getEnclosingType(TypesUtils.getTypeElement(TreeUtils.typeOf(tree)), tree); - } else if (tree.getIdentifier().contentEquals("super")) { + } else if (InternalUtils.isSuperName(tree.getIdentifier())) { // Tree is "MyClass.super", where "MyClass" may be the innermost enclosing type or any // outer type. TypeMirror superTypeMirror = TreeUtils.typeOf(tree); diff --git a/framework/src/main/java/org/checkerframework/framework/util/AnnotatedTypes.java b/framework/src/main/java/org/checkerframework/framework/util/AnnotatedTypes.java index 1eabaa084894..32a723625604 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/AnnotatedTypes.java +++ b/framework/src/main/java/org/checkerframework/framework/util/AnnotatedTypes.java @@ -28,6 +28,7 @@ import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.InternalUtils; import org.checkerframework.javacutil.SystemUtil; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; @@ -474,7 +475,7 @@ private static boolean isRawCall(AnnotatedDeclaredType receiver, Element method, // The below is checking for a super() call where the super type is a raw type. // See framework/tests/all-systems/RawSuper.java for an example. - if (method.getSimpleName().contentEquals("")) { + if (InternalUtils.isInitName(method.getSimpleName())) { ExecutableElement constructor = (ExecutableElement) method; TypeMirror constructorClass = types.erasure(constructor.getEnclosingElement().asType()); TypeMirror directSuper = types.directSupertypes(receiver.getUnderlyingType()).get(0); diff --git a/framework/src/main/java/org/checkerframework/framework/util/JavaExpressionParseUtil.java b/framework/src/main/java/org/checkerframework/framework/util/JavaExpressionParseUtil.java index 69639bdbf3d1..1561ea44a2c9 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/JavaExpressionParseUtil.java +++ b/framework/src/main/java/org/checkerframework/framework/util/JavaExpressionParseUtil.java @@ -56,6 +56,7 @@ import org.checkerframework.framework.util.dependenttypes.DependentTypesError; import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.InternalUtils; import org.checkerframework.javacutil.Resolver; import org.checkerframework.javacutil.TypesUtils; import org.checkerframework.javacutil.trees.TreeBuilder; @@ -490,7 +491,7 @@ public JavaExpression visit(NameExpr expr, Void aVoid) { if (parameters != null) { for (int i = 0; i < parameters.size(); i++) { Element varElt = parameters.get(i).getElement(); - if (varElt.getSimpleName().contentEquals(s)) { + if (InternalUtils.sameName(varElt.getSimpleName(), s)) { throw new ParseRuntimeException( constructJavaExpressionParseError( s, @@ -559,7 +560,7 @@ public JavaExpression visit(NameExpr expr, Void aVoid) { if (!(memberElement.getKind().isClass() || memberElement.getKind().isInterface())) { continue; } - if (memberElement.getSimpleName().contentEquals(identifier)) { + if (InternalUtils.sameName(memberElement.getSimpleName(), identifier)) { return new ClassName(ElementUtils.getType(memberElement)); } } @@ -590,7 +591,8 @@ public JavaExpression visit(NameExpr expr, Void aVoid) { TypeMirror searchType = enclosingType; while (searchType.getKind() == TypeKind.DECLARED) { DeclaredType searchDeclaredType = (DeclaredType) searchType; - if (searchDeclaredType.asElement().getSimpleName().contentEquals(identifier)) { + if (InternalUtils.sameName( + searchDeclaredType.asElement().getSimpleName(), identifier)) { return new ClassName(searchType); } ClassName className = getIdentifierAsInnerClassName(searchType, identifier); diff --git a/framework/src/main/java/org/checkerframework/framework/util/defaults/QualifierDefaults.java b/framework/src/main/java/org/checkerframework/framework/util/defaults/QualifierDefaults.java index c02629f23453..24fcc50e90d7 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/defaults/QualifierDefaults.java +++ b/framework/src/main/java/org/checkerframework/framework/util/defaults/QualifierDefaults.java @@ -33,6 +33,7 @@ import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.InternalUtils; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; import org.plumelib.util.StringsPlume; @@ -1137,7 +1138,7 @@ public Void scan(@FindDistinct AnnotatedTypeMirror t, AnnotationMirror qual) { if (outer.scope != null && outer.scope.getKind() == ElementKind.PARAMETER && isTopLevelType - && outer.scope.getSimpleName().contentEquals("this")) { + && InternalUtils.isThisName(outer.scope.getSimpleName())) { // TODO: comparison against "this" is ugly, won't work // for all possible names for receiver parameter. // Comparison to Names._this might be a bit faster. diff --git a/framework/src/main/java/org/checkerframework/framework/util/element/ParamApplier.java b/framework/src/main/java/org/checkerframework/framework/util/element/ParamApplier.java index 1d3ba8965de2..2479151a5049 100644 --- a/framework/src/main/java/org/checkerframework/framework/util/element/ParamApplier.java +++ b/framework/src/main/java/org/checkerframework/framework/util/element/ParamApplier.java @@ -14,6 +14,7 @@ import org.checkerframework.framework.type.ElementAnnotationApplier; import org.checkerframework.framework.util.element.ElementAnnotationUtil.UnexpectedAnnotationLocationException; import org.checkerframework.javacutil.BugInCF; +import org.checkerframework.javacutil.InternalUtils; import org.plumelib.util.IPair; import java.util.ArrayList; @@ -270,7 +271,7 @@ protected void handleTargeted(List targeted) */ private boolean isReceiver(Element element) { return element.getKind() == ElementKind.PARAMETER - && element.getSimpleName().contentEquals("this"); + && InternalUtils.isThisName(element.getSimpleName()); } @Override diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationBuilder.java b/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationBuilder.java index 9a99efe71fc4..0de448272f41 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationBuilder.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationBuilder.java @@ -598,7 +598,7 @@ private VariableElement findEnumElement(Enum value) { TypeElement enumClassElt = elements.getTypeElement(enumClass); assert enumClassElt != null; for (Element enumElt : enumClassElt.getEnclosedElements()) { - if (enumElt.getSimpleName().contentEquals(value.name())) { + if (InternalUtils.sameName(enumElt.getSimpleName(), value.name())) { return (VariableElement) enumElt; } } @@ -628,7 +628,7 @@ public ExecutableElement findElement(CharSequence key) { annotationMethodsCache = methods; } for (ExecutableElement elt : methods) { - if (elt.getSimpleName().contentEquals(key)) { + if (InternalUtils.sameName(elt.getSimpleName(), key)) { return elt; } } diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationUtils.java b/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationUtils.java index 88e381bb684c..68c873ff3b6c 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationUtils.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/AnnotationUtils.java @@ -639,7 +639,7 @@ public static T getElementValueNotOnClasspath( for (Map.Entry entry : valmap.entrySet()) { ExecutableElement elem = entry.getKey(); - if (elem.getSimpleName().contentEquals(elementName)) { + if (InternalUtils.sameName(elem.getSimpleName(), elementName)) { AnnotationValue val = entry.getValue(); try { return expectedType.cast(val.getValue()); @@ -726,7 +726,7 @@ public static T getElementValue( for (Map.Entry entry : valmap.entrySet()) { ExecutableElement elem = entry.getKey(); - if (elem.getSimpleName().contentEquals(elementName)) { + if (InternalUtils.sameName(elem.getSimpleName(), elementName)) { AnnotationValue val = entry.getValue(); try { return expectedType.cast(val.getValue()); @@ -1621,7 +1621,7 @@ public static void toStringSimple(AnnotationMirror am, StringBuilder sb) { if (args.size() == 1) { Map.Entry first = args.entrySet().iterator().next(); - if (first.getKey().getSimpleName().contentEquals("value")) { + if (InternalUtils.isValueName(first.getKey().getSimpleName())) { formatAnnotationMirrorArg(first.getValue(), sb); oneValue = true; } diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/ElementUtils.java b/javacutil/src/main/java/org/checkerframework/javacutil/ElementUtils.java index 884777236fd5..760ebff3ee27 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/ElementUtils.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/ElementUtils.java @@ -413,7 +413,7 @@ public static CharSequence getSimpleDescription(ExecutableElement element) { * @return true iff the element is java.lang.Object element */ public static boolean isObject(TypeElement element) { - return element.getQualifiedName().contentEquals("java.lang.Object"); + return InternalUtils.isJavaLangObjectName(element.getQualifiedName()); } /** @@ -423,7 +423,7 @@ public static boolean isObject(TypeElement element) { * @return true iff the element is java.lang.String element */ public static boolean isString(TypeElement element) { - return element.getQualifiedName().contentEquals("java.lang.String"); + return InternalUtils.sameName(element.getQualifiedName(), "java.lang.String"); } /** @@ -524,7 +524,7 @@ public static String getSourceFilePath(TypeElement element) { */ public static @Nullable VariableElement findFieldInType(TypeElement type, String name) { for (VariableElement field : ElementFilter.fieldsIn(type.getEnclosedElements())) { - if (field.getSimpleName().contentEquals(name)) { + if (InternalUtils.sameName(field.getSimpleName(), name)) { return field; } } @@ -646,7 +646,7 @@ public static boolean hasReceiver(Element element) { } else { // In constructors, the element for "this" is a non-static field, but that field // does not have a receiver. - return !element.getSimpleName().contentEquals("this"); + return !InternalUtils.isThisName(element.getSimpleName()); } } return element.getKind() == ElementKind.METHOD && !ElementUtils.isStatic(element); @@ -914,7 +914,7 @@ public static boolean isRecordAccessor(ExecutableElement methodElement) { List encloseds = enclosing.getEnclosedElements(); for (Element enclosed : encloseds) { if (isRecordComponentElement(enclosed) - && enclosed.getSimpleName().toString().equals(methodName)) { + && InternalUtils.sameName(enclosed.getSimpleName(), methodName)) { return true; } } @@ -952,7 +952,7 @@ public static boolean isAutoGeneratedRecordMember(Element e) { */ public static boolean matchesElement( ExecutableElement method, String methodName, Class... parameters) { - if (!method.getSimpleName().contentEquals(methodName)) { + if (!InternalUtils.sameName(method.getSimpleName(), methodName)) { return false; } diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/InternalUtils.java b/javacutil/src/main/java/org/checkerframework/javacutil/InternalUtils.java index 4ec70420485f..cb6ee8ed31cf 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/InternalUtils.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/InternalUtils.java @@ -4,10 +4,14 @@ import com.sun.tools.javac.processing.JavacProcessingEnvironment; import com.sun.tools.javac.util.Context; import com.sun.tools.javac.util.JCDiagnostic.DiagnosticPosition; +import com.sun.tools.javac.util.Names; import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.concurrent.ConcurrentHashMap; + import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.Name; /** Miscellaneous static utility methods. */ public class InternalUtils { @@ -55,4 +59,212 @@ public static int compareDiagnosticPosition(Tree tree1, Tree tree2) { return Integer.compare(pos1.getStartPosition(), pos2.getStartPosition()); } + + /** + * Returns true iff {@code name} is the constructor name {@code ""}. + * + *

javac interns all {@code Name}s in a per-compilation table, so two names from the same + * compilation are equal iff they are identical. Comparing against the table's pre-interned + * {@code init} name is a pointer comparison, whereas {@code Name.contentEquals} decodes the + * name's UTF-8 bytes into a fresh {@code String} on every call on byte-backed name tables + * (javac's default before JDK 23, and what Gradle's {@code -XDuseUnsharedTable} forces on all + * JDK versions). + * + * @param name a name + * @return true iff name is "<init>" + */ + public static boolean isInitName(Name name) { + if (name instanceof com.sun.tools.javac.util.Name) { + com.sun.tools.javac.util.Name n = (com.sun.tools.javac.util.Name) name; + return n == names(n).init; + } + return name.contentEquals(""); + } + + /** + * Returns true iff {@code name} is {@code "this"}. See {@link #isInitName} for why this is + * faster than {@code Name.contentEquals}. + * + * @param name a name + * @return true iff name is "this" + */ + public static boolean isThisName(Name name) { + if (name instanceof com.sun.tools.javac.util.Name) { + com.sun.tools.javac.util.Name n = (com.sun.tools.javac.util.Name) name; + return n == names(n)._this; + } + return name.contentEquals("this"); + } + + /** + * Returns true iff {@code name} is {@code "super"}. See {@link #isInitName} for why this is + * faster than {@code Name.contentEquals}. + * + * @param name a name + * @return true iff name is "super" + */ + public static boolean isSuperName(Name name) { + if (name instanceof com.sun.tools.javac.util.Name) { + com.sun.tools.javac.util.Name n = (com.sun.tools.javac.util.Name) name; + return n == names(n)._super; + } + return name.contentEquals("super"); + } + + /** + * Returns true iff {@code name} is {@code "value"} (e.g., the name of an annotation element). + * See {@link #isInitName} for why this is faster than {@code Name.contentEquals}. + * + * @param name a name + * @return true iff name is "value" + */ + public static boolean isValueName(Name name) { + if (name instanceof com.sun.tools.javac.util.Name) { + com.sun.tools.javac.util.Name n = (com.sun.tools.javac.util.Name) name; + return n == names(n).value; + } + return name.contentEquals("value"); + } + + /** + * Returns true iff {@code name} is {@code "java.lang.Object"}. See {@link #isInitName} for why + * this is faster than {@code Name.contentEquals}. + * + * @param name a name, typically a {@code TypeElement}'s qualified name + * @return true iff name is "java.lang.Object" + */ + public static boolean isJavaLangObjectName(Name name) { + if (name instanceof com.sun.tools.javac.util.Name) { + com.sun.tools.javac.util.Name n = (com.sun.tools.javac.util.Name) name; + return n == names(n).java_lang_Object; + } + return name.contentEquals("java.lang.Object"); + } + + /** + * Returns true iff {@code name} is {@code "java.lang.Enum"}. See {@link #isInitName} for why + * this is faster than {@code Name.contentEquals}. + * + * @param name a name, typically a {@code TypeElement}'s qualified name + * @return true iff name is "java.lang.Enum" + */ + public static boolean isJavaLangEnumName(Name name) { + if (name instanceof com.sun.tools.javac.util.Name) { + com.sun.tools.javac.util.Name n = (com.sun.tools.javac.util.Name) name; + return n == names(n).java_lang_Enum; + } + return name.contentEquals("java.lang.Enum"); + } + + /** + * Returns the per-compilation interned-names table that {@code n} belongs to. + * + * @param n a javac name + * @return the {@code Names} table that interned {@code n} + */ + private static Names names(com.sun.tools.javac.util.Name n) { + return n.table.names; + } + + /** + * A cache mapping strings to the {@code Name} that one particular {@link + * com.sun.tools.javac.util.Name.Table} interns them as, so that {@link #sameName} can compare + * by identity instead of decoding the name's UTF-8 bytes on every call. + * + *

The cache pins the table it was built for; {@link #sameName} discards it whenever it sees + * a name from a different table (i.e., a new compilation in the same JVM, as in the test suite + * or a language server). A stale cache is therefore impossible; the worst case is a rebuild. + */ + private static final class NameCache { + /** The name table this cache is valid for. */ + final com.sun.tools.javac.util.Name.Table table; + + /** Maps a string to the {@code Name} that {@link #table} interns it as. */ + final ConcurrentHashMap map = + new ConcurrentHashMap<>(16); + + /** + * Creates a cache for the given table. + * + * @param table the name table this cache is valid for + */ + NameCache(com.sun.tools.javac.util.Name.Table table) { + this.table = table; + } + } + + /** The current {@link NameCache}, or null if no javac name has been compared yet. */ + private static volatile @Nullable NameCache nameCache; + + /** + * Returns true iff {@code name} has the same characters as {@code expected}. + * + *

For a javac {@code Name}, this interns {@code expected} into the name's own table (cached + * across calls) and compares by identity, instead of {@code Name.contentEquals}, which decodes + * the name's UTF-8 bytes into a fresh {@code String} on every call on byte-backed name tables + * (javac's default before JDK 23, and what Gradle's {@code -XDuseUnsharedTable} forces on all + * JDK versions). Measured ~6x faster with zero allocation there, and neutral on string-backed + * tables. + * + *

Only use this for a bounded set of {@code expected} strings (annotation element names, + * method names a checker matches against, identifiers that occur in the source). Each distinct + * string is interned into the compiler's name table and cached for the duration of the + * compilation, so unbounded or one-off probe strings would grow both. For a fixed literal, + * prefer the dedicated helpers ({@link #isInitName}, {@link #isThisName}, ...) when one exists. + * + * @param name a name + * @param expected the expected string + * @return true iff name and expected represent the same characters + */ + public static boolean sameName(Name name, CharSequence expected) { + if (expected instanceof Name) { + // Avoid expected.toString(), which decodes a javac Name. + return sameName(name, (Name) expected); + } + if (name instanceof com.sun.tools.javac.util.Name) { + com.sun.tools.javac.util.Name n = (com.sun.tools.javac.util.Name) name; + String expectedString = expected.toString(); + NameCache c = nameCache; + @SuppressWarnings("interning:not.interned") + boolean needInit = (c == null || c.table != n.table); + if (needInit) { + c = new NameCache(n.table); + nameCache = c; + } + @SuppressWarnings("nullness:dereference.of.nullable") // from needsInit + com.sun.tools.javac.util.Name target = c.map.get(expectedString); + if (target == null) { + target = n.table.fromString(expectedString); + c.map.put(expectedString, target); + } + return n == target; + } + return name.contentEquals(expected); + } + + /** + * Returns true iff {@code name1} and {@code name2} have the same characters. + * + *

javac interns all {@code Name}s in a per-compilation table, so two javac names from the + * same table are equal iff they are identical; {@code Name.contentEquals(Name)} would instead + * decode both names' UTF-8 bytes on byte-backed name tables. Falls back to {@code + * contentEquals} if either name is not a javac name or the tables differ. + * + * @param name1 a name + * @param name2 a name + * @return true iff name1 and name2 represent the same characters + */ + public static boolean sameName(Name name1, Name name2) { + if (name1 instanceof com.sun.tools.javac.util.Name + && name2 instanceof com.sun.tools.javac.util.Name) { + com.sun.tools.javac.util.Name n1 = (com.sun.tools.javac.util.Name) name1; + com.sun.tools.javac.util.Name n2 = (com.sun.tools.javac.util.Name) name2; + @SuppressWarnings("interning:not.interned") + boolean sameTable = (n1.table == n2.table); + if (sameTable) { + return n1 == n2; + } + } + return name1.contentEquals(name2); + } } diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java b/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java index 130a024a79eb..5412c45ccb86 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/TreeUtils.java @@ -176,7 +176,7 @@ private TreeUtils() { * @return true iff tree describes a constructor */ public static boolean isConstructor(MethodTree tree) { - return tree.getName().contentEquals(""); + return InternalUtils.isInitName(tree.getName()); } /** @@ -207,7 +207,7 @@ public static boolean isThisConstructorCall(MethodInvocationTree tree) { * @return true iff tree is a call to the given method */ private static boolean isNamedMethodCall(String name, MethodInvocationTree tree) { - return getMethodName(tree.getMethodSelect()).contentEquals(name); + return InternalUtils.sameName(methodName(tree), name); } /** @@ -254,7 +254,7 @@ public static boolean isSelfAccess(ExpressionTree tree) { tr = ((MemberSelectTree) tr).getExpression(); if (tr instanceof IdentifierTree) { Name ident = ((IdentifierTree) tr).getName(); - return ident.contentEquals("this") || ident.contentEquals("super"); + return InternalUtils.isThisName(ident) || InternalUtils.isSuperName(ident); } } @@ -905,7 +905,7 @@ public static boolean containsThisConstructorInvocation(MethodTree tree) { MethodInvocationTree invocation = (MethodInvocationTree) ((ExpressionStatementTree) st).getExpression(); - return "this".contentEquals(TreeUtils.methodName(invocation)); + return InternalUtils.isThisName(TreeUtils.methodName(invocation)); } /** @@ -1383,7 +1383,7 @@ public static List getMethods( throw new UserError("Configuration problem! Could not load type: " + typeName); } for (ExecutableElement exec : ElementFilter.methodsIn(typeElt.getEnclosedElements())) { - if (exec.getSimpleName().contentEquals(methodName) + if (InternalUtils.sameName(exec.getSimpleName(), methodName) && exec.getParameters().size() == params) { methods.add(exec); } @@ -1428,7 +1428,7 @@ public static ExecutableElement getMethod( throw new UserError("Configuration problem! Could not load type: " + typeName); } for (ExecutableElement exec : ElementFilter.methodsIn(typeElt.getEnclosedElements())) { - if (exec.getSimpleName().contentEquals(methodName) + if (InternalUtils.sameName(exec.getSimpleName(), methodName) && exec.getParameters().size() == paramTypes.length) { boolean typesMatch = true; List params = exec.getParameters(); @@ -1449,7 +1449,7 @@ public static ExecutableElement getMethod( // Didn't find an answer. Compose an error message. List candidates = new ArrayList<>(); for (ExecutableElement exec : ElementFilter.methodsIn(typeElt.getEnclosedElements())) { - if (exec.getSimpleName().contentEquals(methodName)) { + if (InternalUtils.sameName(exec.getSimpleName(), methodName)) { candidates.add(executableElementToString(exec)); } } @@ -1487,7 +1487,7 @@ private static String executableElementToString(ExecutableElement exec) { */ public static boolean isExplicitThisDereference(ExpressionTree expr) { if (expr instanceof IdentifierTree - && ((IdentifierTree) expr).getName().contentEquals("this")) { + && InternalUtils.isThisName(((IdentifierTree) expr).getName())) { // Explicit this reference "this" return true; } @@ -1497,7 +1497,7 @@ public static boolean isExplicitThisDereference(ExpressionTree expr) { } MemberSelectTree memSelTree = (MemberSelectTree) expr; - if (memSelTree.getIdentifier().contentEquals("this")) { + if (InternalUtils.isThisName(memSelTree.getIdentifier())) { // Outer this reference "C.this" return true; } @@ -1518,7 +1518,7 @@ public static boolean isClassLiteral(Tree tree) { if (!(tree instanceof MemberSelectTree)) { return false; } - return "class".equals(((MemberSelectTree) tree).getIdentifier().toString()); + return InternalUtils.sameName(((MemberSelectTree) tree).getIdentifier(), "class"); } /** @@ -1568,8 +1568,8 @@ public static boolean isFieldAccess(Tree tree) { assert isUseOfElement(ident) : "@AssumeAssertion(nullness): tree kind"; Element el = TreeUtils.elementFromUse(ident); if (el.getKind().isField() - && !ident.getName().contentEquals("this") - && !ident.getName().contentEquals("super")) { + && !InternalUtils.isThisName(ident.getName()) + && !InternalUtils.isSuperName(ident.getName())) { return (VariableElement) el; } } @@ -1633,8 +1633,9 @@ public static boolean isMethodAccess(Tree tree) { } else if (tree instanceof IdentifierTree) { // implicit method access IdentifierTree ident = (IdentifierTree) tree; - // The field "super" and "this" are also legal methods - if (ident.getName().contentEquals("super") || ident.getName().contentEquals("this")) { + // The names "this" and "super" are also legal methods. + if (InternalUtils.isThisName(ident.getName()) + || InternalUtils.isSuperName(ident.getName())) { return true; } assert isUseOfElement(ident) : "@AssumeAssertion(nullness): tree kind"; @@ -1704,7 +1705,7 @@ public static VariableElement getField( @FullyQualifiedName String typeName, String fieldName, ProcessingEnvironment env) { TypeElement mapElt = env.getElementUtils().getTypeElement(typeName); for (VariableElement var : ElementFilter.fieldsIn(mapElt.getEnclosedElements())) { - if (var.getSimpleName().contentEquals(fieldName)) { + if (InternalUtils.sameName(var.getSimpleName(), fieldName)) { return var; } } @@ -1730,11 +1731,14 @@ public static boolean isExpressionTree(Tree tree) { public static boolean isEnumSuperCall(MethodInvocationTree tree) { ExecutableElement ex = TreeUtils.elementFromUse(tree); assert ex != null : "@AssumeAssertion(nullness): tree kind"; + // Check the method name first: it is an interned-name comparison and false for + // most invocations, so the class-name comparison is usually skipped. + if (!InternalUtils.isInitName(ex.getSimpleName())) { + return false; + } Name name = ElementUtils.getQualifiedClassName(ex); assert name != null : "@AssumeAssertion(nullness): assumption"; - boolean correctClass = name.contentEquals("java.lang.Enum"); - boolean correctMethod = ex.getSimpleName().contentEquals(""); - return correctClass && correctMethod; + return InternalUtils.isJavaLangEnumName(name); } /** diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/TypesUtils.java b/javacutil/src/main/java/org/checkerframework/javacutil/TypesUtils.java index c40a85e4a4d8..bf9ad80f2d47 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/TypesUtils.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/TypesUtils.java @@ -417,7 +417,7 @@ public static boolean isThrowable(TypeMirror type) { DeclaredType dt = (DeclaredType) type; TypeElement elem = (TypeElement) dt.asElement(); Name name = elem.getQualifiedName(); - if ("java.lang.Throwable".contentEquals(name)) { + if (InternalUtils.sameName(name, "java.lang.Throwable")) { return true; } type = elem.getSuperclass(); @@ -1468,7 +1468,7 @@ public static boolean areSame(TypeVariable typeVariable1, TypeVariable typeVaria Name otherName = typeVariable2.asElement().getSimpleName(); Element otherEnclosingElement = typeVariable2.asElement().getEnclosingElement(); - return typeVariable1.asElement().getSimpleName().contentEquals(otherName) + return InternalUtils.sameName(typeVariable1.asElement().getSimpleName(), otherName) && otherEnclosingElement.equals(typeVariable1.asElement().getEnclosingElement()); } } diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/trees/TreeBuilder.java b/javacutil/src/main/java/org/checkerframework/javacutil/trees/TreeBuilder.java index e60e9fa5b756..2569bf048e87 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/trees/TreeBuilder.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/trees/TreeBuilder.java @@ -26,6 +26,7 @@ import com.sun.tools.javac.util.Names; import org.checkerframework.javacutil.BugInCF; +import org.checkerframework.javacutil.InternalUtils; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; import org.plumelib.util.CollectionsPlume; @@ -510,7 +511,7 @@ public MemberSelectTree buildPrimValueMethodAccess(Tree expr) { for (ExecutableElement method : ElementFilter.methodsIn(elements.getAllMembers(boxedElement))) { - if (method.getSimpleName().contentEquals(primValueName) + if (InternalUtils.sameName(method.getSimpleName(), primValueName) && method.getParameters().isEmpty()) { primValueMethod = (Symbol.MethodSymbol) method; }