{"record":{"id":"e477825adb790a5a","repo":"elastic/elasticsearch","slug":"classes-from-a-previous-version-not-found-delet","errorCode":null,"errorMessage":"Classes from a previous version not found: ${deletedClasses}","messagePattern":"Classes from a previous version not found: (.+?)","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/JarApiComparisonTask.java","lineNumber":212,"sourceCode":"\n        /**\n         * Comparison: The signatures are maps of class names to public class, field, or method\n         * declarations.\n         * </p>\n         * First, we check that the new jar signature contains all the same classes\n         * as the old jar signature. If not, we return an error.\n         * </p>\n         * Second, we iterate over the signature for each class. If a signature from the old\n         * jar is absent in the new jar, we add it to our list of errors.\n         * </p>\n         * Note that it is fine for the new jar to have additional elements, as this\n         * 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}","sourceCodeStart":194,"sourceCodeEnd":230,"githubUrl":"https://github.com/elastic/elasticsearch/blob/db6a809a667c081ca1dc7500389d26975573215f/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/JarApiComparisonTask.java#L194-L230","documentation":"Thrown as IllegalStateException by JarScanner.compareSignatures() when the set of class names in the old (reference) jar is not a subset of the class names in the new jar. Removing a public class from a stable-API jar is a backwards-incompatible change; the comparison treats any deleted class as a violation. The error lists the deleted class names so the developer knows exactly what was removed.","triggerScenarios":"compareSignatures (line 208) computes deletedClasses = oldSignature.keySet() - newSignature.keySet(). If non-empty (line 211), it throws with the set. Each entry is a class path (e.g., 'org/elasticsearch/X.class') present in the old jar but absent from the new. Triggered by deleting a public class, moving it to a different package, or renaming it.","commonSituations":"A public stable-API class was removed or renamed between versions; a class was moved to a different package (counts as delete + add); a class was made non-public or excluded from the jar; the new jar build accidentally excluded a package; refactoring merged a class into another without keeping the old name.","solutions":["For each deleted class in the error, decide: was the removal intentional? If not, restore the class or add a deprecated stub.","If a class was moved/renamed, keep a deprecated forwarding class at the old location to preserve binary compatibility.","If the removal is intentional and the API check should be updated, bump the stable API baseline reference jar to the current version (acknowledging the break).","Verify the new jar build includes all expected classes — check the jar task's includes/excludes and source set."],"exampleFix":"// before: public class Foo removed from stable API\n// old jar: org/elasticsearch/Foo.class\n// new jar: (missing) → throws\n\n// after: keep a deprecated stub for binary compatibility\npackage org.elasticsearch;\n@Deprecated(forRemoval = true)\npublic class Foo {\n    public Foo() { /* deprecated shim */ }\n}","handlingStrategy":"try-catch","validationCode":"Set<String> deleted = new HashSet<>(oldSignature.keySet());\ndeleted.removeAll(newSignature.keySet());\nif (deleted.isEmpty() == false) {\n    System.err.println(\"API break — deleted classes: \" + deleted);\n    // decide: restore classes, bump baseline, or document intentional removal\n}","typeGuard":null,"tryCatchPattern":"try {\n    JarScanner.compareSignatures(oldJS.jarSignature(), newJS.jarSignature());\n} catch (IllegalStateException e) {\n    // e.getMessage() lists deleted classes/members\n    throw new GradleException(\"Stable API backwards-compatibility violation: \" + e.getMessage(), e);\n}","preventionTips":["Never delete or rename public stable-API classes; keep deprecated stubs for binary compatibility.","Run JarApiComparisonTask in CI on every PR touching stable API modules.","When moving a class, leave a deprecated forwarding class at the old location.","Bump the API baseline jar deliberately and document the breaking change when intentional."],"tags":["api-comparison","backwards-compatibility","stable-api","bwc"],"analyzedSha":"db6a809a667c081ca1dc7500389d26975573215f","analyzedAt":"2026-08-12T01:39:14.192Z","schemaVersion":2},"datasetVersion":"2026-08-12T11:17:21.771Z"}