Skip to content

Commit b7530f9

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 c0c76e6 commit b7530f9

17 files changed

Lines changed: 808 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
104104
- Adding slf4j license header to LoggerMessageFormat.java ([#11069](https://github.com/opensearch-project/OpenSearch/pull/11069))
105105
- [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))
106106
- Allowing pipeline processors to access index mapping info by passing ingest service ref as part of the processor factory parameters ([#10307](https://github.com/opensearch-project/OpenSearch/pull/10307))
107+
- [BWC and API enforcement] Introduce checks for enforcing the API restrictions ([#11175](https://github.com/opensearch-project/OpenSearch/pull/11175))
107108

108109
### Dependencies
109110
- 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: 353 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,353 @@
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_CONTINUE_ON_FAILING_CHECKS = "continueOnFailingChecks";
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_CONTINUE_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_CONTINUE_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+
* @param enclosing enclosing element
109+
*/
110+
private void process(ExecutableElement executable, Element enclosing) {
111+
if (!inspectable(executable)) {
112+
return;
113+
}
114+
115+
// The executable element should not be internal (unless constructor for injectable core component)
116+
checkNotInternal(enclosing, executable);
117+
118+
// Process method return types
119+
final TypeMirror returnType = executable.getReturnType();
120+
if (returnType instanceof ReferenceType) {
121+
process(executable, (ReferenceType) returnType);
122+
}
123+
124+
// Process method thrown types
125+
for (final TypeMirror thrownType : executable.getThrownTypes()) {
126+
if (thrownType instanceof ReferenceType) {
127+
process(executable, (ReferenceType) thrownType);
128+
}
129+
}
130+
131+
// Process method type parameters
132+
for (final TypeParameterElement typeParameter : executable.getTypeParameters()) {
133+
for (final TypeMirror boundType : typeParameter.getBounds()) {
134+
if (boundType instanceof ReferenceType) {
135+
process(executable, (ReferenceType) boundType);
136+
}
137+
}
138+
}
139+
140+
// Process method arguments
141+
for (final VariableElement parameter : executable.getParameters()) {
142+
final TypeMirror parameterType = parameter.asType();
143+
if (parameterType instanceof ReferenceType) {
144+
process(executable, (ReferenceType) parameterType);
145+
}
146+
}
147+
}
148+
149+
/**
150+
* Check wildcard type bounds referred by an element
151+
* @param executable element
152+
* @param type wildcard type
153+
*/
154+
private void process(ExecutableElement executable, WildcardType type) {
155+
if (type.getExtendsBound() instanceof ReferenceType) {
156+
process(executable, (ReferenceType) type.getExtendsBound());
157+
}
158+
159+
if (type.getSuperBound() instanceof ReferenceType) {
160+
process(executable, (ReferenceType) type.getSuperBound());
161+
}
162+
}
163+
164+
/**
165+
* Check reference type bounds referred by an executable element
166+
* @param executable executable element
167+
* @param ref reference type
168+
*/
169+
private void process(ExecutableElement executable, ReferenceType ref) {
170+
// The element has been processed already
171+
if (processed.add(ref) == false) {
172+
return;
173+
}
174+
175+
if (ref instanceof DeclaredType) {
176+
final DeclaredType declaredType = (DeclaredType) ref;
177+
178+
final Element element = declaredType.asElement();
179+
if (inspectable(element)) {
180+
checkNotInternal(executable.getEnclosingElement(), element);
181+
checkPublic(executable.getEnclosingElement(), element);
182+
}
183+
184+
for (final TypeMirror type : declaredType.getTypeArguments()) {
185+
if (type instanceof ReferenceType) {
186+
process(executable, (ReferenceType) type);
187+
} else if (type instanceof WildcardType) {
188+
process(executable, (WildcardType) type);
189+
}
190+
}
191+
} else if (ref instanceof ArrayType) {
192+
final TypeMirror componentType = ((ArrayType) ref).getComponentType();
193+
if (componentType instanceof ReferenceType) {
194+
process(executable, (ReferenceType) componentType);
195+
}
196+
} else if (ref instanceof TypeVariable) {
197+
final TypeVariable typeVariable = (TypeVariable) ref;
198+
if (typeVariable.getUpperBound() instanceof ReferenceType) {
199+
process(executable, (ReferenceType) typeVariable.getUpperBound());
200+
}
201+
if (typeVariable.getLowerBound() instanceof ReferenceType) {
202+
process(executable, (ReferenceType) typeVariable.getLowerBound());
203+
}
204+
}
205+
206+
// Check this elements annotations
207+
for (final AnnotationMirror annotation : ref.getAnnotationMirrors()) {
208+
final Element element = annotation.getAnnotationType().asElement();
209+
if (inspectable(element)) {
210+
checkNotInternal(executable.getEnclosingElement(), element);
211+
checkPublic(executable.getEnclosingElement(), element);
212+
}
213+
}
214+
}
215+
216+
/**
217+
* Check if a particular executable element should be inspected or not
218+
* @param executable executable element to inspect
219+
* @return {@code true} if a particular executable element should be inspected, {@code false} otherwise
220+
*/
221+
private boolean inspectable(ExecutableElement executable) {
222+
// The constructors for public APIs could use non-public APIs when those are supposed to be only
223+
// consumed (not instantiated) by external consumers.
224+
return executable.getKind() != ElementKind.CONSTRUCTOR && executable.getModifiers().contains(Modifier.PUBLIC);
225+
}
226+
227+
/**
228+
* Check if a particular element should be inspected or not
229+
* @param element element to inspect
230+
* @return {@code true} if a particular element should be inspected, {@code false} otherwise
231+
*/
232+
private boolean inspectable(Element element) {
233+
final PackageElement pckg = processingEnv.getElementUtils().getPackageOf(element);
234+
return pckg.getQualifiedName().toString().startsWith(OPENSEARCH_PACKAGE);
235+
}
236+
237+
/**
238+
* Check if a particular element belongs to OpenSeach managed packages
239+
* @param element element to inspect
240+
* @return {@code true} if a particular element belongs to OpenSeach managed packages, {@code false} otherwise
241+
*/
242+
private boolean checkPackage(Element element) {
243+
// The element was reported already
244+
if (reported.contains(element)) {
245+
return false;
246+
}
247+
248+
final PackageElement pckg = processingEnv.getElementUtils().getPackageOf(element);
249+
final boolean belongsToOpenSearch = pckg.getQualifiedName().toString().startsWith(OPENSEARCH_PACKAGE);
250+
251+
if (!belongsToOpenSearch) {
252+
reported.add(element);
253+
254+
processingEnv.getMessager()
255+
.printMessage(
256+
reportFailureAs,
257+
"The type "
258+
+ element
259+
+ " is not residing in "
260+
+ OPENSEARCH_PACKAGE
261+
+ ".* package "
262+
+ "and should not be annotated as OpenSearch APIs."
263+
);
264+
}
265+
266+
return belongsToOpenSearch;
267+
}
268+
269+
/**
270+
* Check the fields, methods, constructors, and member types that are directly
271+
* declared in this class or interface.
272+
* @param element class or interface
273+
*/
274+
private void process(Element element) {
275+
// Check the fields, methods, constructors, and member types that are directly
276+
// declared in this class or interface.
277+
for (final Element enclosed : element.getEnclosedElements()) {
278+
// Skip all not-public elements
279+
if (!enclosed.getModifiers().contains(Modifier.PUBLIC)) {
280+
continue;
281+
}
282+
283+
if (enclosed instanceof ExecutableElement) {
284+
process((ExecutableElement) enclosed, element);
285+
}
286+
}
287+
}
288+
289+
/**
290+
* Check if element is public and annotated with {@link PublicApi}, {@link DeprecatedApi} or {@link ExperimentalApi}
291+
* @param referencedBy the referrer for the element
292+
* @param element element to check
293+
*/
294+
private void checkPublic(@Nullable Element referencedBy, final Element element) {
295+
// The element was reported already
296+
if (reported.contains(element)) {
297+
return;
298+
}
299+
300+
if (!element.getModifiers().contains(Modifier.PUBLIC)) {
301+
reported.add(element);
302+
303+
processingEnv.getMessager()
304+
.printMessage(
305+
reportFailureAs,
306+
"The element "
307+
+ element
308+
+ " is part of the public APIs but does not have public visibility"
309+
+ ((referencedBy != null) ? " (referenced by " + referencedBy + ") " : "")
310+
);
311+
}
312+
313+
if (element.getAnnotation(PublicApi.class) == null
314+
&& element.getAnnotation(ExperimentalApi.class) == null
315+
&& element.getAnnotation(DeprecatedApi.class) == null) {
316+
reported.add(element);
317+
318+
processingEnv.getMessager()
319+
.printMessage(
320+
reportFailureAs,
321+
"The element "
322+
+ element
323+
+ " is part of the public APIs but is not maked as @PublicApi, @ExperimentalApi or @DeprecatedApi"
324+
+ ((referencedBy != null) ? " (referenced by " + referencedBy + ") " : "")
325+
);
326+
}
327+
}
328+
329+
/**
330+
* Check if element is not annotated with {@link InternalApi}
331+
* @param referencedBy the referrer for the element
332+
* @param element element to check
333+
*/
334+
private void checkNotInternal(@Nullable Element referencedBy, final Element element) {
335+
// The element was reported already
336+
if (reported.contains(element)) {
337+
return;
338+
}
339+
340+
if (element.getAnnotation(InternalApi.class) != null) {
341+
reported.add(element);
342+
343+
processingEnv.getMessager()
344+
.printMessage(
345+
reportFailureAs,
346+
"The element "
347+
+ element
348+
+ " is part of the public APIs but is marked as @InternalApi"
349+
+ ((referencedBy != null) ? " (referenced by " + referencedBy + ") " : "")
350+
);
351+
}
352+
}
353+
}
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)