From fe899daf64459dec1a86d498a4400c08012c1397 Mon Sep 17 00:00:00 2001 From: Anton Kurako Date: Sun, 19 Jul 2026 13:02:39 +0300 Subject: [PATCH 1/2] Improved DI diagnostics with readable AP and KSP error guidance Improved Kora DI compile-time diagnostics so unresolved, duplicate, circular, generic, and internal processor failures report clearer context, dependency paths, hints, and actionable fixes across annotation processing and KSP. - Improved user-facing dependency errors with consistent `Required at`, `Dependency resolution path`, `Hint`, and `Fix` sections. - Improved AP and KSP parity for dependency claim sources, component declarations, extension failures, and module hint diagnostics. - Fixed vague internal failures by replacing empty or raw unchecked exceptions with explicit `Kora internal error` messages. --- .../DependencyModuleHintProvider.java | 18 ++--- .../annotation/processor/GraphBuilder.java | 65 +++++++++++++--- .../processor/GraphFileGenerator.java | 2 +- .../processor/GraphResolutionHelper.java | 26 ++++--- .../processor/KoraAppProcessor.java | 34 ++++++-- .../annotation/processor/KoraAppUtils.java | 26 ++++++- .../processor/KoraSubmoduleProcessor.java | 18 ++++- .../component/ComponentDependency.java | 8 +- .../component/ComponentDependencyHelper.java | 33 ++++++-- .../processor/component/DependencyClaim.java | 33 +++++++- .../declaration/ComponentDeclaration.java | 50 ++++++++++-- .../CircularDependencyException.java | 24 +++--- .../DuplicateDependencyException.java | 33 +++++--- .../UnresolvedDependencyException.java | 57 ++++++++------ .../processor/extension/KoraExtension.java | 40 +++++++++- .../processor/ComponentTemplatesTest.java | 2 +- .../processor/KoraAppProcessorTest.java | 27 +++---- .../app/ksp/DependencyModuleHintProvider.kt | 22 +++--- .../kora/app/ksp/GraphBuilder.kt | 78 +++++++++++++++---- .../kora/app/ksp/GraphFileGenerator.kt | 5 +- .../kora/app/ksp/GraphResolutionHelper.kt | 15 ++-- .../kora/app/ksp/KoraAppProcessor.kt | 56 +++++++++++-- .../kora/app/ksp/KoraAppUtils.kt | 16 +++- .../kora/app/ksp/ServiceTypesHelper.kt | 4 +- .../app/ksp/component/ComponentDependency.kt | 6 +- .../component/ComponentDependencyHelper.kt | 77 +++++++++++++----- .../kora/app/ksp/component/DependencyClaim.kt | 3 +- .../ksp/declaration/ComponentDeclaration.kt | 40 +++++++++- .../exception/CircularDependencyException.kt | 24 +++--- .../exception/DuplicateDependencyException.kt | 34 ++++---- .../UnresolvedDependencyException.kt | 59 +++++++------- .../kora/app/ksp/extension/KoraExtension.kt | 46 ++++++++++- .../kora/app/ksp/KoraAppKspTest.kt | 11 ++- 33 files changed, 729 insertions(+), 263 deletions(-) diff --git a/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/DependencyModuleHintProvider.java b/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/DependencyModuleHintProvider.java index cb713f2b9..148210827 100644 --- a/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/DependencyModuleHintProvider.java +++ b/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/DependencyModuleHintProvider.java @@ -42,10 +42,9 @@ record ModuleHint(TypeName type, @Nullable String tag, String artifact, String m public String message() { if (tag == null) { return """ - Missing component: %s - Component is provided by standard Kora module you may forgot to plug it: - Gradle dependency: implementation("%s") - Module interface: %s + %s is provided by a standard Kora module. + Gradle dependency: implementation("%s") + Module interface: %s """.formatted(type, artifact, module); } else { String tagForMsg; @@ -56,10 +55,9 @@ public String message() { } return """ - Missing component: %s with %s - Component is provided by standard Kora module you may forgot to plug it: - Gradle dependency: implementation("%s") - Module interface: %s + %s with %s is provided by a standard Kora module. + Gradle dependency: implementation("%s") + Module interface: %s """.formatted(type, tagForMsg, artifact, module); } } @@ -88,7 +86,7 @@ List findHints(TypeMirror missingType, @Nullable String missingTag) { } else if (hint instanceof KoraHint.KoraTipHint t) { result.add(new Hint.TipHint(typeName, t.tag(), t.tip())); } else { - throw new UnsupportedOperationException("Unknown hint type: " + hint); + throw new IllegalStateException("Kora internal error: unknown dependency hint type: " + hint); } } } else { @@ -165,7 +163,7 @@ static KoraHint parse(JsonParser p) throws IOException { next = p.nextToken(); } if (!tags.isEmpty() && tags.size() != 1) { - throw new IllegalArgumentException("More than one tag found in hint: " + tags); + throw new IllegalStateException("Kora internal error: dependency hint declares more than one tag: " + tags); } } case "tag" -> { diff --git a/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/GraphBuilder.java b/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/GraphBuilder.java index 543749b2f..7ee511420 100644 --- a/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/GraphBuilder.java +++ b/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/GraphBuilder.java @@ -101,7 +101,13 @@ public Component withCurrentDependency(int currentDependency) { public ResolvedGraph build() { if (rootSet.isEmpty()) { throw new ProcessingErrorException( - "@KoraApp has no root components, expected at least one declaration annotated with @Root", + """ + @KoraApp has no root components. + + Fix: + - Annotate at least one component or module method with @Root. + - Check that root component is visible from this @KoraApp module set. + """.stripTrailing(), root ); } @@ -139,11 +145,31 @@ public ResolvedGraph build() { .filter(d -> componentCondition.canonicalName().equals(d.declaration().tag())) .toList(); if (conditionDeclarations.isEmpty()) { - throw new ProcessingErrorException("Component declares condition with tag %s, but none found in graph: %s".formatted(componentCondition.toString(), declaration), declaration.source()); + throw new ProcessingErrorException(""" + Component condition cannot be resolved: + required condition tag: @Tag(%s.class) + component: %s + + Fix: + - Add a GraphCondition component with this tag. + - Include a module that provides this GraphCondition. + - Check that @Conditional uses the intended tag. + """.formatted(componentCondition, declaration).stripTrailing(), declaration.source()); } if (conditionDeclarations.size() > 1) { var str = conditionDeclarations.stream().map(DeclarationWithIndex::declaration).map(Object::toString).collect(Collectors.joining("\n")).indent(2); - throw new ProcessingErrorException("Component declares condition with tag %s, but multiple candidates found in graph:\n%s\n%s".formatted(componentCondition.toString(), declaration, str), declaration.source()); + throw new ProcessingErrorException(""" + Multiple GraphCondition components match condition tag: + required condition tag: @Tag(%s.class) + component: %s + + Candidates: + %s + + Fix: + - Keep only one GraphCondition for this tag. + - Use different @Tag(...) values for different conditions. + """.formatted(componentCondition, declaration, str).stripTrailing(), declaration.source()); } var conditionDeclaration = conditionDeclarations.getFirst(); resolvedCondition = this.resolvedComponents.getByDeclaration(conditionDeclaration); @@ -266,7 +292,14 @@ public ResolvedGraph build() { try { extensionResult = Objects.requireNonNull(extension.generateDependency()); } catch (IOException e) { - throw new RuntimeException(e); + throw new ProcessingErrorException(""" + Extension failed to generate dependency: + dependency: %s + + Fix: + - Check earlier errors from the extension annotation processor. + - If no earlier errors exist, report this as a Kora extension bug. + """.formatted(dependencyClaim.type()).stripTrailing(), dependencyClaim.source() == null ? declaration.source() : dependencyClaim.source()); } var extensionComponent = switch (extensionResult) { case ExtensionResult.CodeBlockResult codeBlockResult -> ComponentDeclaration.fromExtension(codeBlockResult); @@ -301,7 +334,15 @@ public ResolvedGraph build() { for (var resolvedDependency : dependencies) { if (resolvedDependency.component().index() > component.index()) { throw new ProcessingErrorException( - "All dependency appeared in graph after component requesting it, this is a bug that we will fix later", + """ + All dependency appeared in graph after the component that requests it. + + This is an internal graph ordering limitation. + + Fix: + - Move the component that provides the All item so it is reachable before the requesting component. + - If this graph should be valid, please report this as a Kora bug with the dependency path. + """.stripTrailing(), resolvedDependency.component().declaration().source() ); } @@ -311,7 +352,7 @@ public ResolvedGraph build() { if (dependency instanceof ComponentDependency.PromisedProxyParameterDependency proxy) { var componentDeclarations = GraphResolutionHelper.findDependencyDeclarations(ctx, declarations, proxy.claim()); if (componentDeclarations.size() != 1) { - throw new IllegalStateException(); + throw new IllegalStateException("Kora internal error: promised proxy dependency expected exactly one target declaration, got " + componentDeclarations.size() + " for " + proxy.claim()); } var realDependency = Objects.requireNonNull(resolvedComponents.getByDeclaration(componentDeclarations.getFirst())); proxy.setPromised(realDependency); @@ -363,7 +404,7 @@ private ComponentDependency processAllOf(ResolutionFrame.Component componentFram if (dependencyClaim.claimType() == ALL_OF_PROMISE) { return new ComponentDependency.AllOfDependency(dependencyClaim); } - throw new IllegalStateException(); + throw new IllegalStateException("Kora internal error: processAllOf called for non-All dependency claim: " + dependencyClaim); } private List findInterceptors(ProcessingContext ctx, ResolvedComponents resolvedComponents, Deque resolutionStack, ComponentDeclaration declaration) { @@ -446,7 +487,7 @@ private static ComponentDeclaration generatePromisedProxy(ProcessingContext ctx, try { javaFile.build().writeTo(ctx.filer); } catch (IOException e) { - throw new RuntimeException(e); + throw new IllegalStateException("Kora internal error: failed to write promised proxy component for " + typeElement.getQualifiedName(), e); } return new ComponentDeclaration.PromisedProxyComponent(typeElement, ClassName.get(packageElement.getQualifiedName().toString(), resultClassName)); } @@ -481,12 +522,12 @@ private boolean checkCycle(ComponentDeclaration declaration) { var declarations = GraphResolutionHelper.findDependencyDeclarations(ctx, this.declarations, proxyDependencyClaim); if (!declarations.isEmpty()) { if (declarations.size() > 1) { - throw new IllegalStateException(); + throw new IllegalStateException("Kora internal error: promised proxy declaration is ambiguous for " + proxyDependencyClaim + ", declarations: " + declarations); } var decl = declarations.getFirst(); var resolved = resolvedComponents.getByDeclaration(decl); if (resolved == null) { - throw new IllegalStateException(); + throw new IllegalStateException("Kora internal error: promised proxy declaration was found but is not resolved: " + decl.declaration().declarationString()); } stack.removeLast(); prevComponent.resolvedDependencies().add(GraphResolutionHelper.toDependency(ctx, resolved, dependencyClaim)); @@ -506,7 +547,7 @@ private boolean checkCycle(ComponentDeclaration declaration) { ctx, declaration, templates, proxyDependencyClaim ); if (proxyComponentDeclarations.size() != 1) { - throw new IllegalStateException(); + throw new IllegalStateException("Kora internal error: generated promised proxy template expected one declaration, got " + proxyComponentDeclarations.size() + " for " + proxyDependencyClaim); } proxyComponentDeclaration = proxyComponentDeclarations.getFirst(); declIdx = this.declarations.add(proxyComponentDeclaration); @@ -516,7 +557,7 @@ private boolean checkCycle(ComponentDeclaration declaration) { } } else { if (proxyComponentDeclarations.size() > 1) { - throw new IllegalStateException(); + throw new IllegalStateException("Kora internal error: promised proxy template is ambiguous for " + proxyDependencyClaim + ", declarations: " + proxyComponentDeclarations); } proxyComponentDeclaration = proxyComponentDeclarations.getFirst(); declIdx = this.declarations.add(proxyComponentDeclaration); diff --git a/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/GraphFileGenerator.java b/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/GraphFileGenerator.java index c3df1f5fd..d242ff70b 100644 --- a/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/GraphFileGenerator.java +++ b/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/GraphFileGenerator.java @@ -254,7 +254,7 @@ private CodeBlock generateComponentStatement(ClassName graphTypeName, ResolvedCo var optionalOf = ((DeclaredType) optional.type()).getTypeArguments().get(0); statement.add("$T.<$T>ofNullable($L)", Optional.class, optionalOf, dependenciesCode); } - case null, default -> throw new RuntimeException("Unknown type " + declaration); + case null, default -> throw new IllegalStateException("Kora internal error: graph generator got unsupported component declaration: " + declaration); } statement.add(")"); return statement.build(); diff --git a/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/GraphResolutionHelper.java b/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/GraphResolutionHelper.java index 5a8672b39..df56c3621 100644 --- a/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/GraphResolutionHelper.java +++ b/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/GraphResolutionHelper.java @@ -45,7 +45,7 @@ public static ComponentDependency.SingleDependency toDependency(ProcessingContex var isDirectAssignable = ctx.types.isAssignable(resolvedComponent.type(), dependencyClaim.type()); var isWrappedAssignable = ctx.serviceTypeHelper.isAssignableToUnwrapped(resolvedComponent.type(), dependencyClaim.type()); if (!isDirectAssignable && !isWrappedAssignable) { - throw new IllegalStateException(); + throw new IllegalStateException("Kora internal error: resolved component is not assignable to dependency claim. Component=" + resolvedComponent.declaration().declarationString() + ", claim=" + dependencyClaim); } var targetDependency = isWrappedAssignable @@ -56,7 +56,7 @@ public static ComponentDependency.SingleDependency toDependency(ProcessingContex case ONE_REQUIRED, ONE_NULLABLE, NODE_OF -> targetDependency; case PROMISE_OF, NULLABLE_PROMISE_OF -> new ComponentDependency.PromiseOfDependency(dependencyClaim, targetDependency); case VALUE_OF, NULLABLE_VALUE_OF -> new ComponentDependency.ValueOfDependency(dependencyClaim, targetDependency); - case ALL_OF_ONE, ALL_OF_PROMISE, ALL_OF_VALUE, TYPE_REF, GRAPH -> throw new IllegalStateException(); + case ALL_OF_ONE, ALL_OF_PROMISE, ALL_OF_VALUE, TYPE_REF, GRAPH -> throw new IllegalStateException("Kora internal error: unsupported single dependency claim type " + dependencyClaim.claimType() + " for " + dependencyClaim); }; } @@ -84,8 +84,7 @@ public static List findDependenciesForAllO // that's fine, default component wasn't directly requested by anyone, so we don't need it continue; } else { - // something went wrong - throw new NullPointerException(); + throw new IllegalStateException("Kora internal error: non-default All dependency declaration is not resolved: " + declaration.declarationString()); } } if (ctx.types.isAssignable(declaration.type(), dependencyClaim.type())) { @@ -94,7 +93,7 @@ public static List findDependenciesForAllO case ALL_OF_ONE -> targetDependency; case ALL_OF_PROMISE -> new ComponentDependency.PromiseOfDependency(dependencyClaim, targetDependency); case ALL_OF_VALUE -> new ComponentDependency.ValueOfDependency(dependencyClaim, targetDependency); - case null, default -> throw new IllegalStateException("Unexpected value: " + dependencyClaim.claimType()); + case null, default -> throw new IllegalStateException("Kora internal error: unsupported All claim type " + dependencyClaim.claimType() + " for " + dependencyClaim); }; result.add(dependency); } @@ -104,7 +103,7 @@ public static List findDependenciesForAllO case ALL_OF_ONE -> targetDependency; case ALL_OF_PROMISE -> new ComponentDependency.PromiseOfDependency(dependencyClaim, targetDependency); case ALL_OF_VALUE -> new ComponentDependency.ValueOfDependency(dependencyClaim, targetDependency); - case null, default -> throw new IllegalStateException("Unexpected value: " + dependencyClaim.claimType()); + case null, default -> throw new IllegalStateException("Kora internal error: unsupported wrapped All claim type " + dependencyClaim.claimType() + " for " + dependencyClaim); }; result.add(dependency); } @@ -114,12 +113,19 @@ public static List findDependenciesForAllO public static List findDependencyDeclarationsFromTemplate(ProcessingContext ctx, ComponentDeclaration forDeclaration, List sourceDeclarations, DependencyClaim dependencyClaim) { if (dependencyClaim.type().getKind() == TypeKind.ERROR) { - throw new ProcessingErrorException("Component error type dependency claim " + dependencyClaim.type(), forDeclaration.source()); + throw new ProcessingErrorException(""" + Dependency type cannot be resolved: + type: %s + + Fix: + - Check imports and module dependencies. + - Compile again after fixing earlier compiler errors. + """.formatted(dependencyClaim.type()).stripTrailing(), dependencyClaim.source() == null ? forDeclaration.source() : dependencyClaim.source()); } var claimType = dependencyClaim.claimType(); if (claimType == ALL_OF_ONE || claimType == ALL_OF_PROMISE || claimType == ALL_OF_VALUE) { - throw new UnsupportedOperationException(); + throw new IllegalStateException("Kora internal error: component templates cannot be resolved for All dependency claim: " + dependencyClaim); } var types = ctx.types; var declarations = new ArrayList(); @@ -138,7 +144,7 @@ public static List findDependencyDeclarationsFromTemplate( continue sources; } if (!(match instanceof ComponentTemplateHelper.TemplateMatch.Some(var map))) { - throw new IllegalStateException(); + throw new IllegalStateException("Kora internal error: unknown component template match result " + match + " for " + sourceDeclaration.declarationString()); } var realReturnType = ComponentTemplateHelper.replace(types, declarationDeclaredType, map); @@ -212,7 +218,7 @@ public static List findDependencyDeclarationsFromTemplate( )); } case ComponentDeclaration.PromisedProxyComponent promisedProxyComponent -> declarations.add(promisedProxyComponent.withType(realReturnType)); - default -> throw new IllegalArgumentException(sourceDeclaration.toString()); + default -> throw new IllegalStateException("Kora internal error: unsupported template component declaration: " + sourceDeclaration); } } if (declarations.isEmpty()) { diff --git a/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/KoraAppProcessor.java b/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/KoraAppProcessor.java index ca7d9d8c4..63bee3b91 100644 --- a/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/KoraAppProcessor.java +++ b/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/KoraAppProcessor.java @@ -67,7 +67,7 @@ protected void process(Set annotations, RoundEnvironment } catch (ProcessingErrorException e) { e.printError(this.processingEnv); } catch (IOException e) { - throw new UncheckedIOException(e); + throw new IllegalStateException("Kora internal error: failed to write generated graph for @KoraApp " + element.getQualifiedName(), e); } } } @@ -82,7 +82,13 @@ private void processApps(Map> annotatedElement } this.koraApps.add((TypeElement) element); } else { - this.processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@KoraApp can be placed only on interfaces", element); + this.processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, """ + @KoraApp can only be applied to interfaces. + + Fix: + - Change this type to an interface. + - Move @KoraApp to an interface that declares root components and modules. + """.stripTrailing(), element); } } } @@ -113,7 +119,13 @@ private void processModules(Map> annotatedElem if (member.getKind() == ElementKind.METHOD && member.getModifiers().contains(Modifier.DEFAULT)) { var method = (ExecutableElement) member; if (method.getReturnType().getKind() != TypeKind.DECLARED) { - messager.printMessage(Diagnostic.Kind.ERROR, "Only reference types are allowed as graph components"); + messager.printMessage(Diagnostic.Kind.ERROR, """ + Module method returns a non-reference type, so it cannot be used as a graph component. + + Fix: + - Return a class or interface type. + - Wrap primitive values in a reference type. + """.stripTrailing(), method); } } } @@ -125,14 +137,26 @@ private void processModules(Map> annotatedElem // this.annotatedClassModules.add(te); // } } else { - messager.printMessage(Diagnostic.Kind.ERROR, "Only interfaces are allowed as modules"); + messager.printMessage(Diagnostic.Kind.ERROR, """ + @Module can only be applied to interfaces. + + Fix: + - Change this type to an interface. + - Move module factory methods to an interface annotated with @Module. + """.stripTrailing(), annotated.element()); } } } private ResolvedGraph buildGraph(RoundEnvironment roundEnv, ProcessingContext ctx, Element classElement) { if (classElement.getKind() != ElementKind.INTERFACE) { - throw new ProcessingErrorException("@KoraApp is only applicable to interfaces", classElement); + throw new ProcessingErrorException(""" + @KoraApp can only be applied to interfaces. + + Fix: + - Change this type to an interface. + - Move @KoraApp to an interface that declares root components and modules. + """.stripTrailing(), classElement); } var type = (TypeElement) classElement; var interfaces = KoraAppUtils.collectInterfaces(this.types, type); diff --git a/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/KoraAppUtils.java b/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/KoraAppUtils.java index b86cf8361..edf8fadea 100644 --- a/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/KoraAppUtils.java +++ b/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/KoraAppUtils.java @@ -38,7 +38,13 @@ static List parseComponents(ProcessingContext ctx, Collect } if (AnnotationUtils.isAnnotationPresent(executableElement, CommonClassNames.factoryModule)) { if (executableElement.getReturnType().getKind() != TypeKind.DECLARED) { - throw new ProcessingErrorException("@FactoryModule method must return a class type", executableElement); + throw new ProcessingErrorException(""" + @FactoryModule method must return a class or interface type. + + Fix: + - Change the return type to a module class/interface. + - Remove @FactoryModule if this method is a regular provider. + """.stripTrailing(), executableElement); } result.add(ComponentDeclaration.fromModule(ctx, module, executableElement)); var returnTypeElement = (TypeElement) ctx.types.asElement(executableElement.getReturnType()); @@ -143,7 +149,14 @@ static Set collectInterfaces(Types types, TypeElement typeElement) private static void collectInterfaces(Types types, Set collectedElements, TypeElement typeElement) { if (collectedElements.add(typeElement)) { if (typeElement.asType().getKind() == TypeKind.ERROR) { - throw new ProcessingErrorException("Element is error: %s".formatted(typeElement.toString()), typeElement); + throw new ProcessingErrorException(""" + Type cannot be resolved during @KoraApp processing: + type: %s + + Fix: + - Check imports and module dependencies. + - Compile again after fixing earlier compiler errors. + """.formatted(typeElement).stripTrailing(), typeElement); } for (var directlyImplementedInterface : typeElement.getInterfaces()) { var interfaceElement = (TypeElement) types.asElement(directlyImplementedInterface); @@ -160,7 +173,14 @@ public static List findKoraSubmoduleModules(Elements elements, Set< var name = typeElement.getQualifiedName().toString() + "SubmoduleImpl"; var module = elements.getTypeElement(name); if (module == null) { - throw new ProcessingErrorException("Submodule `" + name + "` was not generated yet", typeElement); + throw new ProcessingErrorException(""" + Kora submodule was not generated yet: + expected type: %s + + Fix: + - Ensure the submodule processor is enabled. + - Compile again after generated sources are available. + """.formatted(name).stripTrailing(), typeElement); } else { result.add(module); } diff --git a/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/KoraSubmoduleProcessor.java b/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/KoraSubmoduleProcessor.java index b99446455..0a84d22cc 100644 --- a/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/KoraSubmoduleProcessor.java +++ b/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/KoraSubmoduleProcessor.java @@ -51,7 +51,7 @@ protected void process(Set annotations, RoundEnvironment try { this.generateAppParts(); } catch (IOException e) { - throw new RuntimeException(e); + throw new IllegalStateException("Kora internal error: failed to write generated @KoraSubmodule implementation", e); } } } @@ -231,12 +231,24 @@ private ExecutableElement findSinglePublicConstructor(TypeElement element) { .toList(); if (constructors.isEmpty()) { throw new ProcessingErrorException( - "Type annotated with @Component has no public constructors", element + """ + @Component type has no public constructors. + + Fix: + - Add one public constructor. + - Move construction to a module method if constructor cannot be public. + """.stripTrailing(), element ); } if (constructors.size() > 1) { throw new ProcessingErrorException( - "Type annotated with @Component has more then one public constructor", element + """ + @Component type has more than one public constructor. + + Fix: + - Keep exactly one public constructor. + - Make extra constructors non-public. + """.stripTrailing(), element ); } return constructors.get(0); diff --git a/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/component/ComponentDependency.java b/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/component/ComponentDependency.java index 1572dbd2b..52a1f372d 100644 --- a/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/component/ComponentDependency.java +++ b/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/component/ComponentDependency.java @@ -26,7 +26,7 @@ default CodeBlock write(ProcessingContext ctx, ClassName graphTypeName) { case ALL_OF_ONE -> codeBlock.add("$T.all(g", CommonClassNames.all); case ALL_OF_VALUE -> codeBlock.add("$T.allValues(g", CommonClassNames.all); case ALL_OF_PROMISE -> codeBlock.add("$T.allPromises(g", CommonClassNames.all); - default -> throw new IllegalStateException("Unknown claim type: " + allOf.claim().claimType()); + default -> throw new IllegalStateException("Kora internal error: unsupported All claim type for code generation: " + allOf.claim()); } for (var dependency : allOf.resolvedDependencies) { var dependencyNode = dependency.component().nodeRef("some_fake_holder_idc"); @@ -44,7 +44,7 @@ case NullDependency(var claim) -> switch (claim.claimType()) { case ONE_NULLABLE -> CodeBlock.of("($T) null", claim.type()); case NULLABLE_VALUE_OF -> CodeBlock.of("($T<$T>) null", CommonClassNames.valueOf, claim.type()); case NULLABLE_PROMISE_OF -> CodeBlock.of("($T<$T>) null", CommonClassNames.promiseOf, claim.type()); - default -> throw new IllegalArgumentException(claim.claimType().toString()); + default -> throw new IllegalStateException("Kora internal error: unsupported nullable dependency claim type for code generation: " + claim); }; case PromisedProxyParameterDependency promised -> { var dependency = Objects.requireNonNull(promised.realDependency); @@ -56,7 +56,7 @@ case PromiseOfDependency(_, var delegate) when delegate instanceof WrappedTarget case TargetDependency(var claim, var component) -> switch (claim.claimType()) { case ONE_REQUIRED, ONE_NULLABLE -> CodeBlock.of("g.get($T.$N.$N)", graphTypeName, component.holderName(), component.fieldName()); case NODE_OF -> CodeBlock.of("$T.$N.$N", graphTypeName, component.holderName(), component.fieldName()); - default -> throw new IllegalStateException("Unexpected value: " + claim.claimType()); + default -> throw new IllegalStateException("Kora internal error: unsupported target dependency claim type for code generation: " + claim); }; case TypeOfDependency(var claim) -> TypeOfDependency.buildTypeRef(ctx.types, claim.type()); case ValueOfDependency(_, var delegate) when delegate instanceof WrappedTargetDependency -> @@ -75,7 +75,7 @@ case ValueOfDependency(_, var delegate) when delegate instanceof WrappedTargetDe case PROMISE_OF -> { b.add("g.getOnePromiseOf("); } - default -> throw new IllegalStateException("Unknown claim type: " + oneOfDependency.claim().claimType()); + default -> throw new IllegalStateException("Kora internal error: unsupported one-of dependency claim type for code generation: " + oneOfDependency.claim()); } for (int i = 0; i < oneOfDependency.dependencies().size(); i++) { if (i > 0) b.add(", "); diff --git a/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/component/ComponentDependencyHelper.java b/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/component/ComponentDependencyHelper.java index 213df3123..781344f34 100644 --- a/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/component/ComponentDependencyHelper.java +++ b/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/component/ComponentDependencyHelper.java @@ -41,7 +41,7 @@ public static List parseDependencyClaims(ProcessingContext ctx, } } var isNullable = CommonUtils.isNullable(parameterElement); - result.add(parseClaim(element, parameterType, tag, isNullable)); + result.add(parseClaim(parameterElement, parameterType, tag, isNullable)); } return result; } @@ -53,7 +53,7 @@ public static List parseDependencyClaims(ProcessingContext ctx, var parameterElement = element.getParameters().get(i); var tags = TagUtils.parseTagValue(parameterElement); var isNullable = CommonUtils.isNullable(parameterElement); - result.add(parseClaim(element, parameterType, tags, isNullable)); + result.add(parseClaim(parameterElement, parameterType, tags, isNullable)); } return result; } @@ -65,19 +65,29 @@ public static List parseDependencyClaims(ProcessingContext ctx, for (int i = 0; i < fromExtension.dependencyTypes().size(); i++) { var parameterType = fromExtension.dependencyTypes().get(i); var tags = fromExtension.dependencyTags().get(i); - var element = executable == null ? null : executable.getParameters().get(i); + var element = executable == null ? fromExtension.source() : executable.getParameters().get(i); var isNullable = element != null && CommonUtils.isNullable(element); - result.add(parseClaim(fromExtension.source(), parameterType, tags, isNullable)); + result.add(parseClaim(element, parameterType, tags, isNullable)); } return result; } - case ComponentDeclaration.OptionalComponent _, ComponentDeclaration.PromisedProxyComponent _ -> throw new IllegalArgumentException(); + case ComponentDeclaration.OptionalComponent _, ComponentDeclaration.PromisedProxyComponent _ -> + throw new IllegalStateException("Kora internal error: synthetic component cannot declare dependencies: " + componentDeclaration); } } public static DependencyClaim parseClaim(Element sourceElement, TypeMirror parameterType, @Nullable String tag, boolean isNullable) { if (TypeParameterUtils.hasRawTypes(parameterType)) { - throw new ProcessingErrorException("Components with raw types can break dependency resolution in unpredictable way so they are forbidden", sourceElement); + throw new ProcessingErrorException(""" + Dependency uses a raw type: + type: %s + + Raw types are forbidden because they make dependency resolution ambiguous. + + Fix: + - Specify generic type arguments explicitly. + - Replace raw collections/providers with parameterized types. + """.formatted(parameterType).stripTrailing(), sourceElement); } var typeName = TypeName.get(parameterType); @@ -98,7 +108,16 @@ public static DependencyClaim parseClaim(Element sourceElement, TypeMirror param return new DependencyClaim(sourceElement, wildcard.getExtendsBound(), tag, DependencyClaimType.NODE_OF); } } - throw new ProcessingErrorException("Unexpected Node argument: expected Node ", sourceElement); + throw new ProcessingErrorException(""" + Invalid Node dependency argument. + + Expected: + Node + + Fix: + - Use a concrete reference type as Node. + - Avoid wildcards without an extends bound. + """.stripTrailing(), sourceElement); } if (ptn.rawType().canonicalName().equals(CommonClassNames.all.canonicalName())) { if (ptn.typeArguments().getFirst() instanceof ParameterizedTypeName allOfType && dt.getTypeArguments().getFirst() instanceof DeclaredType allOfTypeName) { diff --git a/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/component/DependencyClaim.java b/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/component/DependencyClaim.java index b77fc17f4..6d69f9b48 100644 --- a/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/component/DependencyClaim.java +++ b/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/component/DependencyClaim.java @@ -11,21 +11,46 @@ import javax.lang.model.type.TypeMirror; import java.util.Objects; -public record DependencyClaim(TypeMirror type, @Nullable String tag, DependencyClaimType claimType) { +public record DependencyClaim(TypeMirror type, @Nullable String tag, DependencyClaimType claimType, @Nullable Element source) { public DependencyClaim { Objects.requireNonNull(type); Objects.requireNonNull(claimType); if (TypeParameterUtils.hasTypeParameter(type)) { - throw new IllegalStateException("Component can't have generic dependencies: " + type); + if (source != null) { + throw new ProcessingErrorException(""" + Dependency uses an unresolved generic type: + type: %s + + Kora dependency keys must be concrete types. + + Fix: + - Bind the generic type parameter to a concrete type. + - Move generic construction to a component template or module method. + """.formatted(type).stripTrailing(), source); + } + throw new IllegalStateException("Kora internal error: generic dependency claim was created without source element: " + type); } } + public DependencyClaim(TypeMirror type, @Nullable String tag, DependencyClaimType claimType) { + this(type, tag, claimType, null); + } + public DependencyClaim(Element src, TypeMirror type, @Nullable String tag, DependencyClaimType claimType) { if (type.getKind() != TypeKind.DECLARED) { - throw new ProcessingErrorException("Only reference types are allowed as graph components, got " + type + " at " + src, src); + throw new ProcessingErrorException(""" + Dependency has non-reference type: + type: %s + + Kora graph components must be classes or interfaces. + + Fix: + - Use a reference type instead of a primitive or void type. + - Wrap primitive values in a class or boxed type. + """.formatted(type).stripTrailing(), src); } - this(type, tag, claimType); + this(type, tag, claimType, src); } public boolean tagsMatches(String other) { diff --git a/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/declaration/ComponentDeclaration.java b/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/declaration/ComponentDeclaration.java index a9a611f3a..aa42d4a5e 100644 --- a/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/declaration/ComponentDeclaration.java +++ b/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/declaration/ComponentDeclaration.java @@ -236,14 +236,29 @@ public String toString() { static ComponentDeclaration fromModule(ProcessingContext ctx, ModuleDeclaration module, ExecutableElement method) { var type = method.getReturnType(); if (TypeParameterUtils.hasRawTypes(type)) { - throw new ProcessingErrorException("Components with raw types can break dependency resolution in unpredictable way so they are forbidden", method); + throw new ProcessingErrorException(""" + Component provider returns a raw type: + type: %s + + Raw component types are forbidden because they make dependency resolution ambiguous. + + Fix: + - Specify generic type arguments in the return type. + - Return a concrete parameterized type. + """.formatted(type).stripTrailing(), method); } var tag = TagUtils.parseTagValue(method); if (CommonClassNames.tagFactory.canonicalName().equals(tag)) { if (module instanceof ModuleDeclaration.FactoryModule(var _, var moduleTag)) { tag = moduleTag; } else { - throw new ProcessingErrorException("Tag @Tag.Factory is only allowed for factory modules ", method); + throw new ProcessingErrorException(""" + @Tag.Factory can only be used inside factory modules. + + Fix: + - Move this provider to a factory module. + - Replace @Tag.Factory with an explicit @Tag(...) value. + """.stripTrailing(), method); } } var conditionalAnnotation = AnnotationUtils.findAnnotation(method, CommonClassNames.conditional); @@ -259,7 +274,14 @@ static ComponentDeclaration fromModule(ProcessingContext ctx, ModuleDeclaration static ComponentDeclaration fromAnnotated(ProcessingContext ctx, TypeElement typeElement) { var constructors = CommonUtils.findConstructors(typeElement, m -> m.contains(Modifier.PUBLIC)); if (constructors.size() != 1) { - throw new ProcessingErrorException("Components with raw types can break dependency resolution in unpredictable way so they are forbidden", typeElement); + throw new ProcessingErrorException(""" + @Component class must have exactly one public constructor. + + Fix: + - Keep one public constructor. + - Make extra constructors non-public. + - Move complex construction logic to a module method. + """.stripTrailing(), typeElement); } var constructor = constructors.get(0); var type = typeElement.asType(); @@ -292,7 +314,16 @@ static ComponentDeclaration fromExtension(ProcessingContext ctx, ExtensionResult } var type = typeElement.asType(); if (TypeParameterUtils.hasRawTypes(type)) { - throw new ProcessingErrorException("Components with raw types can break dependency resolution in unpredictable way so they are forbidden", sourceMethod); + throw new ProcessingErrorException(""" + Extension component uses a raw type: + type: %s + + Raw component types are forbidden because they make dependency resolution ambiguous. + + Fix: + - Specify generic type arguments explicitly. + - Generate a concrete parameterized component type. + """.formatted(type).stripTrailing(), sourceMethod); } var className = ClassName.get(typeElement); @@ -303,7 +334,16 @@ static ComponentDeclaration fromExtension(ProcessingContext ctx, ExtensionResult var type = generatedResult.targetType().getReturnType(); var parameterTypes = generatedResult.targetType().getParameterTypes(); if (TypeParameterUtils.hasRawTypes(type)) { - throw new ProcessingErrorException("Components with raw types can break dependency resolution in unpredictable way so they are forbidden", sourceMethod); + throw new ProcessingErrorException(""" + Extension component provider returns a raw type: + type: %s + + Raw component types are forbidden because they make dependency resolution ambiguous. + + Fix: + - Specify generic type arguments explicitly. + - Generate a concrete parameterized return type. + """.formatted(type).stripTrailing(), sourceMethod); } var parameterTags = sourceMethod.getParameters().stream().map(TagUtils::parseTagValue).toList(); var tag = TagUtils.parseTagValue(sourceMethod); diff --git a/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/exception/CircularDependencyException.java b/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/exception/CircularDependencyException.java index 45ba83e5b..ea1665706 100644 --- a/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/exception/CircularDependencyException.java +++ b/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/exception/CircularDependencyException.java @@ -18,20 +18,22 @@ public CircularDependencyException(List cycle, ComponentDe private static ProcessingError getError(List cycle, ComponentDeclaration declaration) { var deps = cycle.stream() - .map(c -> String.format("- %s", c.declarationString())) - .collect(Collectors.joining("\n", "Cycle dependency candidates:\n", "")).indent(2); + .map(ComponentDeclaration::declarationString) + .collect(Collectors.joining("\n ^--- ", "Dependency cycle:\n @--- ", "")).indent(2); + var msg = new StringBuilder(); + msg.append("Circular dependency found:\n ").append(TypeName.get(declaration.type())); if (declaration.tag() == null) { - return new ProcessingError("Encountered circular dependency in graph for source type: " + TypeName.get(declaration.type()) + " (no tags)\n" - + deps - + "\nPlease check that you are not using cycle dependency in %s, this is forbidden.".formatted(CommonClassNames.lifecycle), - declaration.source()); + msg.append(" (no tags)"); } else { - var tagMsg = "@Tag(" + declaration.tag() + ".class)"; - return new ProcessingError("Encountered circular dependency in graph for source type: " + TypeName.get(declaration.type()) + " with " + tagMsg + "\n" - + deps - + "\nPlease check that you are not using cycle dependency in %s, this is forbidden.".formatted(CommonClassNames.lifecycle), - declaration.source()); + msg.append(" with @Tag(").append(declaration.tag()).append(".class)"); } + msg.append("\n\n").append(deps.stripTrailing()); + msg.append(" [CYCLE]"); + msg.append("\n\nFix:"); + msg.append("\n - Break the cycle with ValueOf or PromiseOf where lazy access is valid."); + msg.append("\n - Move shared state into a separate component."); + msg.append("\n - Do not create dependency cycles in ").append(CommonClassNames.lifecycle).append('.'); + return new ProcessingError(msg.toString(), declaration.source()); } } diff --git a/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/exception/DuplicateDependencyException.java b/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/exception/DuplicateDependencyException.java index 68c1c20a6..1a12b4525 100644 --- a/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/exception/DuplicateDependencyException.java +++ b/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/exception/DuplicateDependencyException.java @@ -7,6 +7,7 @@ import io.koraframework.kora.app.annotation.processor.component.DependencyClaim; import io.koraframework.kora.app.annotation.processor.declaration.ComponentDeclaration; +import javax.lang.model.element.VariableElement; import java.util.List; import java.util.stream.Collectors; @@ -29,7 +30,7 @@ private static ProcessingError getErrorForDeclarations(DependencyClaim claim, List foundDeclarations) { var deps = foundDeclarations.stream() .map(c -> String.format("- %s", c.declarationString())) - .collect(Collectors.joining("\n", "Candidates for injection:\n", "")).indent(2); + .collect(Collectors.joining("\n", "Candidates:\n", "")).indent(2); return getError(claim, declaration, deps); } @@ -40,7 +41,7 @@ private static ProcessingError getErrorForDependencies(DependencyClaim claim, var deps = foundDeclarations.stream() .map(ComponentDependency.SingleDependency::component) .map(c -> String.format("- %s", c.declaration().declarationString())) - .collect(Collectors.joining("\n", "Candidates for injection:\n", "")).indent(2); + .collect(Collectors.joining("\n", "Candidates:\n", "")).indent(2); return getError(claim, declaration, deps); } @@ -48,17 +49,27 @@ private static ProcessingError getErrorForDependencies(DependencyClaim claim, private static ProcessingError getError(DependencyClaim claim, ComponentDeclaration declaration, String deps) { + var msg = new StringBuilder(); + msg.append("Multiple components match dependency:\n ").append(TypeName.get(claim.type())); if (claim.tag() == null) { - return new ProcessingError("More than one declaration matches dependency type: " + TypeName.get(claim.type()) + " (no tags)\n" - + deps - + "\nPlease check that injection dependency is declared correctly or that @DefaultComponent annotation is not missing if was intended.", - declaration.source()); + msg.append(" (no tags)"); } else { - var tagMsg = "@Tag(" + claim.tag() + ".class)"; - return new ProcessingError("More than one declaration matches dependency type: " + TypeName.get(claim.type()) + " with " + tagMsg + "\n" - + deps - + "\nPlease check that injection dependency is declared correctly or that @DefaultComponent annotation is not missing if was intended.", - declaration.source()); + msg.append(" with @Tag(").append(claim.tag()).append(".class)"); } + var source = claim.source(); + if (source instanceof VariableElement variableElement) { + msg.append("\n\nRequired at:\n ") + .append(variableElement.getEnclosingElement()) + .append("\n parameter: ") + .append(variableElement.asType()) + .append(" ") + .append(variableElement.getSimpleName()); + } + msg.append("\n\n").append(deps.stripTrailing()); + msg.append("\n\nFix:"); + msg.append("\n - Add different @Tag(...) annotations to candidates and request the needed tag."); + msg.append("\n - Mark fallback candidate with @DefaultComponent."); + msg.append("\n - Remove one duplicate provider."); + return new ProcessingError(msg.toString(), claim.source() == null ? declaration.source() : claim.source()); } } diff --git a/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/exception/UnresolvedDependencyException.java b/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/exception/UnresolvedDependencyException.java index 2ac219fd7..f26f49785 100644 --- a/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/exception/UnresolvedDependencyException.java +++ b/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/exception/UnresolvedDependencyException.java @@ -12,6 +12,7 @@ import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; import java.util.ArrayList; import java.util.Deque; import java.util.List; @@ -27,7 +28,12 @@ public UnresolvedDependencyException(TypeElement koraApp, Deque stack, List hints) { - this(component, dependencyClaim, List.of(new ProcessingError(constructErrorMessage(koraApp, component, dependencyClaim, stack, hints), component.source())), stack); + this(component, dependencyClaim, List.of(getError(koraApp, component, dependencyClaim, stack, hints)), stack); + } + + private static ProcessingError getError(TypeElement koraApp, ComponentDeclaration component, DependencyClaim dependencyClaim, Deque stack, List hints) { + var errorSource = dependencyClaim.source() == null ? component.source() : dependencyClaim.source(); + return new ProcessingError(constructErrorMessage(koraApp, component, dependencyClaim, stack, hints), errorSource); } @@ -55,33 +61,40 @@ public Deque getStack() { private static String constructErrorMessage(TypeElement koraApp, ComponentDeclaration component, DependencyClaim dependencyClaim, Deque stack, List hints) { var msg = new StringBuilder(); + msg.append("No component found for dependency:\n "); + msg.append(TypeName.get(dependencyClaim.type())); if (dependencyClaim.tag() == null) { - var format = """ - Required dependency type wasn't found in graph and can't be auto created: %s (no tags) - Please check class for @%s annotation or that required module with declaration factory is plugged in."""; - msg.append(String.format(format, TypeName.get(dependencyClaim.type()), CommonClassNames.component.simpleName())); + msg.append(" (no tags)"); } else { - var tagMsg = "@Tag(" + dependencyClaim.tag() + ".class)"; - var format = """ - Required dependency type wasn't found in graph and can't be auto created: %s with tag %s. - Please check class for @%s annotation or that required module with declaration factory is plugged in."""; - msg.append(String.format(format, TypeName.get(dependencyClaim.type()), tagMsg, CommonClassNames.component.simpleName())); - } - if (!hints.isEmpty()) { - msg.append("\n\nHints:"); - for (var hint : hints) { - msg.append("\n - Hint: ").append(hint.message()); - } + msg.append(" with ").append(formatTag(dependencyClaim.tag())); } var requestedMsg = getRequestedMessage(component); - msg.append("\n").append(requestedMsg); + msg.append("\n\nRequired at:\n ").append(requestedMsg); + var source = dependencyClaim.source(); + if (source instanceof VariableElement variableElement) { + msg.append("\n parameter: ").append(variableElement.asType()).append(" ").append(variableElement.getSimpleName()); + } var treeMsg = getDependencyTreeSimpleMessage(koraApp, stack, component, dependencyClaim); - msg.append("\n").append(treeMsg); + msg.append("\n\n").append(treeMsg); + if (!hints.isEmpty()) { + msg.append("\n\nHint:"); + for (var hint : hints) { + msg.append("\n - ").append(hint.message().strip().replace("\n", "\n ")); + } + } + msg.append("\n\nFix:"); + msg.append("\n - Add @").append(CommonClassNames.component.simpleName()).append(" to an implementation of ").append(TypeName.get(dependencyClaim.type())).append('.'); + msg.append("\n - Add a module method that returns ").append(TypeName.get(dependencyClaim.type())).append('.'); + msg.append("\n - Include a module that provides ").append(TypeName.get(dependencyClaim.type())).append(" in @KoraApp."); return msg.toString(); } + private static String formatTag(String tag) { + return "@Tag(" + tag + ".class)"; + } + private static String getRequestedMessage(ComponentDeclaration declaration) { var element = declaration.source(); var factoryMethod = (ExecutableElement) null; @@ -99,9 +112,9 @@ private static String getRequestedMessage(ComponentDeclaration declaration) { } while (element != null); if (module != null && factoryMethod != null && factoryMethod.getKind() == ElementKind.CONSTRUCTOR) { - return "Dependency requested at: %s.%s".formatted(module.getEnclosingElement(), factoryMethod); + return "%s.%s".formatted(module.getEnclosingElement(), factoryMethod); } else { - return "Dependency requested at: %s#%s".formatted(module, factoryMethod); + return "%s#%s".formatted(module, factoryMethod); } } @@ -110,7 +123,7 @@ private static String getDependencyTreeSimpleMessage(TypeElement koraApp, ComponentDeclaration declaration, DependencyClaim dependencyClaim) { var msg = new StringBuilder(); - msg.append("Dependency resolution tree:"); + msg.append("Dependency resolution path:"); var stackFrames = new ArrayList(); var i = stack.descendingIterator(); @@ -147,7 +160,7 @@ private static String getDependencyTreeSimpleMessage(TypeElement koraApp, msg.append(delimiter).append(declaration.declarationString()); - var errorMissing = " [ ERROR: MISSING COMPONENT ]"; + var errorMissing = " [MISSING]"; if (dependencyClaim.tag() == null) { msg.append(delimiter) .append(dependencyClaim.type()).append(" ") diff --git a/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/extension/KoraExtension.java b/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/extension/KoraExtension.java index a644d6b7a..178d4e61b 100644 --- a/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/extension/KoraExtension.java +++ b/core/kora-app-annotation-processor/src/main/java/io/koraframework/kora/app/annotation/processor/extension/KoraExtension.java @@ -35,21 +35,53 @@ static KoraExtensionDependencyGenerator generatedFromWithName(Elements elements, return () -> { var maybeGenerated = elements.getTypeElement(packageElement.getQualifiedName() + "." + name); if (maybeGenerated == null) { - throw new ProcessingErrorException("Class %s was expected to be generated from element by annotation processor but was not".formatted(packageElement.getQualifiedName() + "." + name), element); + throw new ProcessingErrorException(""" + Generated dependency class was not found: + expected type: %s + + Fix: + - Ensure the annotation processor that generates this type is enabled. + - Compile again after fixing earlier annotation processing errors. + """.formatted(packageElement.getQualifiedName() + "." + name).stripTrailing(), element); } if (!CommonUtils.hasAopAnnotations(maybeGenerated)) { var constructors = CommonUtils.findConstructors(maybeGenerated, m -> m.contains(Modifier.PUBLIC)); - if (constructors.size() != 1) throw new IllegalStateException(); + if (constructors.size() != 1) { + throw new ProcessingErrorException(""" + Generated dependency class must have exactly one public constructor: + generated type: %s + + Fix: + - Check the processor that generated this class. + - If this type is generated by Kora, report this as a Kora bug. + """.formatted(maybeGenerated.getQualifiedName()).stripTrailing(), maybeGenerated); + } return ExtensionResult.fromExecutable(constructors.get(0)); } var aopProxy = NameUtils.generatedType(maybeGenerated, "_AopProxy"); var aopProxyElement = elements.getTypeElement(packageElement.getQualifiedName() + "." + aopProxy); if (aopProxyElement == null) { // aop annotation processor will handle it - throw new ProcessingErrorException("Class %s was expected to be generated from element by aop annotation processor but was not".formatted(packageElement.getQualifiedName() + "." + name), maybeGenerated); + throw new ProcessingErrorException(""" + Generated AOP proxy class was not found: + expected type: %s + + Fix: + - Ensure the AOP annotation processor is enabled. + - Compile again after fixing earlier AOP processing errors. + """.formatted(packageElement.getQualifiedName() + "." + aopProxy).stripTrailing(), maybeGenerated); } var constructors = CommonUtils.findConstructors(aopProxyElement, m -> m.contains(Modifier.PUBLIC)); - if (constructors.size() != 1) throw new IllegalStateException(); + if (constructors.size() != 1) { + throw new ProcessingErrorException(""" + Generated AOP proxy class must have exactly one public constructor: + generated type: %s + + Fix: + - Check the AOP processor that generated this class. + - If this type is generated by Kora, report this as a Kora bug. + """.formatted(aopProxyElement.getQualifiedName()).stripTrailing(), aopProxyElement); + } return ExtensionResult.fromExecutable(constructors.get(0)); }; } diff --git a/core/kora-app-annotation-processor/src/test/java/io/koraframework/kora/app/annotation/processor/ComponentTemplatesTest.java b/core/kora-app-annotation-processor/src/test/java/io/koraframework/kora/app/annotation/processor/ComponentTemplatesTest.java index 5f8825a09..cc29d5b1c 100644 --- a/core/kora-app-annotation-processor/src/test/java/io/koraframework/kora/app/annotation/processor/ComponentTemplatesTest.java +++ b/core/kora-app-annotation-processor/src/test/java/io/koraframework/kora/app/annotation/processor/ComponentTemplatesTest.java @@ -44,7 +44,7 @@ class MyJsonWriter{} Assertions.fail("Should throw an exception"); } catch (CompileResult.CompilationFailedException e) { Assertions.assertThat(e) - .hasMessageContaining("Required dependency type wasn't found"); + .hasMessageContaining("No component found for dependency:"); } } diff --git a/core/kora-app-annotation-processor/src/test/java/io/koraframework/kora/app/annotation/processor/KoraAppProcessorTest.java b/core/kora-app-annotation-processor/src/test/java/io/koraframework/kora/app/annotation/processor/KoraAppProcessorTest.java index 7446f3c66..2436c4562 100644 --- a/core/kora-app-annotation-processor/src/test/java/io/koraframework/kora/app/annotation/processor/KoraAppProcessorTest.java +++ b/core/kora-app-annotation-processor/src/test/java/io/koraframework/kora/app/annotation/processor/KoraAppProcessorTest.java @@ -154,10 +154,11 @@ void appWithAllOf() throws Throwable { void unresolvedDependency() { assertThatThrownBy(() -> testClass(AppWithUnresolvedDependency.class)) .isInstanceOfSatisfying(CompilationErrorException.class, e -> SoftAssertions.assertSoftly(s -> { - s.assertThat(e.getMessage()).startsWith(""" - Required dependency type wasn't found in graph and can't be auto created: io.koraframework.kora.app.annotation.processor.app.AppWithUnresolvedDependency.Class3 (no tags) - Please check class for @Component annotation or that required module with declaration factory is plugged in. - Dependency requested at: io.koraframework.kora.app.annotation.processor.app.AppWithUnresolvedDependency#class2(io.koraframework.kora.app.annotation.processor.app.AppWithUnresolvedDependency.Class3)"""); + s.assertThat(e.getMessage()).contains("No component found for dependency:"); + s.assertThat(e.getMessage()).contains("io.koraframework.kora.app.annotation.processor.app.AppWithUnresolvedDependency.Class3 (no tags)"); + s.assertThat(e.getMessage()).contains("Required at:"); + s.assertThat(e.getMessage()).contains("Dependency resolution path:"); + s.assertThat(e.getMessage()).contains("Fix:"); s.assertThat(e.diagnostics.get(0).getLineNumber()).isEqualTo(14); s.assertThat(e.diagnostics.get(0).getSource().getName().replace('\\', '/')).isEqualTo("src/test/java/io/koraframework/kora/app/annotation/processor/app/AppWithUnresolvedDependency.java"); })); @@ -167,7 +168,9 @@ void unresolvedDependency() { void testCircularDependency() { assertThatThrownBy(() -> testClass(AppWithCircularDependency.class)) .isInstanceOfSatisfying(CompilationErrorException.class, e -> SoftAssertions.assertSoftly(s -> { - s.assertThat(e.getMessage()).startsWith("Encountered circular dependency in graph for source type:"); + s.assertThat(e.getMessage()).startsWith("Circular dependency found:"); + s.assertThat(e.getMessage()).contains("Dependency cycle:"); + s.assertThat(e.getMessage()).contains("Fix:"); s.assertThat(e.diagnostics.get(0).getSource().getName().replace('\\', '/')).isEqualTo("src/test/java/io/koraframework/kora/app/annotation/processor/app/AppWithCircularDependency.java"); })); } @@ -190,20 +193,18 @@ void appWithFactory() throws Throwable { // testClass(AppWithFactories5.class).init();; TODO больше не нужно assertThatThrownBy(() -> testClass(AppWithFactories6.class)) .isInstanceOf(CompilationErrorException.class) - .hasMessageStartingWith("Encountered circular dependency in graph for source type"); + .hasMessageStartingWith("Circular dependency found:"); testClass(AppWithFactories7.class).init(); testClass(AppWithFactories8.class).init(); assertThatThrownBy(() -> testClass(AppWithFactories10.class)) .isInstanceOf(CompilationErrorException.class) - .hasMessageStartingWith("Required dependency type wasn't found in graph and can't be auto created: java.io.Closeable") + .hasMessageStartingWith("No component found for dependency:") .asInstanceOf(type(CompilationErrorException.class)) .extracting(CompilationErrorException::getDiagnostics, list(Diagnostic.class)) .anySatisfy(d -> { assertThat(d.getKind()).isEqualTo(Diagnostic.Kind.ERROR); - assertThat(d.getMessage(Locale.ENGLISH)).startsWith(""" - Required dependency type wasn't found in graph and can't be auto created: java.io.Closeable (no tags) - Please check class for @Component annotation or that required module with declaration factory is plugged in. - """.trim()); + assertThat(d.getMessage(Locale.ENGLISH)).contains("No component found for dependency:"); + assertThat(d.getMessage(Locale.ENGLISH)).contains("java.io.Closeable (no tags)"); }); // assertThatThrownBy(() -> testClass(AppWithFactories11.class)) // .isInstanceOf(CompilationErrorException.class) @@ -247,8 +248,8 @@ void appWithComponentDescriptorCollisionAndDirect() { assertThatThrownBy(() -> testClass(AppWithComponentCollisionAndDirect.class)) .isInstanceOfSatisfying(CompilationErrorException.class, e -> SoftAssertions.assertSoftly(s -> { var error = e.getDiagnostics().stream().filter(d -> d.getKind() == Diagnostic.Kind.ERROR).findFirst().get(); - s.assertThat(error.getMessage(Locale.US)) - .startsWith("More than one declaration matches dependency type: io.koraframework.kora.app.annotation.processor.app.AppWithComponentCollisionAndDirect.Class1"); + s.assertThat(error.getMessage(Locale.US)).contains("Multiple components match dependency:"); + s.assertThat(error.getMessage(Locale.US)).contains("io.koraframework.kora.app.annotation.processor.app.AppWithComponentCollisionAndDirect.Class1"); })); } diff --git a/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/DependencyModuleHintProvider.kt b/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/DependencyModuleHintProvider.kt index adbc7b129..8ab75cfea 100644 --- a/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/DependencyModuleHintProvider.kt +++ b/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/DependencyModuleHintProvider.kt @@ -23,7 +23,7 @@ class DependencyModuleHintProvider { JsonFactoryBuilder().build().createParser(ObjectReadContext.empty(), r).use { parser -> hints = KoraHint.parseList(parser) } } } catch (e: IOException) { - throw RuntimeException(e) + throw IllegalStateException("Kora internal error: failed to read generated kora-hints.json resource", e) } } @@ -40,23 +40,21 @@ class DependencyModuleHintProvider { override fun message(): String { if (tag == null) { return """ - Missing component: ${type.toTypeName()} - Component is provided by standard Kora module you may forgot to plug it: - Gradle dependency: implementation("$artifact") - Module interface: $module + ${type.toTypeName()} is provided by a standard Kora module. + Gradle dependency: implementation("$artifact") + Module interface: $module """.trimIndent() } else { val tagForMsg = if (this.tag == "io.koraframework.json.common.annotation.Json") { "@io.koraframework.json.common.annotation.Json" } else { - "@Tag({$tag}::class)" + "@Tag($tag::class)" } return """ - Missing component: ${type.toTypeName()} with $tagForMsg - Component is provided by standard Kora module you may forgot to plug it: - Gradle dependency: implementation("$artifact") - Module interface: $module + ${type.toTypeName()} with $tagForMsg is provided by a standard Kora module. + Gradle dependency: implementation("$artifact") + Module interface: $module """.trimIndent() } } @@ -82,7 +80,7 @@ class DependencyModuleHintProvider { when (hint) { is KoraHint.KoraModuleHint -> result.add(Hint.ModuleHint(missingType, hint.tag, hint.artifact, hint.moduleName)) is KoraHint.KoraTipHint -> result.add(Hint.TipHint(missingType, hint.tag, hint.tip)) - else -> throw UnsupportedOperationException("Unknown hint type: $hint") + else -> throw IllegalStateException("Kora internal error: unknown dependency hint type: $hint") } } else { logger.trace("Hint {} doesn't match because of tag", hint) @@ -225,7 +223,7 @@ class DependencyModuleHintProvider { val finalTag = when { tag != null -> tag tags.size == 1 -> tags.first() - tags.isNotEmpty() -> throw RuntimeException("Tags size should be 0 or 1") + tags.isNotEmpty() -> throw IllegalStateException("Kora internal error: dependency hint declares more than one tag: $tags") else -> null } return if (tip != null) { diff --git a/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/GraphBuilder.kt b/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/GraphBuilder.kt index f4f393734..883f72af4 100644 --- a/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/GraphBuilder.kt +++ b/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/GraphBuilder.kt @@ -94,7 +94,13 @@ class GraphBuilder { fun build(): ResolvedGraph { if (rootSet.isEmpty()) { throw ProcessingErrorException( - "@KoraApp has no root components, expected at least one component annotated with @Root", + """ + @KoraApp has no root components. + + Fix: + - Annotate at least one component or module function with @Root. + - Check that root component is visible from this @KoraApp module set. + """.trimIndent(), root ) } @@ -123,11 +129,37 @@ class GraphBuilder { val conditionDeclarations = componentDeclarations.getByType(CommonClassNames.graphCondition) .filter { componentCondition.canonicalName == it.declaration.tag } if (conditionDeclarations.isEmpty()) { - throw ProcessingErrorException("Component declares condition with tag ${componentCondition}, but none found in graph: $declaration", declaration.source) + throw ProcessingErrorException( + """ + Component condition cannot be resolved: + required condition tag: @Tag(${componentCondition}::class) + component: $declaration + + Fix: + - Add a GraphCondition component with this tag. + - Include a module that provides this GraphCondition. + - Check that @Conditional uses the intended tag. + """.trimIndent(), + declaration.source + ) } if (conditionDeclarations.size > 1) { val str = conditionDeclarations.joinToString("\n") { it.declaration.toString() }.prependIndent(" ") - throw ProcessingErrorException("Component declares condition with tag $componentCondition, but multiple candidates found in graph:\n$declaration\n$str", declaration.source) + throw ProcessingErrorException( + """ + Multiple GraphCondition components match condition tag: + required condition tag: @Tag(${componentCondition}::class) + component: $declaration + + Candidates: + $str + + Fix: + - Keep only one GraphCondition for this tag. + - Use different @Tag(...) values for different conditions. + """.trimIndent(), + declaration.source + ) } val conditionDeclaration = conditionDeclarations.first() val resolvedCondition = this.resolvedComponents.getByDeclaration(conditionDeclaration) @@ -244,7 +276,7 @@ class GraphBuilder { val idx = componentDeclarations.add(optionalDeclaration) stack.addLast(frame.copy(currentDependency = currentDependency)) val type = dependencyClaim.type.arguments[0].type!!.resolve().makeNullable() - val claim = ComponentDependencyHelper.parseClaim(type, dependencyClaim.tag, declaration.source) + val claim = ComponentDependencyHelper.parseClaim(type, dependencyClaim.tag, dependencyClaim.source ?: declaration.source) stack.addLast( ResolutionFrame.Component( optionalDeclaration, @@ -295,7 +327,15 @@ class GraphBuilder { for (resolvedDependency in dependencies) { if (resolvedDependency.component!!.index > component.index) { throw ProcessingErrorException( - "All dependency appeared in graph after component requesting it, this is a bug that we will fix later", + """ + All dependency appeared in graph after the component that requests it. + + This is an internal graph ordering limitation. + + Fix: + - Move the component that provides the All item so it is reachable before the requesting component. + - If this graph should be valid, please report this as a Kora bug with the dependency path. + """.trimIndent(), resolvedDependency.component!!.declaration.source ) } @@ -305,9 +345,10 @@ class GraphBuilder { if (dependency is ComponentDependency.PromisedProxyParameterDependency) { val componentDeclarations = GraphResolutionHelper.findDependencyDeclarations(ctx, componentDeclarations, dependency.claim) if (componentDeclarations.size != 1) { - throw IllegalStateException(); + throw IllegalStateException("Kora internal error: promised proxy dependency expected exactly one target declaration, got ${componentDeclarations.size} for ${dependency.claim}") } - val realDependency = resolvedComponents.getByDeclaration(componentDeclarations.first())!! + val realDependency = resolvedComponents.getByDeclaration(componentDeclarations.first()) + ?: throw IllegalStateException("Kora internal error: promised proxy declaration was found but is not resolved: ${componentDeclarations.first().declaration.declarationString()}") dependency.realDependency = realDependency } } @@ -355,7 +396,7 @@ class GraphBuilder { if (dependencyClaim.claimType == ALL || dependencyClaim.claimType == ALL_OF_VALUE || dependencyClaim.claimType == ALL_OF_PROMISE) { return ComponentDependency.AllOfDependency(dependencyClaim) } - throw IllegalStateException() + throw IllegalStateException("Kora internal error: processAllOf called for non-All dependency claim: $dependencyClaim") } private fun findInterceptors(declaration: ComponentDeclaration): Sequence { @@ -508,14 +549,18 @@ class GraphBuilder { if (claimTypeDeclaration !is KSClassDeclaration) throw circularDependencyException if (claimTypeDeclaration.classKind != ClassKind.INTERFACE && !(claimTypeDeclaration.classKind == ClassKind.CLASS && claimTypeDeclaration.isOpen())) throw circularDependencyException val proxyDependencyClaim = DependencyClaim( - dependencyClaim.type, CommonClassNames.promisedProxy.canonicalName, dependencyClaim.claimType + dependencyClaim.type, CommonClassNames.promisedProxy.canonicalName, dependencyClaim.claimType, dependencyClaim.source ) val declarations = GraphResolutionHelper.findDependencyDeclarations(ctx, componentDeclarations, proxyDependencyClaim); if (declarations.isNotEmpty()) { - check(declarations.size == 1) + check(declarations.size == 1) { + "Kora internal error: promised proxy declaration is ambiguous for $proxyDependencyClaim, declarations: $declarations" + } val decl = declarations.first() val resolved = resolvedComponents.getByDeclaration(decl) - checkNotNull(resolved) + checkNotNull(resolved) { + "Kora internal error: promised proxy declaration was found but is not resolved: ${decl.declaration.declarationString()}" + } stack.removeLast() prevFrame.resolvedDependencies.add(GraphResolutionHelper.toDependency(ctx, resolved, dependencyClaim)) stack.addLast(prevFrame.copy(currentDependency = prevFrame.currentDependency + 1)) @@ -533,7 +578,9 @@ class GraphBuilder { proxyComponentDeclarations = findDependencyDeclarationsFromTemplate( ctx, declaration, templateDeclarations, proxyDependencyClaim ) - check(proxyComponentDeclarations.size == 1) + check(proxyComponentDeclarations.size == 1) { + "Kora internal error: generated promised proxy template expected one declaration, got ${proxyComponentDeclarations.size} for $proxyDependencyClaim" + } proxyComponentDeclaration = proxyComponentDeclarations.first() declIdx = this.componentDeclarations.add(proxyComponentDeclaration) } else { @@ -541,7 +588,9 @@ class GraphBuilder { declIdx = this.componentDeclarations.add(generatedDeclaration) } } else { - check(proxyComponentDeclarations.size == 1) + check(proxyComponentDeclarations.size == 1) { + "Kora internal error: promised proxy template is ambiguous for $proxyDependencyClaim, declarations: $proxyComponentDeclarations" + } proxyComponentDeclaration = proxyComponentDeclarations.first() declIdx = this.componentDeclarations.add(proxyComponentDeclaration) } @@ -551,7 +600,8 @@ class GraphBuilder { declaration, DependencyClaim( declaration.type, declaration.tag, - ONE_REQUIRED + ONE_REQUIRED, + declaration.source ) ) ) diff --git a/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/GraphFileGenerator.kt b/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/GraphFileGenerator.kt index 1cacbe927..97b99765b 100644 --- a/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/GraphFileGenerator.kt +++ b/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/GraphFileGenerator.kt @@ -302,7 +302,7 @@ class GraphFileGenerator( is ComponentDependency.TargetDependency -> result.add(singleDependency.component) is ComponentDependency.ValueOfDependency -> result.add(singleDependency.component) is ComponentDependency.WrappedTargetDependency -> result.add(singleDependency.component) - is ComponentDependency.NullDependency -> throw IllegalStateException() + is ComponentDependency.NullDependency -> throw IllegalStateException("Kora internal error: OneOf dependency unexpectedly contains a NullDependency while collecting initialization dependencies: ${dependency.claim}") } } @@ -364,7 +364,7 @@ class GraphFileGenerator( is ComponentDependency.TargetDependency -> result.add(singleDependency.component) is ComponentDependency.ValueOfDependency -> result.add(singleDependency.component) is ComponentDependency.WrappedTargetDependency -> result.add(singleDependency.component) - else -> throw IllegalStateException() + else -> throw IllegalStateException("Kora internal error: OneOf dependency unexpectedly contains unsupported dependency while collecting refresh dependencies: $singleDependency") } } } @@ -401,4 +401,3 @@ class GraphFileGenerator( } } - diff --git a/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/GraphResolutionHelper.kt b/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/GraphResolutionHelper.kt index 89193bc14..04ba6d5a2 100644 --- a/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/GraphResolutionHelper.kt +++ b/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/GraphResolutionHelper.kt @@ -43,7 +43,9 @@ object GraphResolutionHelper { fun toDependency(ctx: ProcessingContext, resolvedComponent: ResolvedComponent, dependencyClaim: DependencyClaim): SingleDependency { val isDirectAssignable = dependencyClaim.type.isAssignableFrom(resolvedComponent.type) val isWrappedAssignable = ctx.serviceTypesHelper.isAssignableToUnwrapped(resolvedComponent.type, dependencyClaim.type) - check(isDirectAssignable || isWrappedAssignable) + check(isDirectAssignable || isWrappedAssignable) { + "Kora internal error: resolved component is not assignable to dependency claim. Component=${resolvedComponent.declaration.declarationString()}, claim=$dependencyClaim" + } val targetDependency = if (isWrappedAssignable) WrappedTargetDependency(dependencyClaim, resolvedComponent) @@ -54,7 +56,7 @@ object GraphResolutionHelper { ONE_REQUIRED, NULLABLE_ONE, NODE_OF -> targetDependency PROMISE_OF, NULLABLE_PROMISE_OF -> PromiseOfDependency(dependencyClaim, targetDependency) VALUE_OF, NULLABLE_VALUE_OF -> ValueOfDependency(dependencyClaim, targetDependency) - ALL, ALL_OF_PROMISE, ALL_OF_VALUE, TYPE_REF, GRAPH -> throw java.lang.IllegalStateException() + ALL, ALL_OF_PROMISE, ALL_OF_VALUE, TYPE_REF, GRAPH -> throw IllegalStateException("Kora internal error: unsupported single dependency claim type ${dependencyClaim.claimType} for $dependencyClaim") } } @@ -194,7 +196,7 @@ object GraphResolutionHelper { ) } - is ComponentDeclaration.OptionalComponent -> throw IllegalStateException() + is ComponentDeclaration.OptionalComponent -> throw IllegalStateException("Kora internal error: optional synthetic component cannot be used as a component template: $template") } } if (result.isEmpty()) { @@ -240,8 +242,7 @@ object GraphResolutionHelper { // that's fine, default component wasn't directly requested by anyone, so we don't need it continue } else { - // something went wrong - throw NullPointerException() + throw IllegalStateException("Kora internal error: non-default All dependency declaration is not resolved: ${declaration.declarationString()}") } } @@ -251,7 +252,7 @@ object GraphResolutionHelper { ALL -> targetDependency ALL_OF_PROMISE -> PromiseOfDependency(dependencyClaim, targetDependency) ALL_OF_VALUE -> ValueOfDependency(dependencyClaim, targetDependency) - else -> throw IllegalStateException("Unexpected value: " + dependencyClaim.claimType) + else -> throw IllegalStateException("Kora internal error: unsupported All claim type ${dependencyClaim.claimType} for $dependencyClaim") } result.add(dependency) } @@ -262,7 +263,7 @@ object GraphResolutionHelper { ALL -> targetDependency ALL_OF_PROMISE -> PromiseOfDependency(dependencyClaim, targetDependency) ALL_OF_VALUE -> ValueOfDependency(dependencyClaim, targetDependency) - else -> throw IllegalStateException("Unexpected value: " + dependencyClaim.claimType) + else -> throw IllegalStateException("Kora internal error: unsupported wrapped All claim type ${dependencyClaim.claimType} for $dependencyClaim") } result.add(dependency) } diff --git a/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/KoraAppProcessor.kt b/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/KoraAppProcessor.kt index 77bb2c04e..0947ed75f 100644 --- a/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/KoraAppProcessor.kt +++ b/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/KoraAppProcessor.kt @@ -91,7 +91,16 @@ class KoraAppProcessor( deferred.add(declaration) } } else { - kspLogger.error("@KoraApp can be placed only on interfaces", declaration) + kspLogger.error( + """ + @KoraApp can only be applied to interfaces. + + Fix: + - Change this type to an interface. + - Move @KoraApp to an interface that declares root components and modules. + """.trimIndent(), + declaration + ) } } hasDeferred = deferred.isNotEmpty() @@ -100,7 +109,16 @@ class KoraAppProcessor( private fun buildGraph(ctx: ProcessingContext, declaration: KSClassDeclaration): ResolvedGraph { if (declaration.classKind != ClassKind.INTERFACE) { - throw ProcessingErrorException("@KoraApp is only applicable to interfaces", declaration) + throw ProcessingErrorException( + """ + @KoraApp can only be applied to interfaces. + + Fix: + - Change this type to an interface. + - Move @KoraApp to an interface that declares root components and modules. + """.trimIndent(), + declaration + ) } val rootErasure = declaration.asStarProjectedType() val rootModule = ModuleDeclaration.MixedInModule(declaration) @@ -136,7 +154,16 @@ class KoraAppProcessor( if (func.isAnnotationPresent(CommonClassNames.factoryModule)) { val returnTypeDecl = func.returnType?.resolve()?.declaration if (returnTypeDecl !is KSClassDeclaration) { - throw ProcessingErrorException("Factory module function must return a class", func) + throw ProcessingErrorException( + """ + @FactoryModule function must return a class or interface type. + + Fix: + - Change the return type to a module class/interface. + - Remove @FactoryModule if this function is a regular provider. + """.trimIndent(), + func + ) } val methodTag = TagUtils.parseTagValue(func) val methodModule = ModuleDeclaration.FactoryModule(returnTypeDecl, methodTag) @@ -218,7 +245,17 @@ class KoraAppProcessor( } classDeclarationByName } else { - resolver.getClassDeclarationByName(ksName) ?: throw ProcessingErrorException("Declaration of ${ksName.asString()} wasn't found", it) + resolver.getClassDeclarationByName(ksName) ?: throw ProcessingErrorException( + """ + Module declaration cannot be resolved: + type: ${ksName.asString()} + + Fix: + - Check imports and module dependencies. + - Compile again after fixing earlier compiler errors. + """.trimIndent(), + it + ) } } .toList() @@ -244,7 +281,16 @@ class KoraAppProcessor( // } // } } else { - kspLogger.error("Only interfaces are allowed as modules", module) + kspLogger.error( + """ + @Module can only be applied to interfaces. + + Fix: + - Change this type to an interface. + - Move module factory methods to an interface annotated with @Module. + """.trimIndent(), + module + ) } } } diff --git a/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/KoraAppUtils.kt b/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/KoraAppUtils.kt index ee1b50336..f4db024f6 100644 --- a/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/KoraAppUtils.kt +++ b/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/KoraAppUtils.kt @@ -22,12 +22,24 @@ object KoraAppUtils { .toList() if (constructors.isEmpty()) { throw ProcessingErrorException( - "Type annotated with @Component has no public constructors", this + """ + @Component type has no public constructors. + + Fix: + - Add one public constructor. + - Move construction to a module function if constructor cannot be public. + """.trimIndent(), this ) } if (constructors.size > 1) { throw ProcessingErrorException( - "Type annotated with @Component has more then one public constructor", this + """ + @Component type has more than one public constructor. + + Fix: + - Keep exactly one public constructor. + - Make extra constructors non-public. + """.trimIndent(), this ) } return constructors[0] diff --git a/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/ServiceTypesHelper.kt b/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/ServiceTypesHelper.kt index 5cd9f09e7..a1490a6d7 100644 --- a/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/ServiceTypesHelper.kt +++ b/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/ServiceTypesHelper.kt @@ -95,7 +95,7 @@ class ServiceTypesHelper(val resolver: Resolver) { fun interceptType(maybeInterceptor: KSType): KSType { if (!interceptorType.isAssignableFrom(maybeInterceptor)) { - throw IllegalArgumentException() + throw IllegalStateException("Kora internal error: interceptType called for non-interceptor type: ${maybeInterceptor.declaration.qualifiedName?.asString()}") } return if (maybeInterceptor.declaration is KSClassDeclaration) { @@ -103,7 +103,7 @@ class ServiceTypesHelper(val resolver: Resolver) { .filter { f -> f.simpleName.asString() == "afterInit" && f.parameters.size == 1 && f.returnType != null && !f.returnType!!.isVoid() } .filter { f -> f.parameters.first().type.toTypeName() == f.returnType!!.toTypeName() } .map { it.returnType!!.resolve() } - .firstOrNull() ?: throw IllegalArgumentException() + .firstOrNull() ?: throw IllegalStateException("Kora internal error: interceptor type has no valid afterInit(T): T method: ${maybeInterceptor.declaration.qualifiedName?.asString()}") } else { val memberOf = interceptorInitFunction.asMemberOf(maybeInterceptor) return memberOf.parameterTypes[0]!!.makeNotNullable() diff --git a/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/component/ComponentDependency.kt b/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/component/ComponentDependency.kt index dea174e7d..69557ebe7 100644 --- a/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/component/ComponentDependency.kt +++ b/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/component/ComponentDependency.kt @@ -23,7 +23,7 @@ sealed interface ComponentDependency { DependencyClaim.DependencyClaimType.ALL -> codeBlock.add("%T.all(it", CommonClassNames.all) DependencyClaim.DependencyClaimType.ALL_OF_VALUE -> codeBlock.add("%T.allValues(it", CommonClassNames.all) DependencyClaim.DependencyClaimType.ALL_OF_PROMISE -> codeBlock.add("%T.allPromises(it", CommonClassNames.all) - else -> throw IllegalStateException("Unexpected dependency type ${claim.claimType}") + else -> throw IllegalStateException("Kora internal error: unsupported All claim type for code generation: $claim") } for (dependency in resolvedDependencies) { val dependencyNode = dependency.component!!.nodeRef("some_fake_holder_idc") @@ -41,7 +41,7 @@ sealed interface ComponentDependency { DependencyClaim.DependencyClaimType.NULLABLE_ONE -> CodeBlock.of("null as %T", claim.type.toTypeName().copy(true)) DependencyClaim.DependencyClaimType.NULLABLE_VALUE_OF -> CodeBlock.of("null as %T", CommonClassNames.valueOf.parameterizedBy(claim.type.toTypeName()).copy(true)) DependencyClaim.DependencyClaimType.NULLABLE_PROMISE_OF -> CodeBlock.of("null as %T", CommonClassNames.promiseOf.parameterizedBy(claim.type.toTypeName()).copy(true)) - else -> throw IllegalArgumentException(claim.claimType.toString()) + else -> throw IllegalStateException("Kora internal error: unsupported nullable dependency claim type for code generation: $claim") } } @@ -99,7 +99,7 @@ sealed interface ComponentDependency { DependencyClaim.DependencyClaimType.ONE_REQUIRED -> b.add("it.getOneOf(") DependencyClaim.DependencyClaimType.VALUE_OF -> b.add("it.getOneValueOf(") DependencyClaim.DependencyClaimType.PROMISE_OF -> b.add("it.getOnePromiseOf(") - else -> throw IllegalStateException("Unknown claim type: " + claim.claimType) + else -> throw IllegalStateException("Kora internal error: unsupported one-of dependency claim type for code generation: $claim") } for ((i, dependency) in dependencies.withIndex()) { if (i > 0) b.add(", ") diff --git a/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/component/ComponentDependencyHelper.kt b/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/component/ComponentDependencyHelper.kt index 29165c019..ab56853f5 100644 --- a/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/component/ComponentDependencyHelper.kt +++ b/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/component/ComponentDependencyHelper.kt @@ -24,7 +24,8 @@ object ComponentDependencyHelper { DependencyClaim( declaration.module.element.asType(listOf()), declaration.module.tag, - DependencyClaim.DependencyClaimType.ONE_REQUIRED + DependencyClaim.DependencyClaimType.ONE_REQUIRED, + element ) ) } @@ -33,7 +34,8 @@ object ComponentDependencyHelper { DependencyClaim( declaration.module.element.asType(listOf()), null, - DependencyClaim.DependencyClaimType.ONE_REQUIRED + DependencyClaim.DependencyClaimType.ONE_REQUIRED, + element ) ) } @@ -45,7 +47,16 @@ object ComponentDependencyHelper { if (declaration.module is ModuleDeclaration.FactoryModule) { tag = declaration.module.tag } else { - throw ProcessingErrorException("Tag @Tag.Factory is only allowed for factory modules ", declaration.method) + throw ProcessingErrorException( + """ + @Tag.Factory can only be used inside factory modules. + + Fix: + - Move this provider to a factory module. + - Replace @Tag.Factory with an explicit @Tag(...) value. + """.trimIndent(), + declaration.method + ) } } result.add(parseClaim(parameterType, tag, declaration.method.parameters[i])) @@ -73,8 +84,10 @@ object ComponentDependencyHelper { } - is ComponentDeclaration.OptionalComponent -> throw IllegalArgumentException() - is ComponentDeclaration.PromisedProxyComponent -> throw IllegalArgumentException() + is ComponentDeclaration.OptionalComponent -> + throw IllegalStateException("Kora internal error: optional synthetic component cannot declare dependencies: $declaration") + is ComponentDeclaration.PromisedProxyComponent -> + throw IllegalStateException("Kora internal error: promised proxy synthetic component cannot declare dependencies: $declaration") } } @@ -83,7 +96,14 @@ object ComponentDependencyHelper { if (parameterType.isError) { throw ProcessingErrorException( ProcessingError( - "Dependency type parameter is not resolvable in the current round of processing: $element\nTry disabling Kora KSP 'symbol-processors' dependency and compile without it to check for errors in your codebase (Kotlin and KSP compiler work only this way)", + """ + Dependency type cannot be resolved in the current KSP round: + element: $element + + Fix: + - Check imports and module dependencies. + - Compile without Kora symbol processors to expose earlier Kotlin errors if KSP hides them. + """.trimIndent(), element, Diagnostic.Kind.WARNING ) @@ -94,58 +114,75 @@ object ComponentDependencyHelper { } catch (e: IllegalArgumentException) { throw ProcessingErrorException( ProcessingError( - "Dependency type parameter is not resolvable in the current round of processing: $element\nTry disabling Kora KSP 'symbol-processors' dependency and compile without it to check for errors in your codebase (Kotlin and KSP compiler work only this way)", + """ + Dependency type cannot be converted to a KotlinPoet type in the current KSP round: + element: $element + + Fix: + - Check imports and module dependencies. + - Compile without Kora symbol processors to expose earlier Kotlin errors if KSP hides them. + """.trimIndent(), element, Diagnostic.Kind.WARNING ) ) } if (typeName == CommonClassNames.graph || typeName == CommonClassNames.refreshableGraph) { - return DependencyClaim(parameterType, tag, DependencyClaim.DependencyClaimType.GRAPH) + return DependencyClaim(parameterType, tag, DependencyClaim.DependencyClaimType.GRAPH, element) } if (typeName is ParameterizedTypeName) { val firstTypeParam = parameterType.arguments[0].type!!.resolve() if (typeName.rawType == CommonClassNames.typeRef) { - return DependencyClaim(firstTypeParam, tag, DependencyClaim.DependencyClaimType.TYPE_REF) + return DependencyClaim(firstTypeParam, tag, DependencyClaim.DependencyClaimType.TYPE_REF, element) } if (typeName.rawType.canonicalName == CommonClassNames.node.canonicalName) { if (firstTypeParam.isMarkedNullable) { - throw ProcessingErrorException("Nullable arguments to node are not allowed", element) + throw ProcessingErrorException( + """ + Invalid Node dependency argument: + Node cannot use a nullable T. + + Fix: + - Use a non-nullable Node. + - Inject nullable dependency directly if nullable access is required. + """.trimIndent(), + element + ) } - return DependencyClaim(firstTypeParam, tag, DependencyClaim.DependencyClaimType.NODE_OF); + return DependencyClaim(firstTypeParam, tag, DependencyClaim.DependencyClaimType.NODE_OF, element); } if (typeName.rawType == CommonClassNames.all) { val allOf = typeName.typeArguments[0] if (allOf is ParameterizedTypeName) { if (allOf.rawType == CommonClassNames.valueOf) { - return DependencyClaim(firstTypeParam.arguments[0].type!!.resolve(), tag, DependencyClaim.DependencyClaimType.ALL_OF_VALUE) + return DependencyClaim(firstTypeParam.arguments[0].type!!.resolve(), tag, DependencyClaim.DependencyClaimType.ALL_OF_VALUE, element) } if (allOf.rawType == CommonClassNames.promiseOf) { - return DependencyClaim(firstTypeParam.arguments[0].type!!.resolve(), tag, DependencyClaim.DependencyClaimType.ALL_OF_PROMISE) + return DependencyClaim(firstTypeParam.arguments[0].type!!.resolve(), tag, DependencyClaim.DependencyClaimType.ALL_OF_PROMISE, element) } } - return DependencyClaim(firstTypeParam, tag, DependencyClaim.DependencyClaimType.ALL) + return DependencyClaim(firstTypeParam, tag, DependencyClaim.DependencyClaimType.ALL, element) } if (typeName.rawType == CommonClassNames.valueOf) { if (parameterType.isMarkedNullable || element.isAnnotationPresent(CommonClassNames.nullable)) { - return DependencyClaim(firstTypeParam, tag, DependencyClaim.DependencyClaimType.NULLABLE_VALUE_OF) + return DependencyClaim(firstTypeParam, tag, DependencyClaim.DependencyClaimType.NULLABLE_VALUE_OF, element) } else { - return DependencyClaim(firstTypeParam, tag, DependencyClaim.DependencyClaimType.VALUE_OF) + return DependencyClaim(firstTypeParam, tag, DependencyClaim.DependencyClaimType.VALUE_OF, element) } } if (typeName.rawType == CommonClassNames.promiseOf) { if (parameterType.isMarkedNullable || element.isAnnotationPresent(CommonClassNames.nullable)) { - return DependencyClaim(firstTypeParam, tag, DependencyClaim.DependencyClaimType.NULLABLE_PROMISE_OF) + return DependencyClaim(firstTypeParam, tag, DependencyClaim.DependencyClaimType.NULLABLE_PROMISE_OF, element) } else { - return DependencyClaim(firstTypeParam, tag, DependencyClaim.DependencyClaimType.PROMISE_OF) + return DependencyClaim(firstTypeParam, tag, DependencyClaim.DependencyClaimType.PROMISE_OF, element) } } } if (parameterType.isMarkedNullable || element.isAnnotationPresent(CommonClassNames.nullable)) { - return DependencyClaim(parameterType, tag, DependencyClaim.DependencyClaimType.NULLABLE_ONE) + return DependencyClaim(parameterType, tag, DependencyClaim.DependencyClaimType.NULLABLE_ONE, element) } else { - return DependencyClaim(parameterType, tag, DependencyClaim.DependencyClaimType.ONE_REQUIRED) + return DependencyClaim(parameterType, tag, DependencyClaim.DependencyClaimType.ONE_REQUIRED, element) } } } diff --git a/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/component/DependencyClaim.kt b/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/component/DependencyClaim.kt index 539970172..e3ccf4e7a 100644 --- a/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/component/DependencyClaim.kt +++ b/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/component/DependencyClaim.kt @@ -1,10 +1,11 @@ package io.koraframework.kora.app.ksp.component +import com.google.devtools.ksp.symbol.KSAnnotated import com.google.devtools.ksp.symbol.KSType import com.squareup.kotlinpoet.ksp.toTypeName import io.koraframework.ksp.common.TagUtils.tagMatches -data class DependencyClaim(val type: KSType, val tag: String?, val claimType: DependencyClaimType) { +data class DependencyClaim(val type: KSType, val tag: String?, val claimType: DependencyClaimType, val source: KSAnnotated? = null) { enum class DependencyClaimType { ONE_REQUIRED, diff --git a/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/declaration/ComponentDeclaration.kt b/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/declaration/ComponentDeclaration.kt index fed094623..1d2e85261 100644 --- a/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/declaration/ComponentDeclaration.kt +++ b/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/declaration/ComponentDeclaration.kt @@ -152,7 +152,14 @@ sealed interface ComponentDeclaration { val type = method.returnType!!.resolve().fixPlatformType(ctx.resolver) if (type.isError) { throw ProcessingErrorException( - "Component type is not resolvable in the current round of processing: func $method()\nTry disabling Kora KSP 'symbol-processors' dependency and compile without it to check for errors in your codebase (Kotlin and KSP compiler work only this way)", + """ + Component return type cannot be resolved in the current KSP round: + function: $method + + Fix: + - Check imports and module dependencies. + - Compile without Kora symbol processors to expose earlier Kotlin errors if KSP hides them. + """.trimIndent(), method ) } @@ -161,7 +168,16 @@ sealed interface ComponentDeclaration { if (module is ModuleDeclaration.FactoryModule) { tag = module.tag } else { - throw ProcessingErrorException("Tag @Tag.Factory is only allowed for factory modules ", method) + throw ProcessingErrorException( + """ + @Tag.Factory can only be used inside factory modules. + + Fix: + - Move this provider to a factory module. + - Replace @Tag.Factory with an explicit @Tag(...) value. + """.trimIndent(), + method + ) } } @@ -184,7 +200,16 @@ sealed interface ComponentDeclaration { fun fromAnnotated(ctx: ProcessingContext, classDeclaration: KSClassDeclaration): AnnotatedComponent { val constructor = classDeclaration.primaryConstructor if (constructor == null) { - throw ProcessingErrorException("@Component annotated class should have primary constructor", classDeclaration) + throw ProcessingErrorException( + """ + @Component class must have a primary constructor. + + Fix: + - Add a primary constructor. + - Move construction to a module function if primary constructor is not possible. + """.trimIndent(), + classDeclaration + ) } val typeParameters = classDeclaration.typeParameters.map { val t = it.bounds.firstOrNull()?.resolve() ?: ctx.resolver.builtIns.anyType @@ -215,7 +240,14 @@ sealed interface ComponentDeclaration { val type = sourceType.returnType!! if (type.isError) { throw ProcessingErrorException( - "Component type is not resolvable in the current round of processing: func $sourceType()\nTry disabling Kora KSP 'symbol-processors' dependency and compile without it to check for errors in your codebase (Kotlin and KSP compiler work only this way)", + """ + Extension component return type cannot be resolved in the current KSP round: + function type: $sourceType + + Fix: + - Check imports and module dependencies. + - Compile without Kora symbol processors to expose earlier Kotlin errors if KSP hides them. + """.trimIndent(), sourceMethod ) } diff --git a/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/exception/CircularDependencyException.kt b/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/exception/CircularDependencyException.kt index 33d6d1991..41fbd2e9a 100644 --- a/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/exception/CircularDependencyException.kt +++ b/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/exception/CircularDependencyException.kt @@ -18,23 +18,21 @@ data class CircularDependencyException( cycle: List, declaration: ComponentDeclaration ): ProcessingError { - val deps = cycle.joinToString("\n", "Cycle dependency candidates:\n", "") { String.format("- %s", it.declarationString()) }.prependIndent(" ") + val deps = cycle.joinToString("\n ^--- ", "Dependency cycle:\n @--- ") { it.declarationString() }.prependIndent(" ") + val msg = StringBuilder() + msg.append("Circular dependency found:\n ").append(declaration.type.toTypeName()) if (declaration.tag == null) { - return ProcessingError( - """Encountered circular dependency in graph for source type: ${declaration.type.toTypeName()} (no tags) - $deps - Please check that you are not using cycle dependency in ${CommonClassNames.lifecycle}, this is forbidden.""".trimIndent(), - declaration.source - ) + msg.append(" (no tags)") } else { - return ProcessingError( - """Encountered circular dependency in graph for source type: ${declaration.type.toTypeName()} with @Tag(${declaration.tag}::class) - $deps - Please check that you are not using cycle dependency in ${CommonClassNames.lifecycle}, this is forbidden.""".trimIndent(), - declaration.source - ) + msg.append(" with @Tag(${declaration.tag}::class)") } + msg.append("\n\n").append(deps.trimEnd()).append(" [CYCLE]") + msg.append("\n\nFix:") + msg.append("\n - Break the cycle with ValueOf or PromiseOf where lazy access is valid.") + msg.append("\n - Move shared state into a separate component.") + msg.append("\n - Do not create dependency cycles in ${CommonClassNames.lifecycle}.") + return ProcessingError(msg.toString(), declaration.source) } } } diff --git a/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/exception/DuplicateDependencyException.kt b/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/exception/DuplicateDependencyException.kt index 6d3c55577..1a12a0c8e 100644 --- a/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/exception/DuplicateDependencyException.kt +++ b/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/exception/DuplicateDependencyException.kt @@ -1,6 +1,7 @@ package io.koraframework.kora.app.ksp.exception import com.squareup.kotlinpoet.ksp.toTypeName +import com.google.devtools.ksp.symbol.KSValueParameter import io.koraframework.kora.app.ksp.component.ComponentDependency import io.koraframework.kora.app.ksp.component.DependencyClaim import io.koraframework.kora.app.ksp.declaration.ComponentDeclaration @@ -31,7 +32,7 @@ data class DuplicateDependencyException( ): ProcessingError { val deps = foundDeclarations .map { String.format("- %s", it.declarationString()) } - .joinToString("\n", "Candidates for injection:\n", "").prependIndent(" ") + .joinToString("\n", "Candidates:\n", "").prependIndent(" ") return getError(claim, declaration, deps) } @@ -41,21 +42,28 @@ data class DuplicateDependencyException( declaration: ComponentDeclaration, deps: String ): ProcessingError { + val msg = StringBuilder() + msg.append("Multiple components match dependency:\n ").append(claim.type.toTypeName()) if (claim.tag == null) { - return ProcessingError( - """More than one component matches dependency type: ${claim.type.toTypeName()} (no tags) - $deps - Please check that injection dependency is declared correctly or that @DefaultComponent annotation is not missing if was intended.""".trimIndent(), - declaration.source - ) + msg.append(" (no tags)") } else { - return ProcessingError( - """More than one component matches dependency type: ${claim.type.toTypeName()} with @Tag(${claim.tag}::class) - $deps - Please check that injection dependency is declared correctly or that @DefaultComponent annotation is not missing if was intended.""".trimIndent(), - declaration.source - ) + msg.append(" with @Tag(${claim.tag}::class)") } + val source = claim.source + if (source is KSValueParameter) { + msg.append("\n\nRequired at:\n ") + .append(source.parent) + .append("\n parameter: ") + .append(source.type.toTypeName()) + .append(" ") + .append(source.name?.asString() ?: "") + } + msg.append("\n\n").append(deps.trimEnd()) + msg.append("\n\nFix:") + msg.append("\n - Add different @Tag(...) annotations to candidates and request the needed tag.") + msg.append("\n - Mark fallback candidate with @DefaultComponent.") + msg.append("\n - Remove one duplicate provider.") + return ProcessingError(msg.toString(), claim.source ?: declaration.source) } } } diff --git a/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/exception/UnresolvedDependencyException.kt b/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/exception/UnresolvedDependencyException.kt index 14a3d99e8..7561d81b8 100644 --- a/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/exception/UnresolvedDependencyException.kt +++ b/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/exception/UnresolvedDependencyException.kt @@ -4,6 +4,7 @@ import com.google.devtools.ksp.isConstructor import com.google.devtools.ksp.symbol.KSClassDeclaration import com.google.devtools.ksp.symbol.KSDeclaration import com.google.devtools.ksp.symbol.KSFunctionDeclaration +import com.google.devtools.ksp.symbol.KSValueParameter import com.squareup.kotlinpoet.ksp.toTypeName import io.koraframework.kora.app.ksp.DependencyModuleHintProvider import io.koraframework.kora.app.ksp.GraphBuilder @@ -20,7 +21,7 @@ class UnresolvedDependencyException( val declaration: ComponentDeclaration, val dependencyClaim: DependencyClaim, hints: List, -) : ProcessingErrorException(listOf(ProcessingError(getMessage(stack, declaration, dependencyClaim, hints).trimIndent(), declaration.source, Diagnostic.Kind.ERROR))) { +) : ProcessingErrorException(listOf(ProcessingError(getMessage(stack, declaration, dependencyClaim, hints).trimIndent(), dependencyClaim.source ?: declaration.source, Diagnostic.Kind.ERROR))) { companion object { @@ -30,36 +31,38 @@ class UnresolvedDependencyException( dependencyClaim: DependencyClaim, hints: List, ): String { - val msg = if (dependencyClaim.tag == null) { - StringBuilder( - "Required dependency type wasn't found in graph and can't be auto created: ${dependencyClaim.type.toTypeName()} (no tags)\n" + - "Keep in mind that nullable & non nullable types are different in Kotlin.\n" + - "Please check class for @${CommonClassNames.component.canonicalName} annotation or that required module with component factory is plugged in." - ) + val msg = StringBuilder() + msg.append("No component found for dependency:\n ") + msg.append(dependencyClaim.type.toTypeName()) + if (dependencyClaim.tag == null) { + msg.append(" (no tags)") } else { - val tagMsg = "@Tag(${dependencyClaim.tag}::class)" - StringBuilder( - "Required dependency type wasn't found in graph and can't be auto created: ${dependencyClaim.type.toTypeName()} with tag ${tagMsg}.\n" + - "Keep in mind that nullable & non nullable types are different in Kotlin).\n" + - "Please check class for @${CommonClassNames.component.canonicalName} annotation or that required module with component factory is plugged in." - ) + msg.append(" with @Tag(${dependencyClaim.tag}::class)") + } + msg.append("\n\nNote:\n Kotlin nullable and non-nullable types are different dependency keys.") + + val requestedMsg = getRequestedMessage(declaration) + msg.append("\n\nRequired at:\n ").append(requestedMsg) + val source = dependencyClaim.source + if (source is KSValueParameter) { + msg.append("\n parameter: ") + .append(source.type.toTypeName()) + .append(" ") + .append(source.name?.asString() ?: "") } + val treeMsg = getDependencyTreeSimpleMessage(stack, declaration, dependencyClaim) + msg.append("\n\n").append(treeMsg) if (hints.isNotEmpty()) { - msg.append("\n\nHints:") + msg.append("\n\nHint:") for (hint in hints) { - msg.append("\n - Hint: ").append(hint.message()) + msg.append("\n - ").append(hint.message().trim().replace("\n", "\n ")) } } - - val claimMsg = "Required dependency claim: $dependencyClaim" - msg.append("\n\n").append(claimMsg) - - val requestedMsg = getRequestedMessage(declaration) - msg.append("\n").append(requestedMsg) - - val treeMsg = getDependencyTreeSimpleMessage(stack, declaration, dependencyClaim) - msg.append("\n").append(treeMsg) + msg.append("\n\nFix:") + msg.append("\n - Add @${CommonClassNames.component.canonicalName} to an implementation of ${dependencyClaim.type.toTypeName()}.") + msg.append("\n - Add a module function that returns ${dependencyClaim.type.toTypeName()}.") + msg.append("\n - Include a module that provides ${dependencyClaim.type.toTypeName()} in @KoraApp.") return msg.toString() } @@ -81,13 +84,13 @@ class UnresolvedDependencyException( } while (element != null) return if (module != null && factoryMethod != null && factoryMethod.isConstructor()) { - "Dependency requested at: ${module.qualifiedName!!.asString()}#${factoryMethod}(${ + "${module.qualifiedName!!.asString()}#${factoryMethod}(${ factoryMethod.parameters.joinToString(", ") { it.type.toTypeName().toString() } })" } else { - "Dependency requested at: ${module!!.qualifiedName!!.asString()}#${factoryMethod!!.qualifiedName!!.asString()}(${ + "${module!!.qualifiedName!!.asString()}#${factoryMethod!!.qualifiedName!!.asString()}(${ factoryMethod.parameters.joinToString(", ") { it.type.toTypeName().toString() } @@ -101,7 +104,7 @@ class UnresolvedDependencyException( dependencyClaim: DependencyClaim, ): String { val msg = StringBuilder() - msg.append("Dependency resolution tree:") + msg.append("Dependency resolution path:") val stackFrames = mutableListOf() val i = stack.descendingIterator() @@ -152,7 +155,7 @@ class UnresolvedDependencyException( msg.append(delimiter).append(declaration.declarationString()) - val errorMissing = " [ ERROR: MISSING COMPONENT ]" + val errorMissing = " [MISSING]" if (dependencyClaim.tag == null) { msg.append(delimiter) .append(dependencyClaim.type.toTypeName()).append(" ") diff --git a/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/extension/KoraExtension.kt b/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/extension/KoraExtension.kt index 56dcfdead..e52ff8c75 100644 --- a/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/extension/KoraExtension.kt +++ b/core/kora-app-symbol-processor/src/main/kotlin/io/koraframework/kora/app/ksp/extension/KoraExtension.kt @@ -30,7 +30,17 @@ interface KoraExtension { return ret@{ val maybeGenerated = resolver.getClassDeclarationByName("$packageName.$generatedTypeName") if (maybeGenerated == null) { - throw ProcessingErrorException("Class $packageName.$generatedTypeName was expected to be generated from element by annotation processor but was not", source) + throw ProcessingErrorException( + """ + Generated dependency class was not found: + expected type: $packageName.$generatedTypeName + + Fix: + - Ensure the symbol/annotation processor that generates this type is enabled. + - Compile again after fixing earlier processing errors. + """.trimIndent(), + source + ) } if (!CommonAopUtils.hasAopAnnotations(maybeGenerated)) { if (maybeGenerated.primaryConstructor != null) { @@ -41,21 +51,49 @@ interface KoraExtension { .filter { it.isPublic() } .toList() if (constructors.size != 1) { - throw ProcessingErrorException("Extension generated type with more than one constructor", maybeGenerated) + throw ProcessingErrorException( + """ + Extension generated type must have exactly one public constructor. + + Fix: + - Generate one public constructor. + - Make extra constructors non-public. + """.trimIndent(), + maybeGenerated + ) } return@ret ExtensionResult.fromConstructor(constructors[0], maybeGenerated) } val aopProxy = maybeGenerated.generatedClass("_AopProxy") val aopProxyDecl = resolver.getClassDeclarationByName("$packageName.$aopProxy") if (aopProxyDecl == null) { - throw ProcessingErrorException("Class $packageName.$generatedTypeName was expected to be generated from element by annotation processor but was not", maybeGenerated) + throw ProcessingErrorException( + """ + Generated AOP proxy class was not found: + expected type: $packageName.$aopProxy + + Fix: + - Ensure the AOP symbol/annotation processor is enabled. + - Compile again after fixing earlier AOP processing errors. + """.trimIndent(), + maybeGenerated + ) } // probably generated by java annotation processor val constructors = aopProxyDecl.getConstructors() .filter { it.isPublic() } .toList() if (constructors.size != 1) { - throw ProcessingErrorException("Extension generated type with more than one constructor", maybeGenerated) + throw ProcessingErrorException( + """ + Generated AOP proxy type must have exactly one public constructor. + + Fix: + - Generate one public constructor. + - Make extra constructors non-public. + """.trimIndent(), + maybeGenerated + ) } return@ret ExtensionResult.fromConstructor(constructors[0], maybeGenerated) } diff --git a/core/kora-app-symbol-processor/src/test/kotlin/io/koraframework/kora/app/ksp/KoraAppKspTest.kt b/core/kora-app-symbol-processor/src/test/kotlin/io/koraframework/kora/app/ksp/KoraAppKspTest.kt index 14b87651c..dd4f862fc 100644 --- a/core/kora-app-symbol-processor/src/test/kotlin/io/koraframework/kora/app/ksp/KoraAppKspTest.kt +++ b/core/kora-app-symbol-processor/src/test/kotlin/io/koraframework/kora/app/ksp/KoraAppKspTest.kt @@ -201,7 +201,7 @@ class KoraAppKspTest { .isInstanceOfSatisfying(CompilationErrorException::class.java) { e -> SoftAssertions.assertSoftly { s: SoftAssertions -> s.assertThat(e.messages) - .anyMatch { it.contains("Required dependency type wasn't found in graph and can't be auto created: io.koraframework.kora.app.ksp.app.AppWithUnresolvedDependency.Class3") } + .anyMatch { it.contains("No component found for dependency:") && it.contains("AppWithUnresolvedDependency.Class3") && it.contains("Fix:") } } } } @@ -211,7 +211,7 @@ class KoraAppKspTest { Assertions.assertThatThrownBy { testClass(AppWithCircularDependency::class) } .isInstanceOfSatisfying(CompilationErrorException::class.java) { e -> SoftAssertions.assertSoftly { s: SoftAssertions -> - s.assertThat(e.messages).anyMatch { it.contains("Encountered circular dependency in graph for source type") } + s.assertThat(e.messages).anyMatch { it.contains("Circular dependency found:") && it.contains("Dependency cycle:") && it.contains("Fix:") } } } } @@ -239,7 +239,7 @@ class KoraAppKspTest { Assertions.assertThatThrownBy { testClass(AppWithFactories6::class) } .isInstanceOfSatisfying(CompilationErrorException::class.java) { e -> SoftAssertions.assertSoftly { s: SoftAssertions -> - s.assertThat(e.messages).anyMatch { it.contains("Encountered circular dependency in graph for source type") } + s.assertThat(e.messages).anyMatch { it.contains("Circular dependency found:") && it.contains("Dependency cycle:") && it.contains("Fix:") } } } @@ -251,7 +251,7 @@ class KoraAppKspTest { .isInstanceOfSatisfying(CompilationErrorException::class.java) { e -> SoftAssertions.assertSoftly { s: SoftAssertions -> s.assertThat(e.messages).anyMatch { - it.contains("Required dependency type wasn't found in graph and can't be auto created: java.io.Closeable") + it.contains("No component found for dependency:") && it.contains("java.io.Closeable") } } } @@ -291,7 +291,7 @@ class KoraAppKspTest { .isInstanceOfSatisfying(CompilationErrorException::class.java) { e -> SoftAssertions.assertSoftly { s: SoftAssertions -> s.assertThat(e.messages) - .anyMatch { it.contains("More than one component matches dependency type: io.koraframework.kora.app.ksp.app.AppWithComponentCollisionAndDirect.Class1") } + .anyMatch { it.contains("Multiple components match dependency:") && it.contains("AppWithComponentCollisionAndDirect.Class1") && it.contains("Fix:") } } } } @@ -457,4 +457,3 @@ class KoraAppKspTest { } } - From a65a891b52a45a05169c0d6028418e5b70f1c3dc Mon Sep 17 00:00:00 2001 From: Anton Kurako Date: Sun, 19 Jul 2026 13:35:15 +0300 Subject: [PATCH 2/2] Loosen test asserts --- build.gradle | 2 +- .../extension/JsonKoraExtensionTest.java | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/build.gradle b/build.gradle index e725a1af0..72e8a937f 100644 --- a/build.gradle +++ b/build.gradle @@ -347,7 +347,7 @@ subprojects { showCauses(true) showExceptions(true) showStackTraces(true) - events("passed", "skipped", "failed") + events("skipped", "failed") exceptionFormat("full") } } diff --git a/json/json-annotation-processor/src/test/java/io/koraframework/json/annotation/processor/extension/JsonKoraExtensionTest.java b/json/json-annotation-processor/src/test/java/io/koraframework/json/annotation/processor/extension/JsonKoraExtensionTest.java index 2e540556d..12cd7f923 100644 --- a/json/json-annotation-processor/src/test/java/io/koraframework/json/annotation/processor/extension/JsonKoraExtensionTest.java +++ b/json/json-annotation-processor/src/test/java/io/koraframework/json/annotation/processor/extension/JsonKoraExtensionTest.java @@ -28,9 +28,8 @@ interface TestInterface {} """); assertThat(compileResult.isFailed()).isTrue(); - assertThat(compileResult.diagnostic()).anyMatch(d -> d.getKind() == Diagnostic.Kind.ERROR - && d.getMessage(Locale.US).contains("Required dependency type wasn't found in graph and can't be auto created: " + - "io.koraframework.json.common.JsonReader")); + assertThat(compileResult.diagnostic()) + .anySatisfy(diagnostic -> assertMissingJsonMapperError(diagnostic, "JsonReader", "testReaderFromExtensionNotFoundForInterface")); } @Test @@ -63,9 +62,8 @@ interface TestInterface {} """); assertThat(compileResult.isFailed()).isTrue(); - assertThat(compileResult.diagnostic()).anyMatch(d -> d.getKind() == Diagnostic.Kind.ERROR - && d.getMessage(Locale.US).contains("Required dependency type wasn't found in graph and can't be auto created: " + - "io.koraframework.json.common.JsonWriter")); + assertThat(compileResult.diagnostic()) + .anySatisfy(diagnostic -> assertMissingJsonMapperError(diagnostic, "JsonWriter", "testWriterFromExtensionNotFoundForInterface")); } @Test @@ -138,4 +136,12 @@ record Impl2(int value) implements TestInterface { } var app = loadGraph("TestApp"); assertThat(app.draw().getNodes()).hasSize(5); } + + private static void assertMissingJsonMapperError(Diagnostic diagnostic, String mapperType, String testPackage) { + assertThat(diagnostic.getKind()).isEqualTo(Diagnostic.Kind.ERROR); + assertThat(diagnostic.getMessage(Locale.US)) + .contains("io.koraframework.json.common." + mapperType) + .contains(testPackage) + .contains("TestApp.TestInterface"); + } }