{"record":{"id":"a52075703dd3cc11","repo":"elastic/elasticsearch","slug":"classes-from-a-previous-version-have-been-modified","errorCode":null,"errorMessage":"Classes from a previous version have been modified, violating backwards compatibility: ${deletedMembersMap}","messagePattern":"Classes from a previous version have been modified, violating backwards compatibility: (.+?)","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/JarApiComparisonTask.java","lineNumber":224,"sourceCode":"         * is backwards compatible.\n         */\n        public static void compareSignatures(Map<String, Set<String>> oldSignature, Map<String, Set<String>> newSignature) {\n            Set<String> deletedClasses = new HashSet<>(oldSignature.keySet());\n            deletedClasses.removeAll(newSignature.keySet());\n            if (deletedClasses.size() > 0) {\n                throw new IllegalStateException(\"Classes from a previous version not found: \" + deletedClasses);\n            }\n\n            Map<String, Set<String>> deletedMembersMap = new HashMap<>();\n            for (Map.Entry<String, Set<String>> entry : oldSignature.entrySet()) {\n                Set<String> deletedMembers = new HashSet<>(entry.getValue());\n                deletedMembers.removeAll(newSignature.get(entry.getKey()));\n                if (deletedMembers.size() > 0) {\n                    deletedMembersMap.put(entry.getKey(), Set.copyOf(deletedMembers));\n                }\n            }\n            if (deletedMembersMap.size() > 0) {\n                throw new IllegalStateException(\n                    \"Classes from a previous version have been modified, violating backwards compatibility: \" + deletedMembersMap\n                );\n            }\n        }\n    }\n}\n","sourceCodeStart":206,"sourceCodeEnd":231,"githubUrl":"https://github.com/elastic/elasticsearch/blob/db6a809a667c081ca1dc7500389d26975573215f/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/JarApiComparisonTask.java#L206-L231","documentation":"Thrown by JarApiComparisonTask when comparing the public API signatures (captured via javap) of a baseline 'old' jar against a newly built 'new' jar. For each class present in both jars, the set of public member signatures is diffed; if the old jar contains any public class/field/method declaration that is absent in the new jar, the change is treated as a source-incompatible removal and the build fails. This enforces Elasticsearch's stable-API guarantee: only additive changes are permitted on API-frozen artifacts.","triggerScenarios":"JarApiComparisonTask.compare() runs as a precommit task. It calls JarScanner.compareSignatures(oldJS.jarSignature(), newJS.jarSignature()). The loop computes deletedMembers = oldSet - newSet per class; any non-empty diff populates deletedMembersMap and throws. Signatures are filtered to lines matching '^\\s*public.*' in javap output, so narrowing visibility (public->package-private), renaming, or deleting a public member all manifest as a deletion.","commonSituations":"Refactoring that removes or renames a public method/field; changing a public return/parameter type (javap renders the new signature string, so the old line vanishes); tightening a public member to package-private; moving a method up the hierarchy (documented false positive in the class Javadoc); deleting a public class; upgrading the baseline jar without re-deriving signatures; differences in javap output between JDK versions used to record vs compare.","solutions":["Inspect deletedMembersMap in the message: it maps each affected class to the exact removed public signature lines — restore the missing public member (e.g. re-add the method/field, or keep a deprecated overload delegating to the new one).","If the removal is intentional and compatible (e.g. moving a method up the type hierarchy), regenerate and commit the updated baseline API jar used by getOldJar() so the comparison reflects the new contract.","If the signature 'changed' only because of a return-type/parameter-type swap to a supertype or implementation type (a known javap-based false positive), keep the old public method overload in place so the old signature line persists.","Verify the javap toolchain JDK matches the one used when the baseline was recorded — a different JDK can emit different javap formatting and create phantom diffs."],"exampleFix":"// before: removed a public method -> breaks API\n// public void oldPublicApi(Request req)\n//\n// after: keep the public signature, deprecate and delegate\n/** @deprecated use {@link #newApi(Context)} */\n@Deprecated\npublic void oldPublicApi(Request req) { newApi(new Context(req)); }","handlingStrategy":"validation","validationCode":"// Before changing public API, check the baseline signatures:\n// Run `./gradlew :<module>:jarApiCheck` (or the configured precommit task) locally.\n// To preview the diff, dump javap signatures of both jars:\n//   javap -classpath old.jar org.elasticsearch.SomeClass > old.sig\n//   javap -classpath new.jar org.elasticsearch.SomeClass > new.sig\n//   diff old.sig new.sig\n// Keep every line starting with 'public' that existed in old.sig.","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Treat public API additions as the only safe change on API-frozen modules; deprecate rather than delete.","Run the jar API comparison precommit task before pushing API-touching changes.","When refactoring, keep a deprecated bridge method with the old public signature so the javap line survives.","Regenerate and commit the baseline jar when a deliberate, reviewed API change is made."],"tags":["gradle","api-compatibility","precommit","bwc","javap"],"analyzedSha":"db6a809a667c081ca1dc7500389d26975573215f","analyzedAt":"2026-08-12T01:39:14.192Z","schemaVersion":2},"datasetVersion":"2026-08-12T08:17:17.861Z"}