{"record":{"id":"56c3d82874d1fe6e","repo":"gradle/gradle","slug":"cannot-declare-module-replacement-s-s-because-i","errorCode":null,"errorMessage":"Cannot declare module replacement %s->%s because it introduces a cycle: %s","messagePattern":"Cannot declare module replacement (.+?)->(.+?) because it introduces a cycle: (.+?)","errorType":"validation","errorClass":"InvalidUserDataException","httpStatus":null,"severity":"error","filePath":"platforms/software/dependency-management/src/main/java/org/gradle/api/internal/artifacts/dsl/ComponentModuleMetadataContainer.java","lineNumber":96,"sourceCode":"\n    private static void detectCycles(Map<ModuleIdentifier, ImmutableModuleReplacements.Replacement> replacements, ModuleIdentifier source, ModuleIdentifier target) {\n        if (source.equals(target)) {\n            throw new InvalidUserDataException(String.format(\"Cannot declare module replacement that replaces self: %s->%s\", source, target));\n        }\n\n        ModuleIdentifier m = unwrap(replacements.get(target));\n        if (m == null) {\n            //target does not exist in the map, there's no cycle for sure\n            return;\n        }\n        Set<ModuleIdentifier> visited = new LinkedHashSet<>();\n        visited.add(source);\n        visited.add(target);\n\n        while(m != null) {\n            if (!visited.add(m)) {\n                //module was already visited, there is a cycle\n                throw new InvalidUserDataException(\n                        format(\"Cannot declare module replacement %s->%s because it introduces a cycle: %s\",\n                                source, target, Joiner.on(\"->\").join(visited) + \"->\" + source));\n            }\n            m = unwrap(replacements.get(m));\n        }\n    }\n\n    private static ModuleIdentifier unwrap(ImmutableModuleReplacements.Replacement replacement) {\n        return replacement == null ? null : replacement.getTarget();\n    }\n\n    private static NotationParser<Object, ModuleIdentifier> parser(ImmutableModuleIdentifierFactory moduleIdentifierFactory) {\n        return NotationParserBuilder\n                .toType(ModuleIdentifier.class)\n                .fromCharSequence(new ModuleIdentifierNotationConverter(moduleIdentifierFactory))\n                .toComposite();\n    }\n}","sourceCodeStart":78,"sourceCodeEnd":114,"githubUrl":"https://github.com/gradle/gradle/blob/534f27719b66953f95cc907aae7f2c1b12f5482d/platforms/software/dependency-management/src/main/java/org/gradle/api/internal/artifacts/dsl/ComponentModuleMetadataContainer.java#L78-L114","documentation":"Whenever a new module replacement module(a){ replacedBy(b) } is declared, detectCycles() walks the existing replacement chain starting from the new target. If the walk ever revisits a module already on the path, resolution would loop forever, so Gradle throws InvalidUserDataException and prints the exact cycle (source->target->...->source).","triggerScenarios":"module(\"g:a\"){ replacedBy(\"g:b\") } combined with module(\"g:b\"){ replacedBy(\"g:a\") }; longer rings such as g:a->g:b->g:c->g:a; any replacement whose target transitively resolves back to the source.","commonSituations":"Replacement declarations spread across multiple build scripts or plugins that individually look fine but together close a ring; two teams aliasing modules toward each other during a migration/merge; incrementally adding reverse-mappings after a rename.","solutions":["Pick one canonical module and make every other module point to it (a chain, never a ring) — e.g. keep g:a->g:b and change or delete g:b->g:a.","Audit all components.modules / componentModule declarations across every applied script and plugin and remove the redundant reverse mapping that closes the loop.","Rerun the build: the message includes the full cycle path (source->...->source), which names exactly which declarations to fix."],"exampleFix":"// before\ncomponents {\n    modules {\n        module(\"com.old:lib\")  { replacedBy(\"com.new:lib\") }\n        module(\"com.new:lib\")  { replacedBy(\"com.old:lib\") } // closes the cycle\n    }\n}\n\n// after\ncomponents {\n    modules {\n        module(\"com.old:lib\") { replacedBy(\"com.new:lib\") } // single direction only\n    }\n}","handlingStrategy":"validation","validationCode":"def declareReplacements(Map<String, String> replacements) {\n    def seen = new HashSet<String>()\n    replacements.each { src, tgt ->\n        def cur = tgt\n        while (cur != null) {\n            assert cur != src : \"Replacement cycle detected involving $src\"\n            cur = replacements[cur]\n        }\n    }\n    components.modules { replacements.each { s, t -> module(s).replacedBy(t) } }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Keep the replacement map acyclic by design: all arrows point toward one canonical module.","Centralize module replacements in a single script/plugin instead of scattering them across build files so cycles are visible at a glance."],"tags":["gradle","module-replacement","cycle","dependency-resolution"],"backgroundTag":"config-cycle-detected","analyzedSha":"534f27719b66953f95cc907aae7f2c1b12f5482d","analyzedAt":"2026-08-22T08:09:12.375Z","schemaVersion":2},"datasetVersion":"2026-08-22T09:17:25.309Z"}