Skip to content

Commit

Permalink
[BWC and API enforcement] Introduce checks for enforcing the API rest…
Browse files Browse the repository at this point in the history
…rictions

Signed-off-by: Andriy Redko <[email protected]>
  • Loading branch information
reta committed Nov 13, 2023
1 parent c676479 commit 8bc186f
Show file tree
Hide file tree
Showing 10 changed files with 593 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
- Add cluster state stats ([#10670](https://github.com/opensearch-project/OpenSearch/pull/10670))
- Adding slf4j license header to LoggerMessageFormat.java ([#11069](https://github.com/opensearch-project/OpenSearch/pull/11069))
- [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))
- [BWC and API enforcement] Introduce checks for enforcing the API restrictions ([#11175](https://github.com/opensearch-project/OpenSearch/pull/11175))

### Dependencies
- 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))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,341 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/

package org.opensearch.common.annotation.processor;

import org.opensearch.common.Nullable;
import org.opensearch.common.annotation.DeprecatedApi;
import org.opensearch.common.annotation.ExperimentalApi;
import org.opensearch.common.annotation.InternalApi;
import org.opensearch.common.annotation.PublicApi;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.AnnotatedConstruct;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.ReferenceType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.TypeVariable;
import javax.lang.model.type.WildcardType;
import javax.tools.Diagnostic.Kind;

import java.util.HashSet;
import java.util.Set;

/**
* The annotation processor for API related annotations: {@link DeprecatedApi}, {@link ExperimentalApi},
* {@link InternalApi} and {@link PublicApi}.
* <p>
* The checks are built on top of the following rules:
* <ul>
* <li>introspect each type annotated with {@link PublicApi}, {@link DeprecatedApi} or {@link ExperimentalApi},
* filtering out package-private declarations</li>
* <li>make sure those leak only {@link PublicApi}, {@link DeprecatedApi} or {@link ExperimentalApi} types as well (exceptions,
* method return values, method arguments, method generic type arguments, class generic type arguments, annotations)</li>
* <li>recursively follow the type introspection chains to enforce the rules down the line</li>
* </ul>
*/
@InternalApi
@SupportedAnnotationTypes("org.opensearch.common.annotation.*")
public class ApiAnnotationProcessor extends AbstractProcessor {
private static final String OPENSEARCH_PACKAGE = "org.opensearch";

private final Set<Element> reported = new HashSet<>();
private final Set<AnnotatedConstruct> processed = new HashSet<>();

@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latest();
}

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment round) {
processingEnv.getMessager().printMessage(Kind.NOTE, "Processing OpenSearch Api annotations");

final Set<? extends Element> elements = round.getElementsAnnotatedWithAny(
Set.of(PublicApi.class, ExperimentalApi.class, DeprecatedApi.class)
);

for (var element : elements) {
if (!checkPackage(element)) {
continue;

Check warning on line 78 in libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java

View check run for this annotation

Codecov / codecov/patch

libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java#L78

Added line #L78 was not covered by tests
}

// Skip all not-public elements
if (!element.getModifiers().contains(Modifier.PUBLIC)) {
continue;

Check warning on line 83 in libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java

View check run for this annotation

Codecov / codecov/patch

libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java#L83

Added line #L83 was not covered by tests
}

if (element instanceof TypeElement) {
process((TypeElement) element);
}
}

return false;
}

/**
* Check top level executable element
* @param executable top level executable element
*/
private void process(ExecutableElement executable) {
if (!inspectable(executable)) {
return;
}

// The executable element should not be internal (unless constructor for injectable core component)
checkNotInternal(null, executable);

// Process method return types
final TypeMirror returnType = executable.getReturnType();
if (returnType instanceof ReferenceType) {
process(executable, (ReferenceType) returnType);

Check warning on line 109 in libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java

View check run for this annotation

Codecov / codecov/patch

libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java#L109

Added line #L109 was not covered by tests
}

// Process method thrown types
for (final TypeMirror thrownType : executable.getThrownTypes()) {
if (thrownType instanceof ReferenceType) {
process(executable, (ReferenceType) thrownType);

Check warning on line 115 in libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java

View check run for this annotation

Codecov / codecov/patch

libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java#L115

Added line #L115 was not covered by tests
}
}

Check warning on line 117 in libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java

View check run for this annotation

Codecov / codecov/patch

libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java#L117

Added line #L117 was not covered by tests

// Process method type parameters
for (final TypeParameterElement typeParameter : executable.getTypeParameters()) {
for (final TypeMirror boundType : typeParameter.getBounds()) {
if (boundType instanceof ReferenceType) {
process(executable, (ReferenceType) boundType);

Check warning on line 123 in libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java

View check run for this annotation

Codecov / codecov/patch

libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java#L123

Added line #L123 was not covered by tests
}
}
}

Check warning on line 126 in libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java

View check run for this annotation

Codecov / codecov/patch

libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java#L125-L126

Added lines #L125 - L126 were not covered by tests

// Process method arguments
for (final VariableElement parameter : executable.getParameters()) {
final TypeMirror parameterType = parameter.asType();
if (parameterType instanceof ReferenceType) {
process(executable, (ReferenceType) parameterType);
}
}
}

/**
* Check wildcard type bounds referred by an element
* @param executable element
* @param type wildcard type
*/
private void process(ExecutableElement executable, WildcardType type) {
if (type.getExtendsBound() instanceof ReferenceType) {
process(executable, (ReferenceType) type.getExtendsBound());

Check warning on line 144 in libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java

View check run for this annotation

Codecov / codecov/patch

libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java#L144

Added line #L144 was not covered by tests
}

if (type.getSuperBound() instanceof ReferenceType) {
process(executable, (ReferenceType) type.getSuperBound());

Check warning on line 148 in libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java

View check run for this annotation

Codecov / codecov/patch

libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java#L148

Added line #L148 was not covered by tests
}
}

Check warning on line 150 in libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java

View check run for this annotation

Codecov / codecov/patch

libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java#L150

Added line #L150 was not covered by tests

/**
* Check reference type bounds referred by an executable element
* @param executable executable element
* @param ref reference type
*/
private void process(ExecutableElement executable, ReferenceType ref) {
// The element has been processed already
if (processed.add(ref) == false) {
return;

Check warning on line 160 in libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java

View check run for this annotation

Codecov / codecov/patch

libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java#L160

Added line #L160 was not covered by tests
}

if (ref instanceof DeclaredType) {
final DeclaredType declaredType = (DeclaredType) ref;

final Element element = declaredType.asElement();
if (inspectable(element)) {
checkNotInternal(executable.getEnclosingElement(), element);
checkPublic(executable.getEnclosingElement(), element);
}

for (final TypeMirror type : declaredType.getTypeArguments()) {
if (type instanceof ReferenceType) {
process(executable, (ReferenceType) type);

Check warning on line 174 in libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java

View check run for this annotation

Codecov / codecov/patch

libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java#L174

Added line #L174 was not covered by tests
} else if (type instanceof WildcardType) {
process(executable, (WildcardType) type);

Check warning on line 176 in libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java

View check run for this annotation

Codecov / codecov/patch

libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java#L176

Added line #L176 was not covered by tests
}
}

Check warning on line 178 in libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java

View check run for this annotation

Codecov / codecov/patch

libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java#L178

Added line #L178 was not covered by tests
} else if (ref instanceof ArrayType) {
final TypeMirror componentType = ((ArrayType) ref).getComponentType();

Check warning on line 180 in libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java

View check run for this annotation

Codecov / codecov/patch

libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java#L180

Added line #L180 was not covered by tests
if (componentType instanceof ReferenceType) {
process(executable, (ReferenceType) componentType);

Check warning on line 182 in libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java

View check run for this annotation

Codecov / codecov/patch

libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java#L182

Added line #L182 was not covered by tests
}
} else if (ref instanceof TypeVariable) {
final TypeVariable typeVariable = (TypeVariable) ref;

Check warning on line 185 in libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java

View check run for this annotation

Codecov / codecov/patch

libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java#L185

Added line #L185 was not covered by tests
if (typeVariable.getUpperBound() instanceof ReferenceType) {
process(executable, (ReferenceType) typeVariable.getUpperBound());

Check warning on line 187 in libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java

View check run for this annotation

Codecov / codecov/patch

libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java#L187

Added line #L187 was not covered by tests
}
if (typeVariable.getLowerBound() instanceof ReferenceType) {
process(executable, (ReferenceType) typeVariable.getLowerBound());

Check warning on line 190 in libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java

View check run for this annotation

Codecov / codecov/patch

libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java#L190

Added line #L190 was not covered by tests
}
}

// Check this elements annotations
for (final AnnotationMirror annotation : ref.getAnnotationMirrors()) {
final Element element = annotation.getAnnotationType().asElement();

Check warning on line 196 in libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java

View check run for this annotation

Codecov / codecov/patch

libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java#L196

Added line #L196 was not covered by tests
if (inspectable(element)) {
checkNotInternal(executable.getEnclosingElement(), element);
checkPublic(executable.getEnclosingElement(), element);

Check warning on line 199 in libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java

View check run for this annotation

Codecov / codecov/patch

libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java#L198-L199

Added lines #L198 - L199 were not covered by tests
}
}

Check warning on line 201 in libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java

View check run for this annotation

Codecov / codecov/patch

libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java#L201

Added line #L201 was not covered by tests
}

/**
* Check if a particular executable element should be inspected or not
* @param executable executable element to inspect
* @return {@code true} if a particular executable element should be inspected, {@code false} otherwise
*/
private boolean inspectable(ExecutableElement executable) {
// The constructors for public APIs could use non-public APIs when those are supposed to be only
// consumed (not instantiated) by external consumers.
return executable.getKind() != ElementKind.CONSTRUCTOR && executable.getModifiers().contains(Modifier.PUBLIC);
}

/**
* Check if a particular element should be inspected or not
* @param element element to inspect
* @return {@code true} if a particular element should be inspected, {@code false} otherwise
*/
private boolean inspectable(Element element) {
final PackageElement pckg = processingEnv.getElementUtils().getPackageOf(element);
return pckg.getQualifiedName().toString().startsWith(OPENSEARCH_PACKAGE);
}

/**
* Check if a particular element belongs to OpenSeach managed packages
* @param element element to inspect
* @return {@code true} if a particular element belongs to OpenSeach managed packages, {@code false} otherwise
*/
private boolean checkPackage(Element element) {
// The element was reported already
if (reported.contains(element)) {
return false;

Check warning on line 233 in libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java

View check run for this annotation

Codecov / codecov/patch

libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java#L233

Added line #L233 was not covered by tests
}

final PackageElement pckg = processingEnv.getElementUtils().getPackageOf(element);
final boolean belongsToOpenSearch = pckg.getQualifiedName().toString().startsWith(OPENSEARCH_PACKAGE);

if (!belongsToOpenSearch) {
reported.add(element);

Check warning on line 240 in libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java

View check run for this annotation

Codecov / codecov/patch

libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java#L240

Added line #L240 was not covered by tests

processingEnv.getMessager()
.printMessage(

Check warning on line 243 in libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java

View check run for this annotation

Codecov / codecov/patch

libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java#L242-L243

Added lines #L242 - L243 were not covered by tests
Kind.ERROR,
"The type "
+ element
+ " is not residing in "
+ OPENSEARCH_PACKAGE
+ ".* package "
+ "and should not be annotated as OpenSearch APIs."
);
}

return belongsToOpenSearch;
}

/**
* Check the fields, methods, constructors, and member types that are directly
* declared in this class or interface.
* @param type class or interface
*/
private void process(Element type) {
// Check the fields, methods, constructors, and member types that are directly
// declared in this class or interface.
for (final Element element : type.getEnclosedElements()) {
// Skip all not-public elements
if (!type.getModifiers().contains(Modifier.PUBLIC)) {
continue;

Check warning on line 268 in libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java

View check run for this annotation

Codecov / codecov/patch

libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java#L268

Added line #L268 was not covered by tests
}

if (element instanceof ExecutableElement) {
process((ExecutableElement) element);
}
}
}

/**
* Check if element is public and annotated with {@link PublicApi}, {@link DeprecatedApi} or {@link ExperimentalApi}
* @param referencedBy the referrer for the element
* @param element element to check
*/
private void checkPublic(@Nullable Element referencedBy, final Element element) {
// The element was reported already
if (reported.contains(element)) {
return;

Check warning on line 285 in libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java

View check run for this annotation

Codecov / codecov/patch

libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java#L285

Added line #L285 was not covered by tests
}

if (!element.getModifiers().contains(Modifier.PUBLIC)) {
reported.add(element);

processingEnv.getMessager()
.printMessage(
Kind.ERROR,
"The element "
+ element
+ " is part of the public APIs but does not have public visibility"
+ ((referencedBy != null) ? " (referenced by " + referencedBy + ") " : "")
);
}

if (element.getAnnotation(PublicApi.class) == null
&& element.getAnnotation(ExperimentalApi.class) == null
&& element.getAnnotation(DeprecatedApi.class) == null) {
reported.add(element);

processingEnv.getMessager()
.printMessage(
Kind.ERROR,
"The element "
+ element
+ " is part of the public APIs but is not maked as @PublicApi, @ExperimentalApi or @DeprecatedApi"
+ ((referencedBy != null) ? " (referenced by " + referencedBy + ") " : "")
);
}
}

/**
* Check if element is not annotated with {@link InternalApi}
* @param referencedBy the referrer for the element
* @param element element to check
*/
private void checkNotInternal(@Nullable Element referencedBy, final Element element) {
// The element was reported already
if (reported.contains(element)) {
return;

Check warning on line 325 in libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java

View check run for this annotation

Codecov / codecov/patch

libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java#L325

Added line #L325 was not covered by tests
}

if (element.getAnnotation(InternalApi.class) != null) {
reported.add(element);

Check warning on line 329 in libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java

View check run for this annotation

Codecov / codecov/patch

libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java#L329

Added line #L329 was not covered by tests

processingEnv.getMessager()
.printMessage(

Check warning on line 332 in libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java

View check run for this annotation

Codecov / codecov/patch

libs/common/src/main/java/org/opensearch/common/annotation/processor/ApiAnnotationProcessor.java#L331-L332

Added lines #L331 - L332 were not covered by tests
Kind.ERROR,
"The element "
+ element
+ " is part of the public APIs but is marked as @InternalApi"
+ ((referencedBy != null) ? " (referenced by " + referencedBy + ") " : "")
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/

/**
* Classes related yo OpenSearch API annotation processing
*
* @opensearch.internal
*/
@org.opensearch.common.annotation.InternalApi
package org.opensearch.common.annotation.processor;
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#
# SPDX-License-Identifier: Apache-2.0
#
# The OpenSearch Contributors require contributions made to
# this file be licensed under the Apache-2.0 license or a
# compatible open source license.
#
# Modifications Copyright OpenSearch Contributors. See
# GitHub history for details.
#

org.opensearch.common.annotation.processor.ApiAnnotationProcessor
Loading

0 comments on commit 8bc186f

Please sign in to comment.