Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Annotated and Literal traits #4318

Merged
merged 3 commits into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading