Skip to content

Commit

Permalink
Annotated and Literal traits (openrewrite#4318)
Browse files Browse the repository at this point in the history
  • Loading branch information
jkschneider authored Jul 10, 2024
1 parent 7a34304 commit 69addbf
Show file tree
Hide file tree
Showing 8 changed files with 455 additions and 2 deletions.
13 changes: 13 additions & 0 deletions rewrite-core/src/main/java/org/openrewrite/trait/TraitMatcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,19 @@
@Incubating(since = "8.30.0")
public interface TraitMatcher<U extends Trait<?>> {

default U require(Tree tree, Cursor parent) {
return require(new Cursor(parent, tree));
}

default U require(Cursor cursor) {
return get(cursor).orElseThrow(() ->
new IllegalStateException("Expected this cursor to match the trait"));
}

default Optional<U> get(Tree tree, Cursor parent) {
return get(new Cursor(parent, tree));
}

/**
* Tests whether a tree at the cursor matches the trait, and if so, returns
* a trait instance containing the semantic information represented by the tree.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Copyright 2024 the original author or authors.
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openrewrite.java.trait;

import lombok.RequiredArgsConstructor;
import lombok.Value;
import org.openrewrite.Cursor;
import org.openrewrite.internal.lang.Nullable;
import org.openrewrite.java.AnnotationMatcher;
import org.openrewrite.java.tree.Expression;
import org.openrewrite.java.tree.J;
import org.openrewrite.trait.SimpleTraitMatcher;
import org.openrewrite.trait.Trait;

import java.util.Optional;

@Value
public class Annotated implements Trait<J.Annotation> {
Cursor cursor;

/**
* @param defaultAlias The name of the annotation attribute that is aliased to
* "value", if any.
* @return The attribute value.
*/
public Optional<Literal> getDefaultAttribute(@Nullable String defaultAlias) {
if (getTree().getArguments() == null) {
return Optional.empty();
}
for (Expression argument : getTree().getArguments()) {
if (!(argument instanceof J.Assignment)) {
return new Literal.Matcher().get(argument, cursor);
}
}
Optional<Literal> valueAttr = getAttribute("value");
if (valueAttr.isPresent()) {
return valueAttr;
}
return defaultAlias != null ?
getAttribute(defaultAlias) :
Optional.empty();
}

public Optional<Literal> getAttribute(String attribute) {
if (getTree().getArguments() == null) {
return Optional.empty();
}
for (Expression argument : getTree().getArguments()) {
if (argument instanceof J.Assignment) {
J.Assignment assignment = (J.Assignment) argument;
if (assignment.getVariable() instanceof J.Identifier) {
J.Identifier identifier = (J.Identifier) assignment.getVariable();
if (identifier.getSimpleName().equals(attribute)) {
return new Literal.Matcher().get(
assignment.getAssignment(),
new Cursor(cursor, argument)
);
}
}
}
}
return Optional.empty();
}

@RequiredArgsConstructor
public static class Matcher extends SimpleTraitMatcher<Annotated> {
private final AnnotationMatcher matcher;

public Matcher(String signature) {
this.matcher = new AnnotationMatcher(signature);
}

@Override
protected @Nullable Annotated test(Cursor cursor) {
Object value = cursor.getValue();
if (value instanceof J.Annotation) {
J.Annotation annotation = (J.Annotation) value;
if (matcher.matches(annotation)) {
return new Annotated(cursor);
}
}
return null;
}
}
}
139 changes: 139 additions & 0 deletions rewrite-java/src/main/java/org/openrewrite/java/trait/Literal.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* Copyright 2024 the original author or authors.
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openrewrite.java.trait;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.openrewrite.Cursor;
import org.openrewrite.internal.lang.Nullable;
import org.openrewrite.java.tree.Expression;
import org.openrewrite.java.tree.J;
import org.openrewrite.trait.SimpleTraitMatcher;
import org.openrewrite.trait.Trait;

import java.util.ArrayList;
import java.util.List;

import static java.util.Collections.singletonList;
import static java.util.Objects.requireNonNull;

/**
* A literal in Java is either a {@link J.Literal} or a {@link J.NewArray}
* with a non-null initializer that itself literals or new arrays that recursively
* contain these constraints. In other languages this trait is inclusive
* of constructs like list or map literals.
*/
@RequiredArgsConstructor
public class Literal implements Trait<Expression> {
@Getter
private final Cursor cursor;

private final ObjectMapper mapper;

public boolean isNull() {
return getTree() instanceof J.Literal && ((J.Literal) getTree()).getValue() == null;
}

public boolean isNotNull() {
return !isNull();
}

public <@Nullable T> T getValue(Class<T> type) {
return getValue(mapper.constructType(type));
}

public <@Nullable T> T getValue(TypeReference<T> type) {
return getValue(mapper.constructType(type));
}

public <@Nullable T> T getValue(JavaType type) {
Expression lit = getTree();
if (lit instanceof J.Literal) {
J.Literal literal = (J.Literal) lit;
if (literal.getValue() == null) {
//noinspection DataFlowIssue
return null;
} else if (type.isCollectionLikeType()) {
List<?> l = singletonList(literal.getValue());
return mapper.convertValue(l, type);
} else {
return mapper.convertValue(literal.getValue(), type);
}
} else if (lit instanceof J.NewArray) {
List<Object> untyped = untypedInitializerLiterals((J.NewArray) lit);
return mapper.convertValue(untyped, type);
}
//noinspection DataFlowIssue
return null;
}

private List<Object> untypedInitializerLiterals(J.NewArray newArray) {
List<Object> acc = new ArrayList<>();
for (Expression init : requireNonNull(newArray.getInitializer())) {
if (init instanceof J.Literal) {
acc.add(((J.Literal) init).getValue());
} else {
acc.add(untypedInitializerLiterals((J.NewArray) init));
}
}
return acc;
}

public static class Matcher extends SimpleTraitMatcher<Literal> {
private static final ObjectMapper DEFAULT_MAPPER = new ObjectMapper();

private ObjectMapper mapper = DEFAULT_MAPPER;

/**
* @param mapper A customized mapper, which should be rare,
* but possibly when you want a custom type factory.
* @return This matcher with a customized mapper set.
*/
public Matcher mapper(ObjectMapper mapper) {
this.mapper = mapper;
return this;
}

@Override
protected @Nullable Literal test(Cursor cursor) {
Object value = cursor.getValue();
return value instanceof J.Literal ||
isNewArrayWithLiteralInitializer(value) ?
new Literal(cursor, mapper) :
null;
}

private boolean isNewArrayWithLiteralInitializer(Object value) {
if (value instanceof J.NewArray) {
List<Expression> init = ((J.NewArray) value).getInitializer();
if (init == null) {
return false;
}
for (Expression expr : init) {
if (!(expr instanceof J.Literal) &&
!isNewArrayWithLiteralInitializer(expr)) {
return false;
}
}
return true;
}
return false;
}
}
}
46 changes: 46 additions & 0 deletions rewrite-java/src/main/java/org/openrewrite/java/trait/Traits.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright 2024 the original author or authors.
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openrewrite.java.trait;

import org.openrewrite.java.AnnotationMatcher;
import org.openrewrite.java.MethodMatcher;

public class Traits {

public static Literal.Matcher literal() {
return new Literal.Matcher();
}

public static VariableAccess.Matcher variableAccess() {
return new VariableAccess.Matcher();
}

public static MethodAccess.Matcher methodAccess(MethodMatcher matcher) {
return new MethodAccess.Matcher(matcher);
}

public static MethodAccess.Matcher methodAccess(String signature) {
return new MethodAccess.Matcher(signature);
}

public static Annotated.Matcher annotated(AnnotationMatcher matcher) {
return new Annotated.Matcher(matcher);
}

public static Annotated.Matcher annotated(String signature) {
return new Annotated.Matcher(signature);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright 2024 the original author or authors.
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openrewrite.java.trait;

import org.junit.jupiter.api.Test;
import org.openrewrite.marker.SearchResult;
import org.openrewrite.test.RewriteTest;

import static org.openrewrite.java.Assertions.java;
import static org.openrewrite.java.trait.Traits.annotated;

class AnnotatedTest implements RewriteTest {

@Test
void attributes() {
rewriteRun(
spec -> spec.recipe(RewriteTest.toRecipe(() ->
annotated("@Example").asVisitor(a -> SearchResult.found(a.getTree(),
a.getDefaultAttribute("name")
.map(lit -> lit.getValue(String.class))
.orElse("unknown"))
)
)),
java(
"""
import java.lang.annotation.Repeatable;
@Repeatable
@interface Example {
String value() default "";
String name() default "";
}
"""
),
java(
"""
@Example("test")
@Example(value = "test")
@Example(name = "test")
class Test {
}
""",
"""
/*~~(test)~~>*/@Example("test")
/*~~(test)~~>*/@Example(value = "test")
/*~~(test)~~>*/@Example(name = "test")
class Test {
}
"""
)
);
}
}
Loading

0 comments on commit 69addbf

Please sign in to comment.