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

valkyrjaio / sindri-java / 30141271108

25 Jul 2026 02:54AM UTC coverage: 97.972%. First build
30141271108

Pull #38

github

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

491 of 507 branches covered (96.84%)

Branch coverage included in aggregate %.

317 of 341 new or added lines in 15 files covered. (92.96%)

1441 of 1465 relevant lines covered (98.36%)

4.31 hits per line

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

78.33
/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
            String pkg = getPackageName(cu.get());
6✔
90
            for (ClassOrInterfaceType ancestor : type.get().getImplementedTypes()) {
13✔
91
                pending.push(resolveName(ancestor, importMap, pkg));
7✔
92
            }
1✔
93
            for (ClassOrInterfaceType ancestor : type.get().getExtendedTypes()) {
13✔
94
                pending.push(resolveName(ancestor, importMap, pkg));
7✔
95
            }
1✔
96
        }
1✔
97

98
        return matched;
2✔
99
    }
100

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

130
    /**
131
     * Collect the fully-qualified middleware classes named by {@code @Middleware} on the method,
132
     * expanding the repeatable container when the source spells it out explicitly.
133
     */
134
    private List<String> middlewareClasses(
135
            MethodDeclaration method, Map<String, String> imports, String pkg) {
136
        List<String> classes = new ArrayList<>();
4✔
137

138
        for (AnnotationExpr annotation : method.getAnnotations()) {
11✔
139
            String annotationName = annotation.getNameAsString();
3✔
140
            if (annotationName.equals("Middleware")) {
4✔
141
                addMiddlewareClass(annotation, imports, pkg, classes);
7✔
142
            } else if (annotationName.equals("Middlewares")) {
4!
NEW
143
                annotation
×
NEW
144
                        .findAll(ArrayInitializerExpr.class)
×
NEW
145
                        .forEach(
×
146
                                array ->
NEW
147
                                        array.getValues()
×
NEW
148
                                                .forEach(
×
149
                                                        value -> {
NEW
150
                                                            if (value
×
151
                                                                    instanceof
NEW
152
                                                                    AnnotationExpr nested) {
×
NEW
153
                                                                addMiddlewareClass(
×
154
                                                                        nested, imports, pkg,
155
                                                                        classes);
156
                                                            }
NEW
157
                                                        }));
×
158
            }
159
        }
1✔
160

161
        return classes;
2✔
162
    }
163

164
    private void addMiddlewareClass(
165
            AnnotationExpr annotation, Map<String, String> imports, String pkg, List<String> into) {
166
        if (!(annotation instanceof NormalAnnotationExpr normal)) {
7!
NEW
167
            return;
×
168
        }
169

170
        for (MemberValuePair pair : normal.getPairs()) {
11✔
171
            if (!pair.getNameAsString().equals("name") || !pair.getValue().isClassExpr()) {
9!
NEW
172
                continue;
×
173
            }
174

175
            String written = pair.getValue().asClassExpr().getType().asString();
6✔
176
            String fqn =
177
                    written.contains(".")
4!
NEW
178
                            ? written
×
179
                            : imports.getOrDefault(
5✔
180
                                    written, pkg.isEmpty() ? written : pkg + "." + written);
6!
181
            if (!into.contains(fqn)) {
4!
182
                into.add(fqn);
4✔
183
            }
184
        }
1✔
185
    }
1✔
186

187
    /**
188
     * Resolve a written {@code implements}/{@code extends} type to a fully-qualified name: an
189
     * already-qualified reference is taken as-is; a simple name is resolved through the file's
190
     * imports, falling back to the same package.
191
     */
192
    private String resolveName(
193
            ClassOrInterfaceType type, Map<String, String> importMap, String pkg) {
194
        String written =
1✔
195
                type.getScope().map(scope -> scope.asString() + ".").orElse("")
11✔
196
                        + type.getNameAsString();
3✔
197
        if (written.contains(".")) {
4✔
198
            return written;
2✔
199
        }
200
        if (importMap.containsKey(written)) {
4✔
201
            return importMap.get(written);
5✔
202
        }
203
        return pkg.isEmpty() ? written : pkg + "." + written;
7!
204
    }
205
}
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