Skip to content

Commit 97a285d

Browse files
committed
GROOVY-10178
1 parent 65042d1 commit 97a285d

5 files changed

Lines changed: 327 additions & 3 deletions

File tree

base-test/org.eclipse.jdt.groovy.core.tests.compiler/src/org/eclipse/jdt/groovy/core/tests/xform/NullCheckTests.java

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2009-2020 the original author or authors.
2+
* Copyright 2009-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -92,8 +92,29 @@ public void testNullCheck2() {
9292
runConformTest(sources, "", "java.lang.IllegalArgumentException: whatever cannot be null");
9393
}
9494

95-
@Test
95+
@Test // GROOVY-10178
9696
public void testNullCheck3() {
97+
//@formatter:off
98+
String[] sources = {
99+
"Main.groovy",
100+
"new Pogo().test(null)\n",
101+
102+
"Pogo.groovy",
103+
"@groovy.transform.CompileStatic\n" +
104+
"class Pogo {\n" +
105+
" @groovy.transform.NullCheck\n" +
106+
" void test(whatever) {\n" +
107+
" print whatever\n" +
108+
" }\n" +
109+
"}\n",
110+
};
111+
//@formatter:on
112+
113+
runConformTest(sources, "", "java.lang.IllegalArgumentException: whatever cannot be null");
114+
}
115+
116+
@Test
117+
public void testNullCheck4() {
97118
//@formatter:off
98119
String[] sources = {
99120
"Main.groovy",

base/org.codehaus.groovy30/.checkstyle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
<file-match-pattern match-pattern="groovy/transform/DelegateASTTransformation.java" include-pattern="false" />
6666
<file-match-pattern match-pattern="groovy/transform/FieldASTTransformation.java" include-pattern="false" />
6767
<file-match-pattern match-pattern="groovy/transform/LogASTTransformation.java" include-pattern="false" />
68+
<file-match-pattern match-pattern="groovy/transform/NullCheckASTTransformation.java" include-pattern="false" />
6869
<file-match-pattern match-pattern="groovy/transform/TupleConstructorASTTransformation.java" include-pattern="false" />
6970
<file-match-pattern match-pattern="groovy/transform/sc/transformers/(Binary|MethodCall)ExpressionTransformer.java" include-pattern="false" />
7071
<file-match-pattern match-pattern="groovy/transform/stc/AbstractExtensionMethodCache.java" include-pattern="false" />
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.codehaus.groovy.transform;
20+
21+
import groovy.transform.NullCheck;
22+
import org.apache.groovy.ast.tools.ConstructorNodeUtils;
23+
import org.codehaus.groovy.ast.ASTNode;
24+
import org.codehaus.groovy.ast.AnnotatedNode;
25+
import org.codehaus.groovy.ast.AnnotationNode;
26+
import org.codehaus.groovy.ast.ClassHelper;
27+
import org.codehaus.groovy.ast.ClassNode;
28+
import org.codehaus.groovy.ast.ConstructorNode;
29+
import org.codehaus.groovy.ast.MethodNode;
30+
import org.codehaus.groovy.ast.Parameter;
31+
import org.codehaus.groovy.ast.expr.ConstantExpression;
32+
import org.codehaus.groovy.ast.expr.ConstructorCallExpression;
33+
import org.codehaus.groovy.ast.expr.Expression;
34+
import org.codehaus.groovy.ast.stmt.BlockStatement;
35+
import org.codehaus.groovy.ast.stmt.ThrowStatement;
36+
import org.codehaus.groovy.classgen.BytecodeSequence;
37+
import org.codehaus.groovy.control.CompilePhase;
38+
import org.codehaus.groovy.control.SourceUnit;
39+
40+
import java.util.List;
41+
42+
import static org.apache.groovy.ast.tools.AnnotatedNodeUtils.isGenerated;
43+
import static org.apache.groovy.ast.tools.MethodNodeUtils.getCodeAsBlock;
44+
import static org.codehaus.groovy.ast.tools.GeneralUtils.constX;
45+
import static org.codehaus.groovy.ast.tools.GeneralUtils.ctorX;
46+
import static org.codehaus.groovy.ast.tools.GeneralUtils.ifS;
47+
import static org.codehaus.groovy.ast.tools.GeneralUtils.isNullX;
48+
import static org.codehaus.groovy.ast.tools.GeneralUtils.param;
49+
import static org.codehaus.groovy.ast.tools.GeneralUtils.params;
50+
import static org.codehaus.groovy.ast.tools.GeneralUtils.throwS;
51+
import static org.codehaus.groovy.ast.tools.GeneralUtils.varX;
52+
import static org.codehaus.groovy.transform.stc.StaticTypesMarker.DIRECT_METHOD_CALL_TARGET;
53+
54+
/**
55+
* Handles generation of code for the @NullCheck annotation.
56+
*/
57+
@GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION)
58+
public class NullCheckASTTransformation extends AbstractASTTransformation {
59+
public static final ClassNode NULL_CHECK_TYPE = ClassHelper.make(NullCheck.class);
60+
private static final String NULL_CHECK_NAME = "@" + NULL_CHECK_TYPE.getNameWithoutPackage();
61+
private static final ClassNode EXCEPTION = ClassHelper.make(IllegalArgumentException.class);
62+
private static final String NULL_CHECK_IS_PROCESSED = "NullCheck.isProcessed";
63+
64+
@Override
65+
public void visit(ASTNode[] nodes, SourceUnit source) {
66+
init(nodes, source);
67+
AnnotatedNode parent = (AnnotatedNode) nodes[1];
68+
AnnotationNode anno = (AnnotationNode) nodes[0];
69+
if (!NULL_CHECK_TYPE.equals(anno.getClassNode())) return;
70+
boolean includeGenerated = isIncludeGenerated(anno);
71+
72+
if (parent instanceof ClassNode) {
73+
ClassNode cNode = (ClassNode) parent;
74+
if (!checkNotInterface(cNode, NULL_CHECK_NAME)) return;
75+
for (ConstructorNode cn : cNode.getDeclaredConstructors()) {
76+
adjustMethod(cn, includeGenerated);
77+
}
78+
for (MethodNode mn : cNode.getAllDeclaredMethods()) {
79+
adjustMethod(mn, includeGenerated);
80+
}
81+
} else if (parent instanceof MethodNode) {
82+
// handles constructor case too
83+
adjustMethod((MethodNode) parent, false);
84+
}
85+
}
86+
87+
private boolean isIncludeGenerated(AnnotationNode anno) {
88+
return memberHasValue(anno, "includeGenerated", true);
89+
}
90+
91+
public static boolean hasIncludeGenerated(ClassNode cNode) {
92+
List<AnnotationNode> annotations = cNode.getAnnotations(NULL_CHECK_TYPE);
93+
if (annotations.isEmpty()) return false;
94+
return hasIncludeGenerated(annotations.get(0));
95+
}
96+
97+
private static boolean hasIncludeGenerated(AnnotationNode node) {
98+
final Expression member = node.getMember("includeGenerated");
99+
return member instanceof ConstantExpression && ((ConstantExpression) member).getValue().equals(true);
100+
}
101+
102+
private void adjustMethod(MethodNode mn, boolean includeGenerated) {
103+
BlockStatement newCode = getCodeAsBlock(mn);
104+
if (mn.getParameters().length == 0) return;
105+
boolean generated = isGenerated(mn);
106+
int startingIndex = 0;
107+
if (!includeGenerated && generated) return;
108+
if (isMarkedAsProcessed(mn)) return;
109+
if (mn instanceof ConstructorNode) {
110+
// some transform has been here already and we assume it knows what it is doing
111+
if (mn.getFirstStatement() instanceof BytecodeSequence) return;
112+
// ignore any constructors calling this(...) or super(...)
113+
ConstructorCallExpression cce = ConstructorNodeUtils.getFirstIfSpecialConstructorCall(mn.getCode());
114+
if (cce != null) {
115+
if (generated) {
116+
return;
117+
} else {
118+
startingIndex = 1; // skip over this/super() call
119+
}
120+
}
121+
}
122+
for (Parameter p : mn.getParameters()) {
123+
if (ClassHelper.isPrimitiveType(p.getType())) continue;
124+
newCode.getStatements().add(startingIndex, ifS(isNullX(varX(p)), makeThrowStmt(p.getName())));
125+
}
126+
mn.setCode(newCode);
127+
}
128+
129+
public static ThrowStatement makeThrowStmt(String name) {
130+
/* GRECLIPSE edit -- GROOVY-10178
131+
return throwS(ctorX(EXCEPTION, constX(name + " cannot be null")));
132+
*/
133+
ConstructorCallExpression newException = ctorX(EXCEPTION, constX(name + " cannot be null"));
134+
newException.putNodeMetaData(DIRECT_METHOD_CALL_TARGET,
135+
EXCEPTION.getDeclaredConstructor(params(param(ClassHelper.STRING_TYPE, "s"))));
136+
return throwS(newException);
137+
// GRECLIPSE end
138+
}
139+
140+
/**
141+
* Mark a method as already processed.
142+
*
143+
* @param mn the method node to be considered already processed
144+
*/
145+
public static void markAsProcessed(MethodNode mn) {
146+
mn.setNodeMetaData(NULL_CHECK_IS_PROCESSED, Boolean.TRUE);
147+
}
148+
149+
private static boolean isMarkedAsProcessed(MethodNode mn) {
150+
Boolean r = mn.getNodeMetaData(NULL_CHECK_IS_PROCESSED);
151+
return null != r && r;
152+
}
153+
}

base/org.codehaus.groovy40/.checkstyle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
<file-match-pattern match-pattern="groovy/transform/ASTTestTransformation.groovy" include-pattern="false" />
5656
<file-match-pattern match-pattern="groovy/transform/ASTTransformationCollectorCodeVisitor.java" include-pattern="false" />
5757
<file-match-pattern match-pattern="groovy/transform/ASTTransformationVisitor.java" include-pattern="false" />
58-
<file-match-pattern match-pattern="groovy/transform/(Field|Log)ASTTransformation.java" include-pattern="false" />
58+
<file-match-pattern match-pattern="groovy/transform/(Field|Log|NullCheck)ASTTransformation.java" include-pattern="false" />
5959
<file-match-pattern match-pattern="groovy/transform/NotYetImplemented.java" include-pattern="false" />
6060
<file-match-pattern match-pattern="groovy/transform/sc/transformers/(Binary|MethodCall)ExpressionTransformer.java" include-pattern="false" />
6161
<file-match-pattern match-pattern="groovy/transform/stc/StaticTypeCheckingSupport.java" include-pattern="false" />
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.codehaus.groovy.transform;
20+
21+
import groovy.transform.NullCheck;
22+
import org.apache.groovy.ast.tools.ConstructorNodeUtils;
23+
import org.codehaus.groovy.ast.ASTNode;
24+
import org.codehaus.groovy.ast.AnnotatedNode;
25+
import org.codehaus.groovy.ast.AnnotationNode;
26+
import org.codehaus.groovy.ast.ClassHelper;
27+
import org.codehaus.groovy.ast.ClassNode;
28+
import org.codehaus.groovy.ast.ConstructorNode;
29+
import org.codehaus.groovy.ast.MethodNode;
30+
import org.codehaus.groovy.ast.Parameter;
31+
import org.codehaus.groovy.ast.expr.ConstantExpression;
32+
import org.codehaus.groovy.ast.expr.ConstructorCallExpression;
33+
import org.codehaus.groovy.ast.expr.Expression;
34+
import org.codehaus.groovy.ast.stmt.BlockStatement;
35+
import org.codehaus.groovy.ast.stmt.ThrowStatement;
36+
import org.codehaus.groovy.classgen.BytecodeSequence;
37+
import org.codehaus.groovy.control.CompilePhase;
38+
import org.codehaus.groovy.control.SourceUnit;
39+
40+
import java.util.List;
41+
42+
import static org.apache.groovy.ast.tools.AnnotatedNodeUtils.isGenerated;
43+
import static org.apache.groovy.ast.tools.MethodNodeUtils.getCodeAsBlock;
44+
import static org.codehaus.groovy.ast.tools.GeneralUtils.constX;
45+
import static org.codehaus.groovy.ast.tools.GeneralUtils.ctorX;
46+
import static org.codehaus.groovy.ast.tools.GeneralUtils.ifS;
47+
import static org.codehaus.groovy.ast.tools.GeneralUtils.isNullX;
48+
import static org.codehaus.groovy.ast.tools.GeneralUtils.param;
49+
import static org.codehaus.groovy.ast.tools.GeneralUtils.params;
50+
import static org.codehaus.groovy.ast.tools.GeneralUtils.throwS;
51+
import static org.codehaus.groovy.ast.tools.GeneralUtils.varX;
52+
import static org.codehaus.groovy.transform.stc.StaticTypesMarker.DIRECT_METHOD_CALL_TARGET;
53+
54+
/**
55+
* Handles generation of code for the @NullCheck annotation.
56+
*/
57+
@GroovyASTTransformation(phase = CompilePhase.INSTRUCTION_SELECTION)
58+
public class NullCheckASTTransformation extends AbstractASTTransformation {
59+
public static final ClassNode NULL_CHECK_TYPE = ClassHelper.make(NullCheck.class);
60+
private static final String NULL_CHECK_NAME = "@" + NULL_CHECK_TYPE.getNameWithoutPackage();
61+
private static final ClassNode EXCEPTION = ClassHelper.make(IllegalArgumentException.class);
62+
private static final ConstructorNode EXCEPTION_STRING_CTOR = EXCEPTION.getDeclaredConstructor(params(param(ClassHelper.STRING_TYPE, "s")));
63+
private static final String NULL_CHECK_IS_PROCESSED = "NullCheck.isProcessed";
64+
65+
@Override
66+
public void visit(ASTNode[] nodes, SourceUnit source) {
67+
init(nodes, source);
68+
AnnotatedNode parent = (AnnotatedNode) nodes[1];
69+
AnnotationNode anno = (AnnotationNode) nodes[0];
70+
if (!NULL_CHECK_TYPE.equals(anno.getClassNode())) return;
71+
boolean includeGenerated = isIncludeGenerated(anno);
72+
73+
if (parent instanceof ClassNode) {
74+
ClassNode cNode = (ClassNode) parent;
75+
if (!checkNotInterface(cNode, NULL_CHECK_NAME)) return;
76+
for (ConstructorNode cn : cNode.getDeclaredConstructors()) {
77+
adjustMethod(cn, includeGenerated);
78+
}
79+
for (MethodNode mn : cNode.getAllDeclaredMethods()) {
80+
adjustMethod(mn, includeGenerated);
81+
}
82+
} else if (parent instanceof MethodNode) {
83+
// handles constructor case too
84+
adjustMethod((MethodNode) parent, false);
85+
}
86+
}
87+
88+
private boolean isIncludeGenerated(AnnotationNode anno) {
89+
return memberHasValue(anno, "includeGenerated", true);
90+
}
91+
92+
public static boolean hasIncludeGenerated(ClassNode cNode) {
93+
List<AnnotationNode> annotations = cNode.getAnnotations(NULL_CHECK_TYPE);
94+
if (annotations.isEmpty()) return false;
95+
return hasIncludeGenerated(annotations.get(0));
96+
}
97+
98+
private static boolean hasIncludeGenerated(AnnotationNode node) {
99+
final Expression member = node.getMember("includeGenerated");
100+
return member instanceof ConstantExpression && ((ConstantExpression) member).getValue().equals(true);
101+
}
102+
103+
private void adjustMethod(MethodNode mn, boolean includeGenerated) {
104+
BlockStatement newCode = getCodeAsBlock(mn);
105+
if (mn.getParameters().length == 0) return;
106+
boolean generated = isGenerated(mn);
107+
int startingIndex = 0;
108+
if (!includeGenerated && generated) return;
109+
if (isMarkedAsProcessed(mn)) return;
110+
if (mn instanceof ConstructorNode) {
111+
// some transform has been here already and we assume it knows what it is doing
112+
if (mn.getFirstStatement() instanceof BytecodeSequence) return;
113+
// ignore any constructors calling this(...) or super(...)
114+
ConstructorCallExpression cce = ConstructorNodeUtils.getFirstIfSpecialConstructorCall(mn.getCode());
115+
if (cce != null) {
116+
if (generated) {
117+
return;
118+
} else {
119+
startingIndex = 1; // skip over this/super() call
120+
}
121+
}
122+
}
123+
for (Parameter p : mn.getParameters()) {
124+
if (ClassHelper.isPrimitiveType(p.getType())) continue;
125+
newCode.getStatements().add(startingIndex, ifS(isNullX(varX(p)), makeThrowStmt(p.getName())));
126+
}
127+
mn.setCode(newCode);
128+
}
129+
130+
public static ThrowStatement makeThrowStmt(final String variableName) {
131+
ConstructorCallExpression newException = ctorX(EXCEPTION, constX(variableName + " cannot be null"));
132+
newException.putNodeMetaData(DIRECT_METHOD_CALL_TARGET, EXCEPTION_STRING_CTOR); // GROOVY-10178
133+
return throwS(newException);
134+
}
135+
136+
/**
137+
* Mark a method as already processed.
138+
*
139+
* @param mn the method node to be considered already processed
140+
*/
141+
public static void markAsProcessed(MethodNode mn) {
142+
mn.setNodeMetaData(NULL_CHECK_IS_PROCESSED, Boolean.TRUE);
143+
}
144+
145+
private static boolean isMarkedAsProcessed(MethodNode mn) {
146+
Boolean r = mn.getNodeMetaData(NULL_CHECK_IS_PROCESSED);
147+
return null != r && r;
148+
}
149+
}

0 commit comments

Comments
 (0)