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

valkyrjaio / sindri-java / 30150631134

25 Jul 2026 08:10AM UTC coverage: 98.672%. First build
30150631134

Pull #38

github

web-flow
Merge bae2a9dfc into 04c800ff7
Pull Request #38: [Grpc] Add gRPC data-class generation

512 of 525 branches covered (97.52%)

Branch coverage included in aggregate %.

377 of 391 new or added lines in 16 files covered. (96.42%)

1494 of 1508 relevant lines covered (99.07%)

4.34 hits per line

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

90.71
/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(resolveNameCandidates(ancestor, importMap, wildcards, pkg));
9✔
93
            }
1✔
94
            for (ClassOrInterfaceType ancestor : type.get().getExtendedTypes()) {
13✔
95
                pending.addAll(resolveNameCandidates(ancestor, importMap, wildcards, pkg));
9✔
96
            }
1✔
97
        }
1✔
98

99
        return matched;
2✔
100
    }
101

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

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

142
    /**
143
     * Collect the fully-qualified middleware classes named by {@code @Middleware} on the method,
144
     * expanding the repeatable container when the source spells it out explicitly.
145
     */
146
    private List<String> middlewareClasses(
147
            MethodDeclaration method, Map<String, String> imports, String pkg) {
148
        List<String> classes = new ArrayList<>();
4✔
149

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

173
        return classes;
2✔
174
    }
175

176
    private void addMiddlewareClass(
177
            AnnotationExpr annotation, Map<String, String> imports, String pkg, List<String> into) {
178
        if (!(annotation instanceof NormalAnnotationExpr normal)) {
7!
NEW
179
            return;
×
180
        }
181

182
        for (MemberValuePair pair : normal.getPairs()) {
11✔
183
            if (!pair.getNameAsString().equals("name") || !pair.getValue().isClassExpr()) {
9!
NEW
184
                continue;
×
185
            }
186

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

199
    /**
200
     * Resolve a written {@code implements}/{@code extends} type to its candidate fully-qualified
201
     * names: an already-qualified reference or an explicit import resolves to exactly one; a bare
202
     * simple name with no explicit import could come from the same package OR any wildcard import,
203
     * so every possibility is returned and the walk matches whichever is a real target (extra
204
     * candidates that resolve to nothing are harmless dead ends). Without this, a stage contract
205
     * reached through a wildcard import would be missed and its stage silently dropped.
206
     */
207
    private List<String> resolveNameCandidates(
208
            ClassOrInterfaceType type,
209
            Map<String, String> importMap,
210
            List<String> wildcards,
211
            String pkg) {
212
        String written =
1✔
213
                type.getScope().map(scope -> scope.asString() + ".").orElse("")
11✔
214
                        + type.getNameAsString();
3✔
215
        if (written.contains(".")) {
4✔
216
            return List.of(written);
3✔
217
        }
218
        if (importMap.containsKey(written)) {
4✔
219
            return List.of(importMap.get(written));
6✔
220
        }
221

222
        List<String> candidates = new ArrayList<>();
4✔
223
        candidates.add(pkg.isEmpty() ? written : pkg + "." + written);
9!
224
        for (String wildcard : wildcards) {
10✔
225
            candidates.add(wildcard + "." + written);
6✔
226
        }
1✔
227
        return candidates;
2✔
228
    }
229
}
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