Skip to content

Commit e591f65

Browse files
committed
[BWC and API enforcement] Introduce checks for enforcing the API restrictions
Signed-off-by: Andriy Redko <andriy.redko@aiven.io>
1 parent c676479 commit e591f65

10 files changed

Lines changed: 604 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
102102
- Add cluster state stats ([#10670](https://github.com/opensearch-project/OpenSearch/pull/10670))
103103
- Adding slf4j license header to LoggerMessageFormat.java ([#11069](https://github.com/opensearch-project/OpenSearch/pull/11069))
104104
- [Streaming Indexing] Introduce new experimental server HTTP transport based on Netty 4 and Project Reactor (Reactor Netty) ([#9672](https://github.com/opensearch-project/OpenSearch/pull/9672))
105+
- [BWC and API enforcement] Introduce checks for enforcing the API restrictions ([#11175](https://github.com/opensearch-project/OpenSearch/pull/11175))
105106

106107
### Dependencies
107108
- Bump `com.google.api.grpc:proto-google-common-protos` from 2.10.0 to 2.25.1 ([#10208](https://github.com/opensearch-project/OpenSearch/pull/10208), [#10298](https://github.com/opensearch-project/OpenSearch/pull/10298))
Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*/
8+
9+
package org.opensearch.common.annotation.processor;
10+
11+
import org.opensearch.common.Nullable;
12+
import org.opensearch.common.annotation.DeprecatedApi;
13+
import org.opensearch.common.annotation.ExperimentalApi;
14+
import org.opensearch.common.annotation.InternalApi;
15+
import org.opensearch.common.annotation.PublicApi;
16+
17+
import javax.annotation.processing.AbstractProcessor;
18+
import javax.annotation.processing.RoundEnvironment;
19+
import javax.annotation.processing.SupportedAnnotationTypes;
20+
import javax.lang.model.AnnotatedConstruct;
21+
import javax.lang.model.SourceVersion;
22+
import javax.lang.model.element.AnnotationMirror;
23+
import javax.lang.model.element.Element;
24+
import javax.lang.model.element.ElementKind;
25+
import javax.lang.model.element.ExecutableElement;
26+
import javax.lang.model.element.Modifier;
27+
import javax.lang.model.element.PackageElement;
28+
import javax.lang.model.element.TypeElement;
29+
import javax.lang.model.element.TypeParameterElement;
30+
import javax.lang.model.element.VariableElement;
31+
import javax.lang.model.type.ArrayType;
32+
import javax.lang.model.type.DeclaredType;
33+
import javax.lang.model.type.ReferenceType;
34+
import javax.lang.model.type.TypeMirror;
35+
import javax.lang.model.type.TypeVariable;
36+
import javax.lang.model.type.WildcardType;
37+
import javax.tools.Diagnostic.Kind;
38+
39+
import java.util.HashSet;
40+
import java.util.Set;
41+
42+
/**
43+
* The annotation processor for API related annotations: {@link DeprecatedApi}, {@link ExperimentalApi},
44+
* {@link InternalApi} and {@link PublicApi}.
45+
* <p>
46+
* The checks are built on top of the following rules:
47+
* <ul>
48+
* <li>introspect each type annotated with {@link PublicApi}, {@link DeprecatedApi} or {@link ExperimentalApi},
49+
* filtering out package-private declarations</li>
50+
* <li>make sure those leak only {@link PublicApi}, {@link DeprecatedApi} or {@link ExperimentalApi} types as well (exceptions,
51+
* method return values, method arguments, method generic type arguments, class generic type arguments, annotations)</li>
52+
* <li>recursively follow the type introspection chains to enforce the rules down the line</li>
53+
* </ul>
54+
*/
55+
@InternalApi
56+
@SupportedAnnotationTypes("org.opensearch.common.annotation.*")
57+
public class ApiAnnotationProcessor extends AbstractProcessor {
58+
private static final String OPTION_WARN_ON_FAILING_CHECKS = "warnOnFailingChecks";
59+
private static final String OPENSEARCH_PACKAGE = "org.opensearch";
60+
61+
private final Set<Element> reported = new HashSet<>();
62+
private final Set<AnnotatedConstruct> processed = new HashSet<>();
63+
private Kind reportFailureAs = Kind.ERROR;
64+
65+
@Override
66+
public SourceVersion getSupportedSourceVersion() {
67+
return SourceVersion.latest();
68+
}
69+
70+
@Override
71+
public Set<String> getSupportedOptions() {
72+
return Set.of(OPTION_WARN_ON_FAILING_CHECKS);
73+
}
74+
75+
@Override
76+
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment round) {
77+
processingEnv.getMessager().printMessage(Kind.NOTE, "Processing OpenSearch Api annotations");
78+
79+
if (processingEnv.getOptions().containsKey(OPTION_WARN_ON_FAILING_CHECKS) == true) {
80+
reportFailureAs = Kind.NOTE;
81+
}
82+
83+
final Set<? extends Element> elements = round.getElementsAnnotatedWithAny(
84+
Set.of(PublicApi.class, ExperimentalApi.class, DeprecatedApi.class)
85+
);
86+
87+
for (var element : elements) {
88+
if (!checkPackage(element)) {
89+
continue;
90+
}
91+
92+
// Skip all not-public elements
93+
if (!element.getModifiers().contains(Modifier.PUBLIC)) {
94+
continue;
95+
}
96+
97+
if (element instanceof TypeElement) {
98+
process((TypeElement) element);
99+
}
100+
}
101+
102+
return false;
103+
}
104+
105+
/**
106+
* Check top level executable element
107+
* @param executable top level executable element
108+
*/
109+
private void process(ExecutableElement executable) {
110+
if (!inspectable(executable)) {
111+
return;
112+
}
113+
114+
// The executable element should not be internal (unless constructor for injectable core component)
115+
checkNotInternal(null, executable);
116+
117+
// Process method return types
118+
final TypeMirror returnType = executable.getReturnType();
119+
if (returnType instanceof ReferenceType) {
120+
process(executable, (ReferenceType) returnType);
121+
}
122+
123+
// Process method thrown types
124+
for (final TypeMirror thrownType : executable.getThrownTypes()) {
125+
if (thrownType instanceof ReferenceType) {
126+
process(executable, (ReferenceType) thrownType);
127+
}
128+
}
129+
130+
// Process method type parameters
131+
for (final TypeParameterElement typeParameter : executable.getTypeParameters()) {
132+
for (final TypeMirror boundType : typeParameter.getBounds()) {
133+
if (boundType instanceof ReferenceType) {
134+
process(executable, (ReferenceType) boundType);
135+
}
136+
}
137+
}
138+
139+
// Process method arguments
140+
for (final VariableElement parameter : executable.getParameters()) {
141+
final TypeMirror parameterType = parameter.asType();
142+
if (parameterType instanceof ReferenceType) {
143+
process(executable, (ReferenceType) parameterType);
144+
}
145+
}
146+
}
147+
148+
/**
149+
* Check wildcard type bounds referred by an element
150+
* @param executable element
151+
* @param type wildcard type
152+
*/
153+
private void process(ExecutableElement executable, WildcardType type) {
154+
if (type.getExtendsBound() instanceof ReferenceType) {
155+
process(executable, (ReferenceType) type.getExtendsBound());
156+
}
157+
158+
if (type.getSuperBound() instanceof ReferenceType) {
159+
process(executable, (ReferenceType) type.getSuperBound());
160+
}
161+
}
162+
163+
/**
164+
* Check reference type bounds referred by an executable element
165+
* @param executable executable element
166+
* @param ref reference type
167+
*/
168+
private void process(ExecutableElement executable, ReferenceType ref) {
169+
// The element has been processed already
170+
if (processed.add(ref) == false) {
171+
return;
172+
}
173+
174+
if (ref instanceof DeclaredType) {
175+
final DeclaredType declaredType = (DeclaredType) ref;
176+
177+
final Element element = declaredType.asElement();
178+
if (inspectable(element)) {
179+
checkNotInternal(executable.getEnclosingElement(), element);
180+
checkPublic(executable.getEnclosingElement(), element);
181+
}
182+
183+
for (final TypeMirror type : declaredType.getTypeArguments()) {
184+
if (type instanceof ReferenceType) {
185+
process(executable, (ReferenceType) type);
186+
} else if (type instanceof WildcardType) {
187+
process(executable, (WildcardType) type);
188+
}
189+
}
190+
} else if (ref instanceof ArrayType) {
191+
final TypeMirror componentType = ((ArrayType) ref).getComponentType();
192+
if (componentType instanceof ReferenceType) {
193+
process(executable, (ReferenceType) componentType);
194+
}
195+
} else if (ref instanceof TypeVariable) {
196+
final TypeVariable typeVariable = (TypeVariable) ref;
197+
if (typeVariable.getUpperBound() instanceof ReferenceType) {
198+
process(executable, (ReferenceType) typeVariable.getUpperBound());
199+
}
200+
if (typeVariable.getLowerBound() instanceof ReferenceType) {
201+
process(executable, (ReferenceType) typeVariable.getLowerBound());
202+
}
203+
}
204+
205+
// Check this elements annotations
206+
for (final AnnotationMirror annotation : ref.getAnnotationMirrors()) {
207+
final Element element = annotation.getAnnotationType().asElement();
208+
if (inspectable(element)) {
209+
checkNotInternal(executable.getEnclosingElement(), element);
210+
checkPublic(executable.getEnclosingElement(), element);
211+
}
212+
}
213+
}
214+
215+
/**
216+
* Check if a particular executable element should be inspected or not
217+
* @param executable executable element to inspect
218+
* @return {@code true} if a particular executable element should be inspected, {@code false} otherwise
219+
*/
220+
private boolean inspectable(ExecutableElement executable) {
221+
// The constructors for public APIs could use non-public APIs when those are supposed to be only
222+
// consumed (not instantiated) by external consumers.
223+
return executable.getKind() != ElementKind.CONSTRUCTOR && executable.getModifiers().contains(Modifier.PUBLIC);
224+
}
225+
226+
/**
227+
* Check if a particular element should be inspected or not
228+
* @param element element to inspect
229+
* @return {@code true} if a particular element should be inspected, {@code false} otherwise
230+
*/
231+
private boolean inspectable(Element element) {
232+
final PackageElement pckg = processingEnv.getElementUtils().getPackageOf(element);
233+
return pckg.getQualifiedName().toString().startsWith(OPENSEARCH_PACKAGE);
234+
}
235+
236+
/**
237+
* Check if a particular element belongs to OpenSeach managed packages
238+
* @param element element to inspect
239+
* @return {@code true} if a particular element belongs to OpenSeach managed packages, {@code false} otherwise
240+
*/
241+
private boolean checkPackage(Element element) {
242+
// The element was reported already
243+
if (reported.contains(element)) {
244+
return false;
245+
}
246+
247+
final PackageElement pckg = processingEnv.getElementUtils().getPackageOf(element);
248+
final boolean belongsToOpenSearch = pckg.getQualifiedName().toString().startsWith(OPENSEARCH_PACKAGE);
249+
250+
if (!belongsToOpenSearch) {
251+
reported.add(element);
252+
253+
processingEnv.getMessager()
254+
.printMessage(
255+
reportFailureAs,
256+
"The type "
257+
+ element
258+
+ " is not residing in "
259+
+ OPENSEARCH_PACKAGE
260+
+ ".* package "
261+
+ "and should not be annotated as OpenSearch APIs."
262+
);
263+
}
264+
265+
return belongsToOpenSearch;
266+
}
267+
268+
/**
269+
* Check the fields, methods, constructors, and member types that are directly
270+
* declared in this class or interface.
271+
* @param type class or interface
272+
*/
273+
private void process(Element type) {
274+
// Check the fields, methods, constructors, and member types that are directly
275+
// declared in this class or interface.
276+
for (final Element element : type.getEnclosedElements()) {
277+
// Skip all not-public elements
278+
if (!type.getModifiers().contains(Modifier.PUBLIC)) {
279+
continue;
280+
}
281+
282+
if (element instanceof ExecutableElement) {
283+
process((ExecutableElement) element);
284+
}
285+
}
286+
}
287+
288+
/**
289+
* Check if element is public and annotated with {@link PublicApi}, {@link DeprecatedApi} or {@link ExperimentalApi}
290+
* @param referencedBy the referrer for the element
291+
* @param element element to check
292+
*/
293+
private void checkPublic(@Nullable Element referencedBy, final Element element) {
294+
// The element was reported already
295+
if (reported.contains(element)) {
296+
return;
297+
}
298+
299+
if (!element.getModifiers().contains(Modifier.PUBLIC)) {
300+
reported.add(element);
301+
302+
processingEnv.getMessager()
303+
.printMessage(
304+
reportFailureAs,
305+
"The element "
306+
+ element
307+
+ " is part of the public APIs but does not have public visibility"
308+
+ ((referencedBy != null) ? " (referenced by " + referencedBy + ") " : "")
309+
);
310+
}
311+
312+
if (element.getAnnotation(PublicApi.class) == null
313+
&& element.getAnnotation(ExperimentalApi.class) == null
314+
&& element.getAnnotation(DeprecatedApi.class) == null) {
315+
reported.add(element);
316+
317+
processingEnv.getMessager()
318+
.printMessage(
319+
reportFailureAs,
320+
"The element "
321+
+ element
322+
+ " is part of the public APIs but is not maked as @PublicApi, @ExperimentalApi or @DeprecatedApi"
323+
+ ((referencedBy != null) ? " (referenced by " + referencedBy + ") " : "")
324+
);
325+
}
326+
}
327+
328+
/**
329+
* Check if element is not annotated with {@link InternalApi}
330+
* @param referencedBy the referrer for the element
331+
* @param element element to check
332+
*/
333+
private void checkNotInternal(@Nullable Element referencedBy, final Element element) {
334+
// The element was reported already
335+
if (reported.contains(element)) {
336+
return;
337+
}
338+
339+
if (element.getAnnotation(InternalApi.class) != null) {
340+
reported.add(element);
341+
342+
processingEnv.getMessager()
343+
.printMessage(
344+
reportFailureAs,
345+
"The element "
346+
+ element
347+
+ " is part of the public APIs but is marked as @InternalApi"
348+
+ ((referencedBy != null) ? " (referenced by " + referencedBy + ") " : "")
349+
);
350+
}
351+
}
352+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*/
8+
9+
/**
10+
* Classes related yo OpenSearch API annotation processing
11+
*
12+
* @opensearch.internal
13+
*/
14+
@org.opensearch.common.annotation.InternalApi
15+
package org.opensearch.common.annotation.processor;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
# The OpenSearch Contributors require contributions made to
5+
# this file be licensed under the Apache-2.0 license or a
6+
# compatible open source license.
7+
#
8+
# Modifications Copyright OpenSearch Contributors. See
9+
# GitHub history for details.
10+
#
11+
12+
org.opensearch.common.annotation.processor.ApiAnnotationProcessor

0 commit comments

Comments
 (0)