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

Java 10+ Local-Variable Type Inference #218

Merged
merged 24 commits into from
Jun 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0bcc1cb
Add test and dummy recipe as baseline for https://github.com/openrewr…
MBoegers May 3, 2023
3c41bca
Add tests for primitives and var keywords
MBoegers May 3, 2023
862f92e
Refine and group tests as nested tests
MBoegers May 9, 2023
937ba31
Merge branch 'openrewrite:main' into 217-usage_of_var
MBoegers May 10, 2023
4bd4a60
Refine and group tests as nested tests
MBoegers May 10, 2023
8984bb5
Implement UseVarKeyword reciepe except for generics
MBoegers May 10, 2023
0180123
Add Todo to implement generics and explicitly skip them for the moment
MBoegers May 10, 2023
8926e0c
UseVarKeyword: rename variables and improve null-checking in type che…
MBoegers May 12, 2023
208fc71
Add handling of static and instance initializer blocks
MBoegers May 24, 2023
f1b73f8
add skipping of generics types
MBoegers May 24, 2023
55a3d4b
replace
MBoegers May 24, 2023
78876d8
Merge branch 'openrewrite:main' into 217-usage_of_var
MBoegers Jun 14, 2023
57ce08d
Extract handling of primitive variable definition for local variable …
MBoegers Jun 14, 2023
856dbd9
Extract handling of Object variable definition for local variable typ…
MBoegers Jun 14, 2023
0c92217
Add Recipe that combines Object and Primitive handling for var. Refac…
MBoegers Jun 14, 2023
019a7a0
add licences to source code
MBoegers Jun 14, 2023
a8d4c4f
Add DocumentedExample to tests and remove Examples from Recipe
MBoegers Jun 16, 2023
5b8f0da
simplify null handling and reorder hot paths
MBoegers Jun 16, 2023
38bb55c
Apply suggestions from code review
MBoegers Jun 16, 2023
d96566b
remove NotNull Annotaions
MBoegers Jun 16, 2023
44ee158
rework determination if inside method and add test for edgecase
MBoegers Jun 16, 2023
6c8ff8b
use configuration UseJavaVersion
MBoegers Jun 16, 2023
0a47c80
Update license header
MBoegers Jun 16, 2023
4c485cb
Merge branch 'openrewrite:main' into 217-usage_of_var
MBoegers Jun 16, 2023
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
/*
* Copyright 2023 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.migrate.lang.var;

import static java.util.Objects.requireNonNull;

import org.openrewrite.Cursor;
import org.openrewrite.java.tree.*;

final class DeclarationCheck {

private DeclarationCheck() {

}

/**
* Determine if var is applicable with regard to location and decleation type.
* <p>
* Var is applicable inside methods and initializer blocks for single variable definition.
* Var is *not* applicable to method definitions.
*
* @param cursor location of the visitor
* @param vd variable definition at question
* @return true if var is applicable in general
*/
public static boolean isVarApplicable(Cursor cursor, J.VariableDeclarations vd) {
boolean isMethodParameter = DeclarationCheck.isMethodParameter(vd, cursor);
boolean isMultiVarDefinition = !DeclarationCheck.isSingleVariableDefinition(vd);
boolean useTernary = DeclarationCheck.initializedByTernary(vd);
if (isMethodParameter || isMultiVarDefinition || useTernary) return false;

boolean isInsideMethod = DeclarationCheck.isInsideMethod(cursor);
boolean isInsideInitializer = DeclarationCheck.isInsideInitializer(cursor, 0);
return isInsideMethod || isInsideInitializer;
}

/**
* Determine if a variable definition defines a single variable that is directly initialized with value different from null, which not make use of var.
*
* @param vd variable definition at hand
* @return true if single variable definition with initialization and without var
*/
private static boolean isSingleVariableDefinition(J.VariableDeclarations vd) {
TypeTree typeExpression = vd.getTypeExpression();

boolean definesSingleVariable = vd.getVariables().size() == 1;
boolean isPureAssigment = JavaType.Primitive.Null.equals(vd.getType());
if (!definesSingleVariable || isPureAssigment) return false;

Expression initializer = vd.getVariables().get(0).getInitializer();
boolean isDeclarationOnly = initializer == null;
if (isDeclarationOnly) return false;

initializer = initializer.unwrap();
boolean isNullAssigment = initializer instanceof J.Literal && ((J.Literal) initializer).getValue() == null;
boolean alreadyUseVar = typeExpression instanceof J.Identifier && "var".equals(((J.Identifier) typeExpression).getSimpleName());
return !isNullAssigment && !alreadyUseVar;
}

/**
* Determine whether the variable declaration at hand defines a primitive variable
*
* @param vd variable declaration at hand
* @return true iff declares primitive type
*/
public static boolean isPrimitive(J.VariableDeclarations vd) {
TypeTree typeExpression = vd.getTypeExpression();
return typeExpression instanceof J.Primitive;
}

/**
* Checks whether the variable declaration at hand has the type
*
* @param vd variable declaration at hand
* @param type type in question
* @return true iff the declaration has a matching type definition
*/
public static boolean declarationHasType(J.VariableDeclarations vd, JavaType type) {
TypeTree typeExpression = vd.getTypeExpression();
return typeExpression != null && type.equals(typeExpression.getType());
}

/**
* Determine whether the definition or the initializer uses generics types
*
* @param vd variable definition at hand
* @return true if definition or initializer uses generic types
*/
public static boolean useGenerics(J.VariableDeclarations vd) {
TypeTree typeExpression = vd.getTypeExpression();
boolean isGenericDefinition = typeExpression instanceof J.ParameterizedType;
if (isGenericDefinition) return true;

Expression initializer = vd.getVariables().get(0).getInitializer();
if (initializer == null) return false;
initializer = initializer.unwrap();

return initializer instanceof J.NewClass
&& ((J.NewClass) initializer).getClazz() instanceof J.ParameterizedType;
}

/**
* Determin if the initilizer uses the ternary operator <code>Expression ? if-then : else</code>
*
* @param vd variable declaration at hand
* @return true iff the ternary operator is used in the initialization
*/
public static boolean initializedByTernary(J.VariableDeclarations vd) {
Expression initializer = vd.getVariables().get(0).getInitializer();
return initializer != null && initializer.unwrap() instanceof J.Ternary;
}

/**
* Determines if a cursor is contained inside a Method declaration without an intermediate Class declaration
*
* @param cursor value to determine
*/
private static boolean isInsideMethod(Cursor cursor) {
Object value = cursor
.dropParentUntil(p -> p instanceof J.MethodDeclaration || p instanceof J.ClassDeclaration|| p.equals(Cursor.ROOT_VALUE))
.getValue();

boolean isNotRoot = !Cursor.ROOT_VALUE.equals(value);
boolean isNotClassDeclaration = !(value instanceof J.ClassDeclaration);
boolean isMethodDeclaration = value instanceof J.MethodDeclaration;

return isNotRoot && isNotClassDeclaration && isMethodDeclaration;
}

/**
* Determine if the variable declaration at hand is part of a method declaration
*
* @param vd variable declaration to check
* @param cursor current location
* @return true iff vd is part of a method declaration
*/
private static boolean isMethodParameter(J.VariableDeclarations vd, Cursor cursor) {
J.MethodDeclaration methodDeclaration = cursor.firstEnclosing(J.MethodDeclaration.class);
return methodDeclaration != null && methodDeclaration.getParameters().contains(vd);
}

/**
* Determine if the visitors location is inside an instance or static initializer block
*
* @param cursor visitors location
* @param nestedBlockLevel number of blocks, default for start 0
* @return true iff the courser is inside an instance or static initializer block
*/
private static boolean isInsideInitializer(Cursor cursor, int nestedBlockLevel) {
if (Cursor.ROOT_VALUE.equals(cursor.getValue())) return false;

Object currentStatement = cursor.getValue();

// initializer blocks are blocks inside the class definition block, therefor a nesting of 2 is mandatory
boolean isClassDeclaration = currentStatement instanceof J.ClassDeclaration;
boolean followedByTwoBlock = nestedBlockLevel >= 2;
if (isClassDeclaration && followedByTwoBlock) return true;

// count direct block nesting (block containing a block), but ignore paddings
boolean isBlock = currentStatement instanceof J.Block;
boolean isNoPadding = !(currentStatement instanceof JRightPadded);
if (isBlock) nestedBlockLevel += 1;
else if (isNoPadding) nestedBlockLevel = 0;

return isInsideInitializer(requireNonNull(cursor.getParent()), nestedBlockLevel);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Copyright 2023 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.migrate.lang.var;

import org.openrewrite.ExecutionContext;
import org.openrewrite.Preconditions;
import org.openrewrite.Recipe;
import org.openrewrite.TreeVisitor;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.JavaParser;
import org.openrewrite.java.JavaTemplate;
import org.openrewrite.java.search.UsesJavaVersion;
import org.openrewrite.java.tree.Expression;
import org.openrewrite.java.tree.J;

import lombok.EqualsAndHashCode;
import lombok.Value;

@Value
@EqualsAndHashCode(callSuper = false)
public class UseVarForObject extends Recipe {

@Override
public String getDisplayName() {
//language=markdown
return "Use `var` for reference-typed variables";
}


@Override
public String getDescription() {
//language=markdown
return "Try to apply local variable type inference `var` to variables containing Objects where possible." +
"This recipe will not touch variable declaration with genrics or initializer containing ternary operators.";
}


@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return Preconditions.check(
new UsesJavaVersion<>(10),
new UseVarForObjectVisitor());
}


static final class UseVarForObjectVisitor extends JavaIsoVisitor<ExecutionContext> {
private final JavaTemplate template = JavaTemplate.builder("var #{} = #{any()}")
.javaParser(JavaParser.fromJavaVersion()).build();


@Override
public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations vd, ExecutionContext ctx) {
vd = super.visitVariableDeclarations(vd, ctx);

boolean isGeneralApplicable = DeclarationCheck.isVarApplicable(this.getCursor(), vd);
if (!isGeneralApplicable) return vd;

boolean isPrimitive = DeclarationCheck.isPrimitive(vd);
boolean usesGenerics = DeclarationCheck.useGenerics(vd);
boolean usesTernary = DeclarationCheck.initializedByTernary(vd);
if (isPrimitive || usesGenerics || usesTernary) return vd;

return transformToVar(vd);
}


private J.VariableDeclarations transformToVar(J.VariableDeclarations vd) {
Expression initializer = vd.getVariables().get(0).getInitializer();
String simpleName = vd.getVariables().get(0).getSimpleName();

return template.apply(this.getCursor(), vd.getCoordinates().replace(), simpleName, initializer);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
* Copyright 2023 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.migrate.lang.var;

import static java.lang.String.format;

import org.openrewrite.ExecutionContext;
import org.openrewrite.Preconditions;
import org.openrewrite.Recipe;
import org.openrewrite.TreeVisitor;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.JavaParser;
import org.openrewrite.java.JavaTemplate;
import org.openrewrite.java.search.UsesJavaVersion;
import org.openrewrite.java.tree.Expression;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.JavaType;

import lombok.EqualsAndHashCode;
import lombok.Value;

@Value
@EqualsAndHashCode(callSuper = false)
public class UseVarForPrimitive extends Recipe {

@Override
public String getDisplayName() {
//language=markdown
return "Use `var` for primitive-typed variables";
}


@Override
public String getDescription() {
//language=markdown
return "Try to apply local variable type inference `var` to primitiv variables where possible." +
"This recipe will not touch variable declaration with initializer containing ternary operators.";
}


@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return Preconditions.check(
new UsesJavaVersion<>(10),
new VarForPrimitivesVisitor());
}

static final class VarForPrimitivesVisitor extends JavaIsoVisitor<ExecutionContext> {

private final JavaType.Primitive SHORT_TYPE = JavaType.Primitive.Short;
private final JavaType.Primitive BYTE_TYPE = JavaType.Primitive.Byte;

private final JavaTemplate template = JavaTemplate.builder("var #{} = #{any()}")
.javaParser(JavaParser.fromJavaVersion()).build();


@Override
public J.VariableDeclarations visitVariableDeclarations( J.VariableDeclarations vd, ExecutionContext ctx) {
vd = super.visitVariableDeclarations(vd, ctx);

boolean isGeneralApplicable = DeclarationCheck.isVarApplicable(this.getCursor(), vd);
if (!isGeneralApplicable) return vd;

// recipe specific
boolean isNoPrimitive = !DeclarationCheck.isPrimitive(vd);
boolean isByteVariable = DeclarationCheck.declarationHasType(vd, BYTE_TYPE);
boolean isShortVariable = DeclarationCheck.declarationHasType(vd, SHORT_TYPE);
if (isNoPrimitive || isByteVariable || isShortVariable) return vd;

return transformToVar(vd);
}


private J.VariableDeclarations transformToVar( J.VariableDeclarations vd) {
Expression initializer = vd.getVariables().get(0).getInitializer();
String simpleName = vd.getVariables().get(0).getSimpleName();

if (initializer instanceof J.Literal) {
initializer = expandWithPrimitivTypeHint(vd, initializer);
}

return template.apply(this.getCursor(), vd.getCoordinates().replace(), simpleName, initializer);
}


private Expression expandWithPrimitivTypeHint( J.VariableDeclarations vd, Expression initializer) {
String valueSource = ((J.Literal) initializer).getValueSource();

if (valueSource == null) return initializer;

boolean isLongLiteral = JavaType.Primitive.Long.equals(vd.getType());
boolean inferredAsLong = valueSource.endsWith("l") || valueSource.endsWith("L");
boolean isFloatLiteral = JavaType.Primitive.Float.equals(vd.getType());
boolean inferredAsFloat = valueSource.endsWith("f") || valueSource.endsWith("F");
boolean isDoubleLiteral = JavaType.Primitive.Double.equals(vd.getType());
boolean inferredAsDouble = valueSource.endsWith("d") || valueSource.endsWith("D") || valueSource.contains(".");

String typNotation = null;
if (isLongLiteral && !inferredAsLong) {
typNotation = "L";
} else if (isFloatLiteral && !inferredAsFloat) {
typNotation = "F";
} else if (isDoubleLiteral && !inferredAsDouble) {
typNotation = "D";
}

if (typNotation != null) {
initializer = ((J.Literal) initializer).withValueSource(format("%s%s", valueSource, typNotation));
}

return initializer;
}
}
}
Loading