From 97a285d6ab00047c067e58aab91107f25c986566 Mon Sep 17 00:00:00 2001 From: Eric Milles Date: Tue, 28 Sep 2021 11:14:59 -0500 Subject: [PATCH] GROOVY-10178 --- .../core/tests/xform/NullCheckTests.java | 25 ++- base/org.codehaus.groovy30/.checkstyle | 1 + .../transform/NullCheckASTTransformation.java | 153 ++++++++++++++++++ base/org.codehaus.groovy40/.checkstyle | 2 +- .../transform/NullCheckASTTransformation.java | 149 +++++++++++++++++ 5 files changed, 327 insertions(+), 3 deletions(-) create mode 100644 base/org.codehaus.groovy30/src/org/codehaus/groovy/transform/NullCheckASTTransformation.java create mode 100644 base/org.codehaus.groovy40/src/org/codehaus/groovy/transform/NullCheckASTTransformation.java diff --git a/base-test/org.eclipse.jdt.groovy.core.tests.compiler/src/org/eclipse/jdt/groovy/core/tests/xform/NullCheckTests.java b/base-test/org.eclipse.jdt.groovy.core.tests.compiler/src/org/eclipse/jdt/groovy/core/tests/xform/NullCheckTests.java index d20f9c2b17..642924e525 100644 --- a/base-test/org.eclipse.jdt.groovy.core.tests.compiler/src/org/eclipse/jdt/groovy/core/tests/xform/NullCheckTests.java +++ b/base-test/org.eclipse.jdt.groovy.core.tests.compiler/src/org/eclipse/jdt/groovy/core/tests/xform/NullCheckTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2009-2020 the original author or authors. + * Copyright 2009-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -92,8 +92,29 @@ public void testNullCheck2() { runConformTest(sources, "", "java.lang.IllegalArgumentException: whatever cannot be null"); } - @Test + @Test // GROOVY-10178 public void testNullCheck3() { + //@formatter:off + String[] sources = { + "Main.groovy", + "new Pogo().test(null)\n", + + "Pogo.groovy", + "@groovy.transform.CompileStatic\n" + + "class Pogo {\n" + + " @groovy.transform.NullCheck\n" + + " void test(whatever) {\n" + + " print whatever\n" + + " }\n" + + "}\n", + }; + //@formatter:on + + runConformTest(sources, "", "java.lang.IllegalArgumentException: whatever cannot be null"); + } + + @Test + public void testNullCheck4() { //@formatter:off String[] sources = { "Main.groovy", diff --git a/base/org.codehaus.groovy30/.checkstyle b/base/org.codehaus.groovy30/.checkstyle index 1e50b9f93b..2f1ccd902b 100644 --- a/base/org.codehaus.groovy30/.checkstyle +++ b/base/org.codehaus.groovy30/.checkstyle @@ -65,6 +65,7 @@ + diff --git a/base/org.codehaus.groovy30/src/org/codehaus/groovy/transform/NullCheckASTTransformation.java b/base/org.codehaus.groovy30/src/org/codehaus/groovy/transform/NullCheckASTTransformation.java new file mode 100644 index 0000000000..50e4a3bef5 --- /dev/null +++ b/base/org.codehaus.groovy30/src/org/codehaus/groovy/transform/NullCheckASTTransformation.java @@ -0,0 +1,153 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.codehaus.groovy.transform; + +import groovy.transform.NullCheck; +import org.apache.groovy.ast.tools.ConstructorNodeUtils; +import org.codehaus.groovy.ast.ASTNode; +import org.codehaus.groovy.ast.AnnotatedNode; +import org.codehaus.groovy.ast.AnnotationNode; +import org.codehaus.groovy.ast.ClassHelper; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.ConstructorNode; +import org.codehaus.groovy.ast.MethodNode; +import org.codehaus.groovy.ast.Parameter; +import org.codehaus.groovy.ast.expr.ConstantExpression; +import org.codehaus.groovy.ast.expr.ConstructorCallExpression; +import org.codehaus.groovy.ast.expr.Expression; +import org.codehaus.groovy.ast.stmt.BlockStatement; +import org.codehaus.groovy.ast.stmt.ThrowStatement; +import org.codehaus.groovy.classgen.BytecodeSequence; +import org.codehaus.groovy.control.CompilePhase; +import org.codehaus.groovy.control.SourceUnit; + +import java.util.List; + +import static org.apache.groovy.ast.tools.AnnotatedNodeUtils.isGenerated; +import static org.apache.groovy.ast.tools.MethodNodeUtils.getCodeAsBlock; +import static org.codehaus.groovy.ast.tools.GeneralUtils.constX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.ctorX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.ifS; +import static org.codehaus.groovy.ast.tools.GeneralUtils.isNullX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.param; +import static org.codehaus.groovy.ast.tools.GeneralUtils.params; +import static org.codehaus.groovy.ast.tools.GeneralUtils.throwS; +import static org.codehaus.groovy.ast.tools.GeneralUtils.varX; +import static org.codehaus.groovy.transform.stc.StaticTypesMarker.DIRECT_METHOD_CALL_TARGET; + +/** + * Handles generation of code for the @NullCheck annotation. + */ +@GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION) +public class NullCheckASTTransformation extends AbstractASTTransformation { + public static final ClassNode NULL_CHECK_TYPE = ClassHelper.make(NullCheck.class); + private static final String NULL_CHECK_NAME = "@" + NULL_CHECK_TYPE.getNameWithoutPackage(); + private static final ClassNode EXCEPTION = ClassHelper.make(IllegalArgumentException.class); + private static final String NULL_CHECK_IS_PROCESSED = "NullCheck.isProcessed"; + + @Override + public void visit(ASTNode[] nodes, SourceUnit source) { + init(nodes, source); + AnnotatedNode parent = (AnnotatedNode) nodes[1]; + AnnotationNode anno = (AnnotationNode) nodes[0]; + if (!NULL_CHECK_TYPE.equals(anno.getClassNode())) return; + boolean includeGenerated = isIncludeGenerated(anno); + + if (parent instanceof ClassNode) { + ClassNode cNode = (ClassNode) parent; + if (!checkNotInterface(cNode, NULL_CHECK_NAME)) return; + for (ConstructorNode cn : cNode.getDeclaredConstructors()) { + adjustMethod(cn, includeGenerated); + } + for (MethodNode mn : cNode.getAllDeclaredMethods()) { + adjustMethod(mn, includeGenerated); + } + } else if (parent instanceof MethodNode) { + // handles constructor case too + adjustMethod((MethodNode) parent, false); + } + } + + private boolean isIncludeGenerated(AnnotationNode anno) { + return memberHasValue(anno, "includeGenerated", true); + } + + public static boolean hasIncludeGenerated(ClassNode cNode) { + List annotations = cNode.getAnnotations(NULL_CHECK_TYPE); + if (annotations.isEmpty()) return false; + return hasIncludeGenerated(annotations.get(0)); + } + + private static boolean hasIncludeGenerated(AnnotationNode node) { + final Expression member = node.getMember("includeGenerated"); + return member instanceof ConstantExpression && ((ConstantExpression) member).getValue().equals(true); + } + + private void adjustMethod(MethodNode mn, boolean includeGenerated) { + BlockStatement newCode = getCodeAsBlock(mn); + if (mn.getParameters().length == 0) return; + boolean generated = isGenerated(mn); + int startingIndex = 0; + if (!includeGenerated && generated) return; + if (isMarkedAsProcessed(mn)) return; + if (mn instanceof ConstructorNode) { + // some transform has been here already and we assume it knows what it is doing + if (mn.getFirstStatement() instanceof BytecodeSequence) return; + // ignore any constructors calling this(...) or super(...) + ConstructorCallExpression cce = ConstructorNodeUtils.getFirstIfSpecialConstructorCall(mn.getCode()); + if (cce != null) { + if (generated) { + return; + } else { + startingIndex = 1; // skip over this/super() call + } + } + } + for (Parameter p : mn.getParameters()) { + if (ClassHelper.isPrimitiveType(p.getType())) continue; + newCode.getStatements().add(startingIndex, ifS(isNullX(varX(p)), makeThrowStmt(p.getName()))); + } + mn.setCode(newCode); + } + + public static ThrowStatement makeThrowStmt(String name) { + /* GRECLIPSE edit -- GROOVY-10178 + return throwS(ctorX(EXCEPTION, constX(name + " cannot be null"))); + */ + ConstructorCallExpression newException = ctorX(EXCEPTION, constX(name + " cannot be null")); + newException.putNodeMetaData(DIRECT_METHOD_CALL_TARGET, + EXCEPTION.getDeclaredConstructor(params(param(ClassHelper.STRING_TYPE, "s")))); + return throwS(newException); + // GRECLIPSE end + } + + /** + * Mark a method as already processed. + * + * @param mn the method node to be considered already processed + */ + public static void markAsProcessed(MethodNode mn) { + mn.setNodeMetaData(NULL_CHECK_IS_PROCESSED, Boolean.TRUE); + } + + private static boolean isMarkedAsProcessed(MethodNode mn) { + Boolean r = mn.getNodeMetaData(NULL_CHECK_IS_PROCESSED); + return null != r && r; + } +} diff --git a/base/org.codehaus.groovy40/.checkstyle b/base/org.codehaus.groovy40/.checkstyle index 7a99b37422..393dcc7511 100644 --- a/base/org.codehaus.groovy40/.checkstyle +++ b/base/org.codehaus.groovy40/.checkstyle @@ -55,7 +55,7 @@ - + diff --git a/base/org.codehaus.groovy40/src/org/codehaus/groovy/transform/NullCheckASTTransformation.java b/base/org.codehaus.groovy40/src/org/codehaus/groovy/transform/NullCheckASTTransformation.java new file mode 100644 index 0000000000..3f1a2dac1b --- /dev/null +++ b/base/org.codehaus.groovy40/src/org/codehaus/groovy/transform/NullCheckASTTransformation.java @@ -0,0 +1,149 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.codehaus.groovy.transform; + +import groovy.transform.NullCheck; +import org.apache.groovy.ast.tools.ConstructorNodeUtils; +import org.codehaus.groovy.ast.ASTNode; +import org.codehaus.groovy.ast.AnnotatedNode; +import org.codehaus.groovy.ast.AnnotationNode; +import org.codehaus.groovy.ast.ClassHelper; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.ConstructorNode; +import org.codehaus.groovy.ast.MethodNode; +import org.codehaus.groovy.ast.Parameter; +import org.codehaus.groovy.ast.expr.ConstantExpression; +import org.codehaus.groovy.ast.expr.ConstructorCallExpression; +import org.codehaus.groovy.ast.expr.Expression; +import org.codehaus.groovy.ast.stmt.BlockStatement; +import org.codehaus.groovy.ast.stmt.ThrowStatement; +import org.codehaus.groovy.classgen.BytecodeSequence; +import org.codehaus.groovy.control.CompilePhase; +import org.codehaus.groovy.control.SourceUnit; + +import java.util.List; + +import static org.apache.groovy.ast.tools.AnnotatedNodeUtils.isGenerated; +import static org.apache.groovy.ast.tools.MethodNodeUtils.getCodeAsBlock; +import static org.codehaus.groovy.ast.tools.GeneralUtils.constX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.ctorX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.ifS; +import static org.codehaus.groovy.ast.tools.GeneralUtils.isNullX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.param; +import static org.codehaus.groovy.ast.tools.GeneralUtils.params; +import static org.codehaus.groovy.ast.tools.GeneralUtils.throwS; +import static org.codehaus.groovy.ast.tools.GeneralUtils.varX; +import static org.codehaus.groovy.transform.stc.StaticTypesMarker.DIRECT_METHOD_CALL_TARGET; + +/** + * Handles generation of code for the @NullCheck annotation. + */ +@GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION) +public class NullCheckASTTransformation extends AbstractASTTransformation { + public static final ClassNode NULL_CHECK_TYPE = ClassHelper.make(NullCheck.class); + private static final String NULL_CHECK_NAME = "@" + NULL_CHECK_TYPE.getNameWithoutPackage(); + private static final ClassNode EXCEPTION = ClassHelper.make(IllegalArgumentException.class); + private static final ConstructorNode EXCEPTION_STRING_CTOR = EXCEPTION.getDeclaredConstructor(params(param(ClassHelper.STRING_TYPE, "s"))); + private static final String NULL_CHECK_IS_PROCESSED = "NullCheck.isProcessed"; + + @Override + public void visit(ASTNode[] nodes, SourceUnit source) { + init(nodes, source); + AnnotatedNode parent = (AnnotatedNode) nodes[1]; + AnnotationNode anno = (AnnotationNode) nodes[0]; + if (!NULL_CHECK_TYPE.equals(anno.getClassNode())) return; + boolean includeGenerated = isIncludeGenerated(anno); + + if (parent instanceof ClassNode) { + ClassNode cNode = (ClassNode) parent; + if (!checkNotInterface(cNode, NULL_CHECK_NAME)) return; + for (ConstructorNode cn : cNode.getDeclaredConstructors()) { + adjustMethod(cn, includeGenerated); + } + for (MethodNode mn : cNode.getAllDeclaredMethods()) { + adjustMethod(mn, includeGenerated); + } + } else if (parent instanceof MethodNode) { + // handles constructor case too + adjustMethod((MethodNode) parent, false); + } + } + + private boolean isIncludeGenerated(AnnotationNode anno) { + return memberHasValue(anno, "includeGenerated", true); + } + + public static boolean hasIncludeGenerated(ClassNode cNode) { + List annotations = cNode.getAnnotations(NULL_CHECK_TYPE); + if (annotations.isEmpty()) return false; + return hasIncludeGenerated(annotations.get(0)); + } + + private static boolean hasIncludeGenerated(AnnotationNode node) { + final Expression member = node.getMember("includeGenerated"); + return member instanceof ConstantExpression && ((ConstantExpression) member).getValue().equals(true); + } + + private void adjustMethod(MethodNode mn, boolean includeGenerated) { + BlockStatement newCode = getCodeAsBlock(mn); + if (mn.getParameters().length == 0) return; + boolean generated = isGenerated(mn); + int startingIndex = 0; + if (!includeGenerated && generated) return; + if (isMarkedAsProcessed(mn)) return; + if (mn instanceof ConstructorNode) { + // some transform has been here already and we assume it knows what it is doing + if (mn.getFirstStatement() instanceof BytecodeSequence) return; + // ignore any constructors calling this(...) or super(...) + ConstructorCallExpression cce = ConstructorNodeUtils.getFirstIfSpecialConstructorCall(mn.getCode()); + if (cce != null) { + if (generated) { + return; + } else { + startingIndex = 1; // skip over this/super() call + } + } + } + for (Parameter p : mn.getParameters()) { + if (ClassHelper.isPrimitiveType(p.getType())) continue; + newCode.getStatements().add(startingIndex, ifS(isNullX(varX(p)), makeThrowStmt(p.getName()))); + } + mn.setCode(newCode); + } + + public static ThrowStatement makeThrowStmt(final String variableName) { + ConstructorCallExpression newException = ctorX(EXCEPTION, constX(variableName + " cannot be null")); + newException.putNodeMetaData(DIRECT_METHOD_CALL_TARGET, EXCEPTION_STRING_CTOR); // GROOVY-10178 + return throwS(newException); + } + + /** + * Mark a method as already processed. + * + * @param mn the method node to be considered already processed + */ + public static void markAsProcessed(MethodNode mn) { + mn.setNodeMetaData(NULL_CHECK_IS_PROCESSED, Boolean.TRUE); + } + + private static boolean isMarkedAsProcessed(MethodNode mn) { + Boolean r = mn.getNodeMetaData(NULL_CHECK_IS_PROCESSED); + return null != r && r; + } +}