• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

valkyrjaio / sindri-java / 30189008732

26 Jul 2026 05:12AM UTC coverage: 98.626%. First build
30189008732

Pull #38

github

web-flow
Merge 9fc51bbbe into 6a875280c
Pull Request #38: [Grpc] Add gRPC data-class generation

512 of 525 branches covered (97.52%)

Branch coverage included in aggregate %.

381 of 396 new or added lines in 17 files covered. (96.21%)

1498 of 1513 relevant lines covered (99.01%)

4.33 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.97
/src/main/java/io/sindri/ast/MiddlewareClassifier.java
1
/*
2
 * This file is part of the Sindri package.
3
 *
4
 * (c) Melech Mizrachi <melechmizrachi@gmail.com>
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9

10
package io.sindri.ast;
11

12
import com.github.javaparser.ast.CompilationUnit;
13
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
14
import com.github.javaparser.ast.body.MethodDeclaration;
15
import com.github.javaparser.ast.expr.AnnotationExpr;
16
import com.github.javaparser.ast.expr.ArrayInitializerExpr;
17
import com.github.javaparser.ast.expr.MemberValuePair;
18
import com.github.javaparser.ast.expr.NormalAnnotationExpr;
19
import com.github.javaparser.ast.type.ClassOrInterfaceType;
20
import io.sindri.ast.abstract_.AstReader;
21
import java.util.ArrayDeque;
22
import java.util.ArrayList;
23
import java.util.Deque;
24
import java.util.HashSet;
25
import java.util.LinkedHashMap;
26
import java.util.LinkedHashSet;
27
import java.util.List;
28
import java.util.Map;
29
import java.util.Optional;
30
import java.util.Set;
31

32
/**
33
 * Classifies a middleware class by which stage contracts it implements — the syntactic equivalent
34
 * of the runtime collector's {@code isAssignableFrom} cascade, for the route-data cache.
35
 *
36
 * <p>A {@code @Middleware} annotation names only the class, not its stage; the stage is the set of
37
 * stage contracts the class implements, directly or through an abstract base or a sub-contract.
38
 * This walks the class's full {@code implements}/{@code extends} ancestry (resolving names through
39
 * each source's imports) and reports which of the given target contracts it reaches. Targets are
40
 * matched by fully-qualified name and never parsed, so the framework contracts need not be
41
 * resolvable — which keeps the cache generator working even before the target module has been
42
 * published.
43
 */
44
public class MiddlewareClassifier extends AstReader {
3✔
45

46
    /** Resolves a fully-qualified type name to its parsed source, or empty when unresolvable. */
47
    public interface SourceResolver {
48

49
        Optional<CompilationUnit> resolve(String fqn);
50
    }
51

52
    /**
53
     * Return the subset of {@code targetFqns} the middleware reaches through its type hierarchy.
54
     *
55
     * @param middlewareFqn the middleware class
56
     * @param resolver resolves an app/framework class to its source (empty stops that branch)
57
     * @param targetFqns the stage-contract fully-qualified names to match against
58
     * @return the matched target FQNs (a class may match several stages)
59
     */
60
    public Set<String> classify(
61
            String middlewareFqn, SourceResolver resolver, Set<String> targetFqns) {
62
        Set<String> matched = new LinkedHashSet<>();
4✔
63
        Set<String> visited = new HashSet<>();
4✔
64
        Deque<String> pending = new ArrayDeque<>();
4✔
65
        pending.push(middlewareFqn);
3✔
66

67
        while (!pending.isEmpty()) {
3✔
68
            String fqn = pending.pop();
4✔
69
            if (!visited.add(fqn)) {
4!
NEW
70
                continue;
×
71
            }
72
            if (targetFqns.contains(fqn)) {
4✔
73
                // A target is terminal: record it, and don't parse it (it may be unresolvable).
74
                matched.add(fqn);
4✔
75
                continue;
1✔
76
            }
77

78
            Optional<CompilationUnit> cu = resolver.resolve(fqn);
4✔
79
            if (cu.isEmpty()) {
3✔
80
                // Unresolvable ancestor (a non-target framework or JDK type) — stop this branch.
81
                continue;
1✔
82
            }
83
            Optional<ClassOrInterfaceDeclaration> type = findClass(cu.get());
6✔
84
            if (type.isEmpty()) {
3!
NEW
85
                continue;
×
86
            }
87

88
            Map<String, String> importMap = buildImportMap(cu.get());
6✔
89
            List<String> wildcards = wildcardPackages(cu.get());
6✔
90
            String pkg = getPackageName(cu.get());
6✔
91
            for (ClassOrInterfaceType ancestor : type.get().getImplementedTypes()) {
13✔
92
                pending.addAll(
9✔
93
                        resolveNameCandidates(ancestor, importMap, wildcards, pkg, resolver));
1✔
94
            }
1✔
95
            for (ClassOrInterfaceType ancestor : type.get().getExtendedTypes()) {
13✔
96
                pending.addAll(
9✔
97
                        resolveNameCandidates(ancestor, importMap, wildcards, pkg, resolver));
1✔
98
            }
1✔
99
        }
1✔
100

101
        return matched;
2✔
102
    }
103

104
    /** The packages of the file's wildcard imports ({@code import pkg.*;}). */
105
    private List<String> wildcardPackages(CompilationUnit cu) {
106
        List<String> packages = new ArrayList<>();
4✔
107
        for (var importDeclaration : cu.getImports()) {
11✔
108
            if (importDeclaration.isAsterisk() && !importDeclaration.isStatic()) {
6!
109
                packages.add(importDeclaration.getNameAsString());
5✔
110
            }
111
        }
1✔
112
        return packages;
2✔
113
    }
114

115
    /**
116
     * Read every {@code @Middleware} on a method and group each by the stage contracts it reaches.
117
     *
118
     * <p>Shared by the gRPC, HTTP, and CLI route readers — the {@code @Middleware} attribute and
119
     * the classification are identical across protocols; only the set of stage contracts differs.
120
     *
121
     * @param method the handler method
122
     * @param imports the declaring file's imports, for resolving the middleware class names
123
     * @param pkg the declaring file's package, for same-package middleware
124
     * @param resolver resolves a middleware/base class to its source
125
     * @param targetFqns the protocol's stage-contract fully-qualified names
126
     * @return each matched target contract mapped to the middleware classes assigned to it, in
127
     *     order
128
     */
129
    public Map<String, List<String>> classifyMethod(
130
            MethodDeclaration method,
131
            Map<String, String> imports,
132
            String pkg,
133
            SourceResolver resolver,
134
            Set<String> targetFqns) {
135
        Map<String, List<String>> byTarget = new LinkedHashMap<>();
4✔
136
        for (String middlewareFqn : middlewareClasses(method, imports, pkg)) {
14✔
137
            for (String matched : classify(middlewareFqn, resolver, targetFqns)) {
14✔
138
                byTarget.computeIfAbsent(matched, key -> new ArrayList<>()).add(middlewareFqn);
12✔
139
            }
1✔
140
        }
1✔
141
        return byTarget;
2✔
142
    }
143

144
    /**
145
     * Collect the fully-qualified middleware classes named by {@code @Middleware} on the method,
146
     * expanding the repeatable container when the source spells it out explicitly. Classes are
147
     * appended in source order and never deduplicated — the generated cache mirrors the runtime
148
     * collector, which likewise appends, so a duplicate runs as many times as it is declared. Which
149
     * copy to drop and whether order matters are the developer's call, not the framework's.
150
     */
151
    private List<String> middlewareClasses(
152
            MethodDeclaration method, Map<String, String> imports, String pkg) {
153
        List<String> classes = new ArrayList<>();
4✔
154

155
        for (AnnotationExpr annotation : method.getAnnotations()) {
11✔
156
            String annotationName = annotation.getNameAsString();
3✔
157
            if (annotationName.equals("Middleware")) {
4✔
158
                addMiddlewareClass(annotation, imports, pkg, classes);
7✔
159
            } else if (annotationName.equals("Middlewares")) {
4✔
160
                annotation
2✔
161
                        .findAll(ArrayInitializerExpr.class)
6✔
162
                        .forEach(
1✔
163
                                array ->
164
                                        array.getValues()
8✔
165
                                                .forEach(
1✔
166
                                                        value -> {
167
                                                            if (value
3!
168
                                                                    instanceof
169
                                                                    AnnotationExpr nested) {
3✔
170
                                                                addMiddlewareClass(
6✔
171
                                                                        nested, imports, pkg,
172
                                                                        classes);
173
                                                            }
174
                                                        }));
1✔
175
            }
176
        }
1✔
177

178
        return classes;
2✔
179
    }
180

181
    private void addMiddlewareClass(
182
            AnnotationExpr annotation, Map<String, String> imports, String pkg, List<String> into) {
183
        if (!(annotation instanceof NormalAnnotationExpr normal)) {
7!
NEW
184
            return;
×
185
        }
186

187
        for (MemberValuePair pair : normal.getPairs()) {
11✔
188
            if (!pair.getNameAsString().equals("name") || !pair.getValue().isClassExpr()) {
9!
NEW
189
                continue;
×
190
            }
191

192
            String written = pair.getValue().asClassExpr().getType().asString();
6✔
193
            String fqn =
194
                    written.contains(".")
4✔
195
                            ? written
2✔
196
                            : imports.getOrDefault(
5✔
197
                                    written, pkg.isEmpty() ? written : pkg + "." + written);
6!
198
            into.add(fqn);
4✔
199
        }
1✔
200
    }
1✔
201

202
    /**
203
     * Resolve a written {@code implements}/{@code extends} type to its candidate fully-qualified
204
     * names, following Java's name-resolution precedence: an already-qualified reference or an
205
     * explicit single-type import resolves to exactly one; a bare simple name resolves to the
206
     * same-package type if one actually exists (a same-package type shadows every wildcard import),
207
     * and only otherwise fans out to the wildcard-import packages. Returning the wildcard
208
     * candidates only when the same-package name does not resolve keeps a genuine wildcard-imported
209
     * stage contract from being dropped, while stopping a local class that merely shares a
210
     * contract's simple name from being misclassified as that contract.
211
     */
212
    private List<String> resolveNameCandidates(
213
            ClassOrInterfaceType type,
214
            Map<String, String> importMap,
215
            List<String> wildcards,
216
            String pkg,
217
            SourceResolver resolver) {
218
        String written =
1✔
219
                type.getScope().map(scope -> scope.asString() + ".").orElse("")
11✔
220
                        + type.getNameAsString();
3✔
221
        if (written.contains(".")) {
4✔
222
            return List.of(written);
3✔
223
        }
224
        if (importMap.containsKey(written)) {
4✔
225
            return List.of(importMap.get(written));
6✔
226
        }
227

228
        String samePackage = pkg.isEmpty() ? written : pkg + "." + written;
7!
229
        if (resolver.resolve(samePackage).isPresent()) {
5✔
230
            // A same-package type shadows any wildcard import (JLS §6.5.5.1), so it is the only
231
            // candidate — don't also try the wildcard packages and risk a false target match.
232
            return List.of(samePackage);
3✔
233
        }
234

235
        List<String> candidates = new ArrayList<>();
4✔
236
        candidates.add(samePackage);
4✔
237
        for (String wildcard : wildcards) {
10✔
238
            candidates.add(wildcard + "." + written);
6✔
239
        }
1✔
240
        return candidates;
2✔
241
    }
242
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc