Repository: groovy Updated Branches: refs/heads/master 6a7c42e8c -> a6c82ad08
Move source files to proper packages further Project: http://git-wip-us.apache.org/repos/asf/groovy/repo Commit: http://git-wip-us.apache.org/repos/asf/groovy/commit/a6c82ad0 Tree: http://git-wip-us.apache.org/repos/asf/groovy/tree/a6c82ad0 Diff: http://git-wip-us.apache.org/repos/asf/groovy/diff/a6c82ad0 Branch: refs/heads/master Commit: a6c82ad08b85ae8746b982a1a19107e93bfdb568 Parents: 6a7c42e Author: sunlan <[email protected]> Authored: Wed Dec 20 12:32:48 2017 +0800 Committer: sunlan <[email protected]> Committed: Wed Dec 20 12:32:48 2017 +0800 ---------------------------------------------------------------------- .../transform/ASTTestTransformation.groovy | 233 -------------- ...itionalInterruptibleASTTransformation.groovy | 145 --------- .../ThreadInterruptibleASTTransformation.groovy | 98 ------ .../TimedInterruptibleASTTransformation.groovy | 321 ------------------- .../transform/ASTTestTransformation.groovy | 233 ++++++++++++++ ...itionalInterruptibleASTTransformation.groovy | 145 +++++++++ .../ThreadInterruptibleASTTransformation.groovy | 98 ++++++ .../TimedInterruptibleASTTransformation.groovy | 321 +++++++++++++++++++ 8 files changed, 797 insertions(+), 797 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/groovy/blob/a6c82ad0/src/main/groovy/groovy/org/codehaus/groovy/transform/ASTTestTransformation.groovy ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/org/codehaus/groovy/transform/ASTTestTransformation.groovy b/src/main/groovy/groovy/org/codehaus/groovy/transform/ASTTestTransformation.groovy deleted file mode 100644 index dcfe314..0000000 --- a/src/main/groovy/groovy/org/codehaus/groovy/transform/ASTTestTransformation.groovy +++ /dev/null @@ -1,233 +0,0 @@ -/* - * 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.CompilationUnitAware -import org.codehaus.groovy.ast.ASTNode -import org.codehaus.groovy.ast.AnnotationNode -import org.codehaus.groovy.ast.ClassCodeVisitorSupport -import org.codehaus.groovy.ast.ClassHelper -import org.codehaus.groovy.ast.ClassNode -import org.codehaus.groovy.ast.MethodNode -import org.codehaus.groovy.ast.expr.ClosureExpression -import org.codehaus.groovy.ast.expr.PropertyExpression -import org.codehaus.groovy.ast.expr.VariableExpression -import org.codehaus.groovy.ast.stmt.Statement -import org.codehaus.groovy.control.CompilationUnit -import org.codehaus.groovy.control.CompilePhase -import org.codehaus.groovy.control.CompilerConfiguration -import org.codehaus.groovy.control.ErrorCollector -import org.codehaus.groovy.control.Janitor -import org.codehaus.groovy.control.ProcessingUnit -import org.codehaus.groovy.control.SourceUnit -import org.codehaus.groovy.control.customizers.ImportCustomizer -import org.codehaus.groovy.control.io.ReaderSource -import org.codehaus.groovy.runtime.MethodClosure -import org.codehaus.groovy.syntax.SyntaxException -import org.codehaus.groovy.tools.Utilities - -import static org.codehaus.groovy.ast.tools.GeneralUtils.classX -import static org.codehaus.groovy.ast.tools.GeneralUtils.propX - -@GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS) -class ASTTestTransformation extends AbstractASTTransformation implements CompilationUnitAware { - private CompilationUnit compilationUnit - - void visit(final ASTNode[] nodes, final SourceUnit source) { - AnnotationNode annotationNode = nodes[0] - def member = annotationNode.getMember('phase') - def phase = null - if (member) { - if (member instanceof VariableExpression) { - phase = CompilePhase.valueOf(member.text) - } else if (member instanceof PropertyExpression) { - phase = CompilePhase.valueOf(member.propertyAsString) - } - annotationNode.setMember('phase', propX(classX(ClassHelper.make(CompilePhase)), phase.toString())) - } - member = annotationNode.getMember('value') - if (member && !(member instanceof ClosureExpression)) { - throw new SyntaxException("ASTTest value must be a closure", member.getLineNumber(), member.getColumnNumber()) - } - if (!member && !annotationNode.getNodeMetaData(ASTTestTransformation)) { - throw new SyntaxException("Missing test expression", annotationNode.getLineNumber(), annotationNode.getColumnNumber()) - } - // convert value into node metadata so that the expression doesn't mix up with other AST xforms like type checking - annotationNode.putNodeMetaData(ASTTestTransformation, member) - annotationNode.getMembers().remove('value') - - def pcallback = compilationUnit.progressCallback - def callback = new CompilationUnit.ProgressCallback() { - Binding binding = new Binding([:].withDefault {null}) - - @Override - void call(final ProcessingUnit context, final int phaseRef) { - if (phase==null || phaseRef == phase.phaseNumber) { - ClosureExpression testClosure = nodes[0].getNodeMetaData(ASTTestTransformation) - StringBuilder sb = new StringBuilder() - for (int i = testClosure.lineNumber; i <= testClosure.lastLineNumber; i++) { - sb.append(source.source.getLine(i, new Janitor())).append('\n') - } - def testSource = sb.substring(testClosure.columnNumber + 1, sb.length()) - testSource = testSource.substring(0, testSource.lastIndexOf('}')) - CompilerConfiguration config = new CompilerConfiguration() - def customizer = new ImportCustomizer() - config.addCompilationCustomizers(customizer) - binding['sourceUnit'] = source - binding['node'] = nodes[1] - binding['lookup'] = new MethodClosure(LabelFinder, "lookup").curry(nodes[1]) - binding['compilationUnit'] = compilationUnit - binding['compilePhase'] = CompilePhase.fromPhaseNumber(phaseRef) - - GroovyShell shell = new GroovyShell(binding, config) - - source.AST.imports.each { - customizer.addImport(it.alias, it.type.name) - } - source.AST.starImports.each { - customizer.addStarImports(it.packageName) - } - source.AST.staticImports.each { - customizer.addStaticImport(it.value.alias, it.value.type.name, it.value.fieldName) - } - source.AST.staticStarImports.each { - customizer.addStaticStars(it.value.className) - } - shell.evaluate(testSource) - } - } - } - - if (pcallback!=null) { - if (pcallback instanceof ProgressCallbackChain) { - pcallback.addCallback(callback) - } else { - pcallback = new ProgressCallbackChain(pcallback, callback) - } - callback = pcallback - } - - compilationUnit.setProgressCallback(callback) - - } - - void setCompilationUnit(final CompilationUnit unit) { - this.compilationUnit = unit - } - - private static class AssertionSourceDelegatingSourceUnit extends SourceUnit { - private final ReaderSource delegate - - AssertionSourceDelegatingSourceUnit(final String name, final ReaderSource source, final CompilerConfiguration flags, final GroovyClassLoader loader, final ErrorCollector er) { - super(name, '', flags, loader, er) - delegate = source - } - - @Override - String getSample(final int line, final int column, final Janitor janitor) { - String sample = null; - String text = delegate.getLine(line, janitor); - - if (text != null) { - if (column > 0) { - String marker = Utilities.repeatString(" ", column - 1) + "^"; - - if (column > 40) { - int start = column - 30 - 1; - int end = (column + 10 > text.length() ? text.length() : column + 10 - 1); - sample = " " + text.substring(start, end) + Utilities.eol() + " " + - marker.substring(start, marker.length()); - } else { - sample = " " + text + Utilities.eol() + " " + marker; - } - } else { - sample = text; - } - } - - return sample; - - } - - } - - private static class ProgressCallbackChain extends CompilationUnit.ProgressCallback { - - private final List<CompilationUnit.ProgressCallback> chain = new LinkedList<CompilationUnit.ProgressCallback>() - - ProgressCallbackChain(CompilationUnit.ProgressCallback... callbacks) { - if (callbacks!=null) { - callbacks.each { addCallback(it) } - } - } - - public void addCallback(CompilationUnit.ProgressCallback callback) { - chain << callback - } - - @Override - void call(final ProcessingUnit context, final int phase) { - chain*.call(context, phase) - } - } - - public static class LabelFinder extends ClassCodeVisitorSupport { - - public static List<Statement> lookup(MethodNode node, String label) { - LabelFinder finder = new LabelFinder(label, null) - node.code.visit(finder) - - finder.targets - } - - public static List<Statement> lookup(ClassNode node, String label) { - LabelFinder finder = new LabelFinder(label, null) - node.methods*.code*.visit(finder) - node.declaredConstructors*.code*.visit(finder) - - finder.targets - } - - private final String label - private final SourceUnit unit - - private final List<Statement> targets = new LinkedList<Statement>(); - - LabelFinder(final String label, final SourceUnit unit) { - this.label = label - this.unit = unit; - } - - @Override - protected SourceUnit getSourceUnit() { - unit - } - - @Override - protected void visitStatement(final Statement statement) { - super.visitStatement(statement) - if (statement.statementLabel==label) targets << statement - } - - List<Statement> getTargets() { - return Collections.unmodifiableList(targets) - } - } - -} http://git-wip-us.apache.org/repos/asf/groovy/blob/a6c82ad0/src/main/groovy/groovy/org/codehaus/groovy/transform/ConditionalInterruptibleASTTransformation.groovy ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/org/codehaus/groovy/transform/ConditionalInterruptibleASTTransformation.groovy b/src/main/groovy/groovy/org/codehaus/groovy/transform/ConditionalInterruptibleASTTransformation.groovy deleted file mode 100644 index 2cda121..0000000 --- a/src/main/groovy/groovy/org/codehaus/groovy/transform/ConditionalInterruptibleASTTransformation.groovy +++ /dev/null @@ -1,145 +0,0 @@ -/* - * 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.ConditionalInterrupt -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.FieldNode -import org.codehaus.groovy.ast.MethodNode -import org.codehaus.groovy.ast.Parameter -import org.codehaus.groovy.ast.PropertyNode -import org.codehaus.groovy.ast.expr.ArgumentListExpression -import org.codehaus.groovy.ast.expr.ClosureExpression -import org.codehaus.groovy.ast.expr.Expression -import org.codehaus.groovy.ast.expr.MethodCallExpression -import org.codehaus.groovy.ast.expr.VariableExpression -import org.codehaus.groovy.ast.tools.ClosureUtils -import org.codehaus.groovy.control.CompilePhase - -/** - * Allows "interrupt-safe" executions of scripts by adding a custom conditional - * check on loops (for, while, do) and first statement of closures. By default, also adds an interrupt check - * statement on the beginning of method calls. - * - * @see groovy.transform.ConditionalInterrupt - * @author Cedric Champeau - * @author Hamlet D'Arcy - * @author Paul King - * @since 1.8.0 - */ -@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION) -public class ConditionalInterruptibleASTTransformation extends AbstractInterruptibleASTTransformation { - - private static final ClassNode MY_TYPE = ClassHelper.make(ConditionalInterrupt) - - private ClosureExpression conditionNode - private String conditionMethod - private MethodCallExpression conditionCallExpression - private ClassNode currentClass - - protected ClassNode type() { - return MY_TYPE - } - - protected void setupTransform(AnnotationNode node) { - super.setupTransform(node) - def member = node.getMember("value") - if (!member || !(member instanceof ClosureExpression)) internalError("Expected closure value for annotation parameter 'value'. Found $member") - conditionNode = member; - conditionMethod = 'conditionalTransform' + node.hashCode() + '$condition' - conditionCallExpression = new MethodCallExpression(new VariableExpression('this'), conditionMethod, new ArgumentListExpression()) - } - - protected String getErrorMessage() { - 'Execution interrupted. The following condition failed: ' + convertClosureToSource(conditionNode) - } - - void visitClass(ClassNode type) { - currentClass = type - def method = type.addMethod(conditionMethod, ACC_PRIVATE | ACC_SYNTHETIC, ClassHelper.OBJECT_TYPE, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, conditionNode.code) - method.synthetic = true - if (applyToAllMembers) { - super.visitClass(type) - } - } - - protected Expression createCondition() { - conditionCallExpression - } - - @Override - void visitAnnotations(AnnotatedNode node) { - // this transformation does not apply on annotation nodes - // visiting could lead to stack overflows - } - - @Override - void visitField(FieldNode node) { - if (!node.isStatic() && !node.isSynthetic()) { - super.visitField node - } - } - - @Override - void visitProperty(PropertyNode node) { - if (!node.isStatic() && !node.isSynthetic()) { - super.visitProperty node - } - } - - @Override - void visitClosureExpression(ClosureExpression closureExpr) { - if (closureExpr == conditionNode) return // do not visit the closure from the annotation itself - def code = closureExpr.code - closureExpr.code = wrapBlock(code) - super.visitClosureExpression closureExpr - } - - @Override - void visitMethod(MethodNode node) { - if (node.name == conditionMethod && !node.isSynthetic()) return // do not visit the generated method - if (node.name == 'run' && currentClass.isScript() && node.parameters.length == 0) { - // the run() method should not have the statement added, otherwise the script binding won't be set before - // the condition is actually tested - super.visitMethod(node) - } else { - if (checkOnMethodStart && !node.isSynthetic() && !node.isStatic() && !node.isAbstract()) { - def code = node.code - node.code = wrapBlock(code); - } - if (!node.isSynthetic() && !node.isStatic()) super.visitMethod(node) - } - } - - /** - * Converts a ClosureExpression into the String source. - * @param expression a closure - * @return the source the closure was created from - */ - private String convertClosureToSource(ClosureExpression expression) { - try { - return ClosureUtils.convertClosureToSource(this.source.source, expression); - } catch(Exception e) { - return e.message - } - } -} http://git-wip-us.apache.org/repos/asf/groovy/blob/a6c82ad0/src/main/groovy/groovy/org/codehaus/groovy/transform/ThreadInterruptibleASTTransformation.groovy ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/org/codehaus/groovy/transform/ThreadInterruptibleASTTransformation.groovy b/src/main/groovy/groovy/org/codehaus/groovy/transform/ThreadInterruptibleASTTransformation.groovy deleted file mode 100644 index a4fb4c3..0000000 --- a/src/main/groovy/groovy/org/codehaus/groovy/transform/ThreadInterruptibleASTTransformation.groovy +++ /dev/null @@ -1,98 +0,0 @@ -/* - * 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.CompileStatic -import groovy.transform.ThreadInterrupt -import org.codehaus.groovy.ast.ClassHelper -import org.codehaus.groovy.ast.ClassNode -import org.codehaus.groovy.ast.MethodNode -import org.codehaus.groovy.ast.Parameter -import org.codehaus.groovy.ast.expr.ArgumentListExpression -import org.codehaus.groovy.ast.expr.ClassExpression -import org.codehaus.groovy.ast.expr.ClosureExpression -import org.codehaus.groovy.ast.expr.Expression -import org.codehaus.groovy.ast.expr.MethodCallExpression -import org.codehaus.groovy.control.CompilePhase - -/** - * Allows "interrupt-safe" executions of scripts by adding Thread.currentThread().isInterrupted() - * checks on loops (for, while, do) and first statement of closures. By default, also adds an interrupt check - * statement on the beginning of method calls. - * - * @see groovy.transform.ThreadInterrupt - * - * @author Cedric Champeau - * @author Hamlet D'Arcy - * - * @since 1.8.0 - */ -@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION) -@CompileStatic -public class ThreadInterruptibleASTTransformation extends AbstractInterruptibleASTTransformation { - - private static final ClassNode MY_TYPE = ClassHelper.make(ThreadInterrupt) - private static final ClassNode THREAD_TYPE = ClassHelper.make(Thread) - private static final MethodNode CURRENTTHREAD_METHOD - private static final MethodNode ISINTERRUPTED_METHOD - - static { - CURRENTTHREAD_METHOD = THREAD_TYPE.getMethod('currentThread', Parameter.EMPTY_ARRAY) - ISINTERRUPTED_METHOD = THREAD_TYPE.getMethod('isInterrupted', Parameter.EMPTY_ARRAY) - } - - protected ClassNode type() { - return MY_TYPE; - } - - protected String getErrorMessage() { - 'Execution interrupted. The current thread has been interrupted.' - } - - protected Expression createCondition() { - def currentThread = new MethodCallExpression(new ClassExpression(THREAD_TYPE), - 'currentThread', - ArgumentListExpression.EMPTY_ARGUMENTS) - currentThread.methodTarget = CURRENTTHREAD_METHOD - def isInterrupted = new MethodCallExpression( - currentThread, - 'isInterrupted', ArgumentListExpression.EMPTY_ARGUMENTS) - isInterrupted.methodTarget = ISINTERRUPTED_METHOD - [currentThread, isInterrupted]*.implicitThis = false - - isInterrupted - } - - - @Override - public void visitClosureExpression(ClosureExpression closureExpr) { - def code = closureExpr.code - closureExpr.code = wrapBlock(code) - super.visitClosureExpression closureExpr - } - - @Override - public void visitMethod(MethodNode node) { - if (checkOnMethodStart && !node.isSynthetic() && !node.isAbstract()) { - def code = node.code - node.code = wrapBlock(code); - } - super.visitMethod(node) - } -} http://git-wip-us.apache.org/repos/asf/groovy/blob/a6c82ad0/src/main/groovy/groovy/org/codehaus/groovy/transform/TimedInterruptibleASTTransformation.groovy ---------------------------------------------------------------------- diff --git a/src/main/groovy/groovy/org/codehaus/groovy/transform/TimedInterruptibleASTTransformation.groovy b/src/main/groovy/groovy/org/codehaus/groovy/transform/TimedInterruptibleASTTransformation.groovy deleted file mode 100644 index fbc923b..0000000 --- a/src/main/groovy/groovy/org/codehaus/groovy/transform/TimedInterruptibleASTTransformation.groovy +++ /dev/null @@ -1,321 +0,0 @@ -/* - * 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.TimedInterrupt -import org.codehaus.groovy.ast.ASTNode -import org.codehaus.groovy.ast.AnnotatedNode -import org.codehaus.groovy.ast.AnnotationNode -import org.codehaus.groovy.ast.ClassCodeVisitorSupport -import org.codehaus.groovy.ast.ClassHelper -import org.codehaus.groovy.ast.ClassNode -import org.codehaus.groovy.ast.FieldNode -import org.codehaus.groovy.ast.MethodNode -import org.codehaus.groovy.ast.PropertyNode -import org.codehaus.groovy.ast.expr.ClosureExpression -import org.codehaus.groovy.ast.expr.ConstantExpression -import org.codehaus.groovy.ast.expr.DeclarationExpression -import org.codehaus.groovy.ast.expr.Expression -import org.codehaus.groovy.ast.stmt.BlockStatement -import org.codehaus.groovy.ast.stmt.DoWhileStatement -import org.codehaus.groovy.ast.stmt.ForStatement -import org.codehaus.groovy.ast.stmt.WhileStatement -import org.codehaus.groovy.control.CompilePhase -import org.codehaus.groovy.control.SourceUnit - -import java.util.concurrent.TimeUnit -import java.util.concurrent.TimeoutException - -import static org.codehaus.groovy.ast.ClassHelper.make -import static org.codehaus.groovy.ast.tools.GeneralUtils.args -import static org.codehaus.groovy.ast.tools.GeneralUtils.callX -import static org.codehaus.groovy.ast.tools.GeneralUtils.classX -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.ltX -import static org.codehaus.groovy.ast.tools.GeneralUtils.plusX -import static org.codehaus.groovy.ast.tools.GeneralUtils.propX -import static org.codehaus.groovy.ast.tools.GeneralUtils.throwS -import static org.codehaus.groovy.ast.tools.GeneralUtils.varX - -/** - * Allows "interrupt-safe" executions of scripts by adding timer expiration - * checks on loops (for, while, do) and first statement of closures. By default, - * also adds an interrupt check statement on the beginning of method calls. - * - * @author Cedric Champeau - * @author Hamlet D'Arcy - * @author Paul King - * @see groovy.transform.ThreadInterrupt - * @since 1.8.0 - */ -@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION) -public class TimedInterruptibleASTTransformation extends AbstractASTTransformation { - - private static final ClassNode MY_TYPE = make(TimedInterrupt) - private static final String CHECK_METHOD_START_MEMBER = 'checkOnMethodStart' - private static final String APPLY_TO_ALL_CLASSES = 'applyToAllClasses' - private static final String APPLY_TO_ALL_MEMBERS = 'applyToAllMembers' - private static final String THROWN_EXCEPTION_TYPE = "thrown" - - public void visit(ASTNode[] nodes, SourceUnit source) { - init(nodes, source); - AnnotationNode node = nodes[0] - AnnotatedNode annotatedNode = nodes[1] - if (!MY_TYPE.equals(node.getClassNode())) { - internalError("Transformation called from wrong annotation: $node.classNode.name") - } - - def checkOnMethodStart = getConstantAnnotationParameter(node, CHECK_METHOD_START_MEMBER, Boolean.TYPE, true) - def applyToAllMembers = getConstantAnnotationParameter(node, APPLY_TO_ALL_MEMBERS, Boolean.TYPE, true) - def applyToAllClasses = applyToAllMembers ? getConstantAnnotationParameter(node, APPLY_TO_ALL_CLASSES, Boolean.TYPE, true) : false - def maximum = getConstantAnnotationParameter(node, 'value', Long.TYPE, Long.MAX_VALUE) - def thrown = AbstractInterruptibleASTTransformation.getClassAnnotationParameter(node, THROWN_EXCEPTION_TYPE, make(TimeoutException)) - - Expression unit = node.getMember('unit') ?: propX(classX(TimeUnit), "SECONDS") - - // should be limited to the current SourceUnit or propagated to the whole CompilationUnit - // DO NOT inline visitor creation in code below. It has state that must not persist between calls - if (applyToAllClasses) { - // guard every class and method defined in this script - source.getAST()?.classes?.each { ClassNode it -> - def visitor = new TimedInterruptionVisitor(source, checkOnMethodStart, applyToAllClasses, applyToAllMembers, maximum, unit, thrown, node.hashCode()) - visitor.visitClass(it) - } - } else if (annotatedNode instanceof ClassNode) { - // only guard this particular class - def visitor = new TimedInterruptionVisitor(source, checkOnMethodStart, applyToAllClasses, applyToAllMembers, maximum, unit, thrown, node.hashCode()) - visitor.visitClass annotatedNode - } else if (!applyToAllMembers && annotatedNode instanceof MethodNode) { - // only guard this particular method (plus initCode for class) - def visitor = new TimedInterruptionVisitor(source, checkOnMethodStart, applyToAllClasses, applyToAllMembers, maximum, unit, thrown, node.hashCode()) - visitor.visitMethod annotatedNode - visitor.visitClass annotatedNode.declaringClass - } else if (!applyToAllMembers && annotatedNode instanceof FieldNode) { - // only guard this particular field (plus initCode for class) - def visitor = new TimedInterruptionVisitor(source, checkOnMethodStart, applyToAllClasses, applyToAllMembers, maximum, unit, thrown, node.hashCode()) - visitor.visitField annotatedNode - visitor.visitClass annotatedNode.declaringClass - } else if (!applyToAllMembers && annotatedNode instanceof DeclarationExpression) { - // only guard this particular declaration (plus initCode for class) - def visitor = new TimedInterruptionVisitor(source, checkOnMethodStart, applyToAllClasses, applyToAllMembers, maximum, unit, thrown, node.hashCode()) - visitor.visitDeclarationExpression annotatedNode - visitor.visitClass annotatedNode.declaringClass - } else { - // only guard the script class - source.getAST()?.classes?.each { ClassNode it -> - if (it.isScript()) { - def visitor = new TimedInterruptionVisitor(source, checkOnMethodStart, applyToAllClasses, applyToAllMembers, maximum, unit, thrown, node.hashCode()) - visitor.visitClass(it) - } - } - } - } - - static def getConstantAnnotationParameter(AnnotationNode node, String parameterName, Class type, defaultValue) { - def member = node.getMember(parameterName) - if (member) { - if (member instanceof ConstantExpression) { - // TODO not sure this try offers value - testing Groovy annotation type handing - throw GroovyBugError or remove? - try { - return member.value.asType(type) - } catch (ignore) { - internalError("Expecting boolean value for ${parameterName} annotation parameter. Found $member") - } - } else { - internalError("Expecting boolean value for ${parameterName} annotation parameter. Found $member") - } - } - return defaultValue - } - - private static void internalError(String message) { - throw new RuntimeException("Internal error: $message") - } - - private static class TimedInterruptionVisitor extends ClassCodeVisitorSupport { - final private SourceUnit source - final private boolean checkOnMethodStart - final private boolean applyToAllClasses - final private boolean applyToAllMembers - private FieldNode expireTimeField = null - private FieldNode startTimeField = null - private final Expression unit - private final maximum - private final ClassNode thrown - private final String basename - - TimedInterruptionVisitor(source, checkOnMethodStart, applyToAllClasses, applyToAllMembers, maximum, unit, thrown, hash) { - this.source = source - this.checkOnMethodStart = checkOnMethodStart - this.applyToAllClasses = applyToAllClasses - this.applyToAllMembers = applyToAllMembers - this.unit = unit - this.maximum = maximum - this.thrown = thrown - this.basename = 'timedInterrupt' + hash - } - - /** - * @return Returns the interruption check statement. - */ - final createInterruptStatement() { - ifS( - - ltX( - propX(varX("this"), basename + '$expireTime'), - callX(make(System), 'nanoTime') - ), - throwS( - ctorX(thrown, - args( - plusX( - plusX( - constX('Execution timed out after ' + maximum + ' '), - callX(callX(unit, 'name'), 'toLowerCase', propX(classX(Locale), 'US')) - ), - plusX( - constX('. Start time: '), - propX(varX("this"), basename + '$startTime') - ) - ) - - ) - ) - ) - ) - } - - /** - * Takes a statement and wraps it into a block statement which first element is the interruption check statement. - * @param statement the statement to be wrapped - * @return a {@link BlockStatement block statement} which first element is for checking interruption, and the - * second one the statement to be wrapped. - */ - private wrapBlock(statement) { - def stmt = new BlockStatement(); - stmt.addStatement(createInterruptStatement()); - stmt.addStatement(statement); - stmt - } - - @Override - void visitClass(ClassNode node) { - if (node.getDeclaredField(basename + '$expireTime')) { - return - } - expireTimeField = node.addField(basename + '$expireTime', - ACC_FINAL | ACC_PRIVATE, - ClassHelper.long_TYPE, - plusX( - callX(make(System), 'nanoTime'), - callX( - propX(classX(TimeUnit), 'NANOSECONDS'), - 'convert', - args(constX(maximum, true), unit) - ) - ) - ); - expireTimeField.synthetic = true - startTimeField = node.addField(basename + '$startTime', - ACC_FINAL | ACC_PRIVATE, - make(Date), - ctorX(make(Date)) - ) - startTimeField.synthetic = true - - // force these fields to be initialized first - node.fields.remove(expireTimeField) - node.fields.remove(startTimeField) - node.fields.add(0, startTimeField) - node.fields.add(0, expireTimeField) - if (applyToAllMembers) { - super.visitClass node - } - } - - @Override - void visitClosureExpression(ClosureExpression closureExpr) { - def code = closureExpr.code - if (code instanceof BlockStatement) { - code.statements.add(0, createInterruptStatement()) - } else { - closureExpr.code = wrapBlock(code) - } - super.visitClosureExpression closureExpr - } - - @Override - void visitField(FieldNode node) { - if (!node.isStatic() && !node.isSynthetic()) { - super.visitField node - } - } - - @Override - void visitProperty(PropertyNode node) { - if (!node.isStatic() && !node.isSynthetic()) { - super.visitProperty node - } - } - - /** - * Shortcut method which avoids duplicating code for every type of loop. - * Actually wraps the loopBlock of different types of loop statements. - */ - private visitLoop(loopStatement) { - def statement = loopStatement.loopBlock - loopStatement.loopBlock = wrapBlock(statement) - } - - @Override - void visitForLoop(ForStatement forStatement) { - visitLoop(forStatement) - super.visitForLoop(forStatement) - } - - @Override - void visitDoWhileLoop(final DoWhileStatement doWhileStatement) { - visitLoop(doWhileStatement) - super.visitDoWhileLoop(doWhileStatement) - } - - @Override - void visitWhileLoop(final WhileStatement whileStatement) { - visitLoop(whileStatement) - super.visitWhileLoop(whileStatement) - } - - @Override - void visitMethod(MethodNode node) { - if (checkOnMethodStart && !node.isSynthetic() && !node.isStatic() && !node.isAbstract()) { - def code = node.code - node.code = wrapBlock(code); - } - if (!node.isSynthetic() && !node.isStatic()) { - super.visitMethod(node) - } - } - - protected SourceUnit getSourceUnit() { - return source; - } - } -} http://git-wip-us.apache.org/repos/asf/groovy/blob/a6c82ad0/src/main/groovy/org/codehaus/groovy/transform/ASTTestTransformation.groovy ---------------------------------------------------------------------- diff --git a/src/main/groovy/org/codehaus/groovy/transform/ASTTestTransformation.groovy b/src/main/groovy/org/codehaus/groovy/transform/ASTTestTransformation.groovy new file mode 100644 index 0000000..dcfe314 --- /dev/null +++ b/src/main/groovy/org/codehaus/groovy/transform/ASTTestTransformation.groovy @@ -0,0 +1,233 @@ +/* + * 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.CompilationUnitAware +import org.codehaus.groovy.ast.ASTNode +import org.codehaus.groovy.ast.AnnotationNode +import org.codehaus.groovy.ast.ClassCodeVisitorSupport +import org.codehaus.groovy.ast.ClassHelper +import org.codehaus.groovy.ast.ClassNode +import org.codehaus.groovy.ast.MethodNode +import org.codehaus.groovy.ast.expr.ClosureExpression +import org.codehaus.groovy.ast.expr.PropertyExpression +import org.codehaus.groovy.ast.expr.VariableExpression +import org.codehaus.groovy.ast.stmt.Statement +import org.codehaus.groovy.control.CompilationUnit +import org.codehaus.groovy.control.CompilePhase +import org.codehaus.groovy.control.CompilerConfiguration +import org.codehaus.groovy.control.ErrorCollector +import org.codehaus.groovy.control.Janitor +import org.codehaus.groovy.control.ProcessingUnit +import org.codehaus.groovy.control.SourceUnit +import org.codehaus.groovy.control.customizers.ImportCustomizer +import org.codehaus.groovy.control.io.ReaderSource +import org.codehaus.groovy.runtime.MethodClosure +import org.codehaus.groovy.syntax.SyntaxException +import org.codehaus.groovy.tools.Utilities + +import static org.codehaus.groovy.ast.tools.GeneralUtils.classX +import static org.codehaus.groovy.ast.tools.GeneralUtils.propX + +@GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS) +class ASTTestTransformation extends AbstractASTTransformation implements CompilationUnitAware { + private CompilationUnit compilationUnit + + void visit(final ASTNode[] nodes, final SourceUnit source) { + AnnotationNode annotationNode = nodes[0] + def member = annotationNode.getMember('phase') + def phase = null + if (member) { + if (member instanceof VariableExpression) { + phase = CompilePhase.valueOf(member.text) + } else if (member instanceof PropertyExpression) { + phase = CompilePhase.valueOf(member.propertyAsString) + } + annotationNode.setMember('phase', propX(classX(ClassHelper.make(CompilePhase)), phase.toString())) + } + member = annotationNode.getMember('value') + if (member && !(member instanceof ClosureExpression)) { + throw new SyntaxException("ASTTest value must be a closure", member.getLineNumber(), member.getColumnNumber()) + } + if (!member && !annotationNode.getNodeMetaData(ASTTestTransformation)) { + throw new SyntaxException("Missing test expression", annotationNode.getLineNumber(), annotationNode.getColumnNumber()) + } + // convert value into node metadata so that the expression doesn't mix up with other AST xforms like type checking + annotationNode.putNodeMetaData(ASTTestTransformation, member) + annotationNode.getMembers().remove('value') + + def pcallback = compilationUnit.progressCallback + def callback = new CompilationUnit.ProgressCallback() { + Binding binding = new Binding([:].withDefault {null}) + + @Override + void call(final ProcessingUnit context, final int phaseRef) { + if (phase==null || phaseRef == phase.phaseNumber) { + ClosureExpression testClosure = nodes[0].getNodeMetaData(ASTTestTransformation) + StringBuilder sb = new StringBuilder() + for (int i = testClosure.lineNumber; i <= testClosure.lastLineNumber; i++) { + sb.append(source.source.getLine(i, new Janitor())).append('\n') + } + def testSource = sb.substring(testClosure.columnNumber + 1, sb.length()) + testSource = testSource.substring(0, testSource.lastIndexOf('}')) + CompilerConfiguration config = new CompilerConfiguration() + def customizer = new ImportCustomizer() + config.addCompilationCustomizers(customizer) + binding['sourceUnit'] = source + binding['node'] = nodes[1] + binding['lookup'] = new MethodClosure(LabelFinder, "lookup").curry(nodes[1]) + binding['compilationUnit'] = compilationUnit + binding['compilePhase'] = CompilePhase.fromPhaseNumber(phaseRef) + + GroovyShell shell = new GroovyShell(binding, config) + + source.AST.imports.each { + customizer.addImport(it.alias, it.type.name) + } + source.AST.starImports.each { + customizer.addStarImports(it.packageName) + } + source.AST.staticImports.each { + customizer.addStaticImport(it.value.alias, it.value.type.name, it.value.fieldName) + } + source.AST.staticStarImports.each { + customizer.addStaticStars(it.value.className) + } + shell.evaluate(testSource) + } + } + } + + if (pcallback!=null) { + if (pcallback instanceof ProgressCallbackChain) { + pcallback.addCallback(callback) + } else { + pcallback = new ProgressCallbackChain(pcallback, callback) + } + callback = pcallback + } + + compilationUnit.setProgressCallback(callback) + + } + + void setCompilationUnit(final CompilationUnit unit) { + this.compilationUnit = unit + } + + private static class AssertionSourceDelegatingSourceUnit extends SourceUnit { + private final ReaderSource delegate + + AssertionSourceDelegatingSourceUnit(final String name, final ReaderSource source, final CompilerConfiguration flags, final GroovyClassLoader loader, final ErrorCollector er) { + super(name, '', flags, loader, er) + delegate = source + } + + @Override + String getSample(final int line, final int column, final Janitor janitor) { + String sample = null; + String text = delegate.getLine(line, janitor); + + if (text != null) { + if (column > 0) { + String marker = Utilities.repeatString(" ", column - 1) + "^"; + + if (column > 40) { + int start = column - 30 - 1; + int end = (column + 10 > text.length() ? text.length() : column + 10 - 1); + sample = " " + text.substring(start, end) + Utilities.eol() + " " + + marker.substring(start, marker.length()); + } else { + sample = " " + text + Utilities.eol() + " " + marker; + } + } else { + sample = text; + } + } + + return sample; + + } + + } + + private static class ProgressCallbackChain extends CompilationUnit.ProgressCallback { + + private final List<CompilationUnit.ProgressCallback> chain = new LinkedList<CompilationUnit.ProgressCallback>() + + ProgressCallbackChain(CompilationUnit.ProgressCallback... callbacks) { + if (callbacks!=null) { + callbacks.each { addCallback(it) } + } + } + + public void addCallback(CompilationUnit.ProgressCallback callback) { + chain << callback + } + + @Override + void call(final ProcessingUnit context, final int phase) { + chain*.call(context, phase) + } + } + + public static class LabelFinder extends ClassCodeVisitorSupport { + + public static List<Statement> lookup(MethodNode node, String label) { + LabelFinder finder = new LabelFinder(label, null) + node.code.visit(finder) + + finder.targets + } + + public static List<Statement> lookup(ClassNode node, String label) { + LabelFinder finder = new LabelFinder(label, null) + node.methods*.code*.visit(finder) + node.declaredConstructors*.code*.visit(finder) + + finder.targets + } + + private final String label + private final SourceUnit unit + + private final List<Statement> targets = new LinkedList<Statement>(); + + LabelFinder(final String label, final SourceUnit unit) { + this.label = label + this.unit = unit; + } + + @Override + protected SourceUnit getSourceUnit() { + unit + } + + @Override + protected void visitStatement(final Statement statement) { + super.visitStatement(statement) + if (statement.statementLabel==label) targets << statement + } + + List<Statement> getTargets() { + return Collections.unmodifiableList(targets) + } + } + +} http://git-wip-us.apache.org/repos/asf/groovy/blob/a6c82ad0/src/main/groovy/org/codehaus/groovy/transform/ConditionalInterruptibleASTTransformation.groovy ---------------------------------------------------------------------- diff --git a/src/main/groovy/org/codehaus/groovy/transform/ConditionalInterruptibleASTTransformation.groovy b/src/main/groovy/org/codehaus/groovy/transform/ConditionalInterruptibleASTTransformation.groovy new file mode 100644 index 0000000..2cda121 --- /dev/null +++ b/src/main/groovy/org/codehaus/groovy/transform/ConditionalInterruptibleASTTransformation.groovy @@ -0,0 +1,145 @@ +/* + * 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.ConditionalInterrupt +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.FieldNode +import org.codehaus.groovy.ast.MethodNode +import org.codehaus.groovy.ast.Parameter +import org.codehaus.groovy.ast.PropertyNode +import org.codehaus.groovy.ast.expr.ArgumentListExpression +import org.codehaus.groovy.ast.expr.ClosureExpression +import org.codehaus.groovy.ast.expr.Expression +import org.codehaus.groovy.ast.expr.MethodCallExpression +import org.codehaus.groovy.ast.expr.VariableExpression +import org.codehaus.groovy.ast.tools.ClosureUtils +import org.codehaus.groovy.control.CompilePhase + +/** + * Allows "interrupt-safe" executions of scripts by adding a custom conditional + * check on loops (for, while, do) and first statement of closures. By default, also adds an interrupt check + * statement on the beginning of method calls. + * + * @see groovy.transform.ConditionalInterrupt + * @author Cedric Champeau + * @author Hamlet D'Arcy + * @author Paul King + * @since 1.8.0 + */ +@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION) +public class ConditionalInterruptibleASTTransformation extends AbstractInterruptibleASTTransformation { + + private static final ClassNode MY_TYPE = ClassHelper.make(ConditionalInterrupt) + + private ClosureExpression conditionNode + private String conditionMethod + private MethodCallExpression conditionCallExpression + private ClassNode currentClass + + protected ClassNode type() { + return MY_TYPE + } + + protected void setupTransform(AnnotationNode node) { + super.setupTransform(node) + def member = node.getMember("value") + if (!member || !(member instanceof ClosureExpression)) internalError("Expected closure value for annotation parameter 'value'. Found $member") + conditionNode = member; + conditionMethod = 'conditionalTransform' + node.hashCode() + '$condition' + conditionCallExpression = new MethodCallExpression(new VariableExpression('this'), conditionMethod, new ArgumentListExpression()) + } + + protected String getErrorMessage() { + 'Execution interrupted. The following condition failed: ' + convertClosureToSource(conditionNode) + } + + void visitClass(ClassNode type) { + currentClass = type + def method = type.addMethod(conditionMethod, ACC_PRIVATE | ACC_SYNTHETIC, ClassHelper.OBJECT_TYPE, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, conditionNode.code) + method.synthetic = true + if (applyToAllMembers) { + super.visitClass(type) + } + } + + protected Expression createCondition() { + conditionCallExpression + } + + @Override + void visitAnnotations(AnnotatedNode node) { + // this transformation does not apply on annotation nodes + // visiting could lead to stack overflows + } + + @Override + void visitField(FieldNode node) { + if (!node.isStatic() && !node.isSynthetic()) { + super.visitField node + } + } + + @Override + void visitProperty(PropertyNode node) { + if (!node.isStatic() && !node.isSynthetic()) { + super.visitProperty node + } + } + + @Override + void visitClosureExpression(ClosureExpression closureExpr) { + if (closureExpr == conditionNode) return // do not visit the closure from the annotation itself + def code = closureExpr.code + closureExpr.code = wrapBlock(code) + super.visitClosureExpression closureExpr + } + + @Override + void visitMethod(MethodNode node) { + if (node.name == conditionMethod && !node.isSynthetic()) return // do not visit the generated method + if (node.name == 'run' && currentClass.isScript() && node.parameters.length == 0) { + // the run() method should not have the statement added, otherwise the script binding won't be set before + // the condition is actually tested + super.visitMethod(node) + } else { + if (checkOnMethodStart && !node.isSynthetic() && !node.isStatic() && !node.isAbstract()) { + def code = node.code + node.code = wrapBlock(code); + } + if (!node.isSynthetic() && !node.isStatic()) super.visitMethod(node) + } + } + + /** + * Converts a ClosureExpression into the String source. + * @param expression a closure + * @return the source the closure was created from + */ + private String convertClosureToSource(ClosureExpression expression) { + try { + return ClosureUtils.convertClosureToSource(this.source.source, expression); + } catch(Exception e) { + return e.message + } + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/a6c82ad0/src/main/groovy/org/codehaus/groovy/transform/ThreadInterruptibleASTTransformation.groovy ---------------------------------------------------------------------- diff --git a/src/main/groovy/org/codehaus/groovy/transform/ThreadInterruptibleASTTransformation.groovy b/src/main/groovy/org/codehaus/groovy/transform/ThreadInterruptibleASTTransformation.groovy new file mode 100644 index 0000000..a4fb4c3 --- /dev/null +++ b/src/main/groovy/org/codehaus/groovy/transform/ThreadInterruptibleASTTransformation.groovy @@ -0,0 +1,98 @@ +/* + * 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.CompileStatic +import groovy.transform.ThreadInterrupt +import org.codehaus.groovy.ast.ClassHelper +import org.codehaus.groovy.ast.ClassNode +import org.codehaus.groovy.ast.MethodNode +import org.codehaus.groovy.ast.Parameter +import org.codehaus.groovy.ast.expr.ArgumentListExpression +import org.codehaus.groovy.ast.expr.ClassExpression +import org.codehaus.groovy.ast.expr.ClosureExpression +import org.codehaus.groovy.ast.expr.Expression +import org.codehaus.groovy.ast.expr.MethodCallExpression +import org.codehaus.groovy.control.CompilePhase + +/** + * Allows "interrupt-safe" executions of scripts by adding Thread.currentThread().isInterrupted() + * checks on loops (for, while, do) and first statement of closures. By default, also adds an interrupt check + * statement on the beginning of method calls. + * + * @see groovy.transform.ThreadInterrupt + * + * @author Cedric Champeau + * @author Hamlet D'Arcy + * + * @since 1.8.0 + */ +@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION) +@CompileStatic +public class ThreadInterruptibleASTTransformation extends AbstractInterruptibleASTTransformation { + + private static final ClassNode MY_TYPE = ClassHelper.make(ThreadInterrupt) + private static final ClassNode THREAD_TYPE = ClassHelper.make(Thread) + private static final MethodNode CURRENTTHREAD_METHOD + private static final MethodNode ISINTERRUPTED_METHOD + + static { + CURRENTTHREAD_METHOD = THREAD_TYPE.getMethod('currentThread', Parameter.EMPTY_ARRAY) + ISINTERRUPTED_METHOD = THREAD_TYPE.getMethod('isInterrupted', Parameter.EMPTY_ARRAY) + } + + protected ClassNode type() { + return MY_TYPE; + } + + protected String getErrorMessage() { + 'Execution interrupted. The current thread has been interrupted.' + } + + protected Expression createCondition() { + def currentThread = new MethodCallExpression(new ClassExpression(THREAD_TYPE), + 'currentThread', + ArgumentListExpression.EMPTY_ARGUMENTS) + currentThread.methodTarget = CURRENTTHREAD_METHOD + def isInterrupted = new MethodCallExpression( + currentThread, + 'isInterrupted', ArgumentListExpression.EMPTY_ARGUMENTS) + isInterrupted.methodTarget = ISINTERRUPTED_METHOD + [currentThread, isInterrupted]*.implicitThis = false + + isInterrupted + } + + + @Override + public void visitClosureExpression(ClosureExpression closureExpr) { + def code = closureExpr.code + closureExpr.code = wrapBlock(code) + super.visitClosureExpression closureExpr + } + + @Override + public void visitMethod(MethodNode node) { + if (checkOnMethodStart && !node.isSynthetic() && !node.isAbstract()) { + def code = node.code + node.code = wrapBlock(code); + } + super.visitMethod(node) + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/a6c82ad0/src/main/groovy/org/codehaus/groovy/transform/TimedInterruptibleASTTransformation.groovy ---------------------------------------------------------------------- diff --git a/src/main/groovy/org/codehaus/groovy/transform/TimedInterruptibleASTTransformation.groovy b/src/main/groovy/org/codehaus/groovy/transform/TimedInterruptibleASTTransformation.groovy new file mode 100644 index 0000000..fbc923b --- /dev/null +++ b/src/main/groovy/org/codehaus/groovy/transform/TimedInterruptibleASTTransformation.groovy @@ -0,0 +1,321 @@ +/* + * 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.TimedInterrupt +import org.codehaus.groovy.ast.ASTNode +import org.codehaus.groovy.ast.AnnotatedNode +import org.codehaus.groovy.ast.AnnotationNode +import org.codehaus.groovy.ast.ClassCodeVisitorSupport +import org.codehaus.groovy.ast.ClassHelper +import org.codehaus.groovy.ast.ClassNode +import org.codehaus.groovy.ast.FieldNode +import org.codehaus.groovy.ast.MethodNode +import org.codehaus.groovy.ast.PropertyNode +import org.codehaus.groovy.ast.expr.ClosureExpression +import org.codehaus.groovy.ast.expr.ConstantExpression +import org.codehaus.groovy.ast.expr.DeclarationExpression +import org.codehaus.groovy.ast.expr.Expression +import org.codehaus.groovy.ast.stmt.BlockStatement +import org.codehaus.groovy.ast.stmt.DoWhileStatement +import org.codehaus.groovy.ast.stmt.ForStatement +import org.codehaus.groovy.ast.stmt.WhileStatement +import org.codehaus.groovy.control.CompilePhase +import org.codehaus.groovy.control.SourceUnit + +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException + +import static org.codehaus.groovy.ast.ClassHelper.make +import static org.codehaus.groovy.ast.tools.GeneralUtils.args +import static org.codehaus.groovy.ast.tools.GeneralUtils.callX +import static org.codehaus.groovy.ast.tools.GeneralUtils.classX +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.ltX +import static org.codehaus.groovy.ast.tools.GeneralUtils.plusX +import static org.codehaus.groovy.ast.tools.GeneralUtils.propX +import static org.codehaus.groovy.ast.tools.GeneralUtils.throwS +import static org.codehaus.groovy.ast.tools.GeneralUtils.varX + +/** + * Allows "interrupt-safe" executions of scripts by adding timer expiration + * checks on loops (for, while, do) and first statement of closures. By default, + * also adds an interrupt check statement on the beginning of method calls. + * + * @author Cedric Champeau + * @author Hamlet D'Arcy + * @author Paul King + * @see groovy.transform.ThreadInterrupt + * @since 1.8.0 + */ +@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION) +public class TimedInterruptibleASTTransformation extends AbstractASTTransformation { + + private static final ClassNode MY_TYPE = make(TimedInterrupt) + private static final String CHECK_METHOD_START_MEMBER = 'checkOnMethodStart' + private static final String APPLY_TO_ALL_CLASSES = 'applyToAllClasses' + private static final String APPLY_TO_ALL_MEMBERS = 'applyToAllMembers' + private static final String THROWN_EXCEPTION_TYPE = "thrown" + + public void visit(ASTNode[] nodes, SourceUnit source) { + init(nodes, source); + AnnotationNode node = nodes[0] + AnnotatedNode annotatedNode = nodes[1] + if (!MY_TYPE.equals(node.getClassNode())) { + internalError("Transformation called from wrong annotation: $node.classNode.name") + } + + def checkOnMethodStart = getConstantAnnotationParameter(node, CHECK_METHOD_START_MEMBER, Boolean.TYPE, true) + def applyToAllMembers = getConstantAnnotationParameter(node, APPLY_TO_ALL_MEMBERS, Boolean.TYPE, true) + def applyToAllClasses = applyToAllMembers ? getConstantAnnotationParameter(node, APPLY_TO_ALL_CLASSES, Boolean.TYPE, true) : false + def maximum = getConstantAnnotationParameter(node, 'value', Long.TYPE, Long.MAX_VALUE) + def thrown = AbstractInterruptibleASTTransformation.getClassAnnotationParameter(node, THROWN_EXCEPTION_TYPE, make(TimeoutException)) + + Expression unit = node.getMember('unit') ?: propX(classX(TimeUnit), "SECONDS") + + // should be limited to the current SourceUnit or propagated to the whole CompilationUnit + // DO NOT inline visitor creation in code below. It has state that must not persist between calls + if (applyToAllClasses) { + // guard every class and method defined in this script + source.getAST()?.classes?.each { ClassNode it -> + def visitor = new TimedInterruptionVisitor(source, checkOnMethodStart, applyToAllClasses, applyToAllMembers, maximum, unit, thrown, node.hashCode()) + visitor.visitClass(it) + } + } else if (annotatedNode instanceof ClassNode) { + // only guard this particular class + def visitor = new TimedInterruptionVisitor(source, checkOnMethodStart, applyToAllClasses, applyToAllMembers, maximum, unit, thrown, node.hashCode()) + visitor.visitClass annotatedNode + } else if (!applyToAllMembers && annotatedNode instanceof MethodNode) { + // only guard this particular method (plus initCode for class) + def visitor = new TimedInterruptionVisitor(source, checkOnMethodStart, applyToAllClasses, applyToAllMembers, maximum, unit, thrown, node.hashCode()) + visitor.visitMethod annotatedNode + visitor.visitClass annotatedNode.declaringClass + } else if (!applyToAllMembers && annotatedNode instanceof FieldNode) { + // only guard this particular field (plus initCode for class) + def visitor = new TimedInterruptionVisitor(source, checkOnMethodStart, applyToAllClasses, applyToAllMembers, maximum, unit, thrown, node.hashCode()) + visitor.visitField annotatedNode + visitor.visitClass annotatedNode.declaringClass + } else if (!applyToAllMembers && annotatedNode instanceof DeclarationExpression) { + // only guard this particular declaration (plus initCode for class) + def visitor = new TimedInterruptionVisitor(source, checkOnMethodStart, applyToAllClasses, applyToAllMembers, maximum, unit, thrown, node.hashCode()) + visitor.visitDeclarationExpression annotatedNode + visitor.visitClass annotatedNode.declaringClass + } else { + // only guard the script class + source.getAST()?.classes?.each { ClassNode it -> + if (it.isScript()) { + def visitor = new TimedInterruptionVisitor(source, checkOnMethodStart, applyToAllClasses, applyToAllMembers, maximum, unit, thrown, node.hashCode()) + visitor.visitClass(it) + } + } + } + } + + static def getConstantAnnotationParameter(AnnotationNode node, String parameterName, Class type, defaultValue) { + def member = node.getMember(parameterName) + if (member) { + if (member instanceof ConstantExpression) { + // TODO not sure this try offers value - testing Groovy annotation type handing - throw GroovyBugError or remove? + try { + return member.value.asType(type) + } catch (ignore) { + internalError("Expecting boolean value for ${parameterName} annotation parameter. Found $member") + } + } else { + internalError("Expecting boolean value for ${parameterName} annotation parameter. Found $member") + } + } + return defaultValue + } + + private static void internalError(String message) { + throw new RuntimeException("Internal error: $message") + } + + private static class TimedInterruptionVisitor extends ClassCodeVisitorSupport { + final private SourceUnit source + final private boolean checkOnMethodStart + final private boolean applyToAllClasses + final private boolean applyToAllMembers + private FieldNode expireTimeField = null + private FieldNode startTimeField = null + private final Expression unit + private final maximum + private final ClassNode thrown + private final String basename + + TimedInterruptionVisitor(source, checkOnMethodStart, applyToAllClasses, applyToAllMembers, maximum, unit, thrown, hash) { + this.source = source + this.checkOnMethodStart = checkOnMethodStart + this.applyToAllClasses = applyToAllClasses + this.applyToAllMembers = applyToAllMembers + this.unit = unit + this.maximum = maximum + this.thrown = thrown + this.basename = 'timedInterrupt' + hash + } + + /** + * @return Returns the interruption check statement. + */ + final createInterruptStatement() { + ifS( + + ltX( + propX(varX("this"), basename + '$expireTime'), + callX(make(System), 'nanoTime') + ), + throwS( + ctorX(thrown, + args( + plusX( + plusX( + constX('Execution timed out after ' + maximum + ' '), + callX(callX(unit, 'name'), 'toLowerCase', propX(classX(Locale), 'US')) + ), + plusX( + constX('. Start time: '), + propX(varX("this"), basename + '$startTime') + ) + ) + + ) + ) + ) + ) + } + + /** + * Takes a statement and wraps it into a block statement which first element is the interruption check statement. + * @param statement the statement to be wrapped + * @return a {@link BlockStatement block statement} which first element is for checking interruption, and the + * second one the statement to be wrapped. + */ + private wrapBlock(statement) { + def stmt = new BlockStatement(); + stmt.addStatement(createInterruptStatement()); + stmt.addStatement(statement); + stmt + } + + @Override + void visitClass(ClassNode node) { + if (node.getDeclaredField(basename + '$expireTime')) { + return + } + expireTimeField = node.addField(basename + '$expireTime', + ACC_FINAL | ACC_PRIVATE, + ClassHelper.long_TYPE, + plusX( + callX(make(System), 'nanoTime'), + callX( + propX(classX(TimeUnit), 'NANOSECONDS'), + 'convert', + args(constX(maximum, true), unit) + ) + ) + ); + expireTimeField.synthetic = true + startTimeField = node.addField(basename + '$startTime', + ACC_FINAL | ACC_PRIVATE, + make(Date), + ctorX(make(Date)) + ) + startTimeField.synthetic = true + + // force these fields to be initialized first + node.fields.remove(expireTimeField) + node.fields.remove(startTimeField) + node.fields.add(0, startTimeField) + node.fields.add(0, expireTimeField) + if (applyToAllMembers) { + super.visitClass node + } + } + + @Override + void visitClosureExpression(ClosureExpression closureExpr) { + def code = closureExpr.code + if (code instanceof BlockStatement) { + code.statements.add(0, createInterruptStatement()) + } else { + closureExpr.code = wrapBlock(code) + } + super.visitClosureExpression closureExpr + } + + @Override + void visitField(FieldNode node) { + if (!node.isStatic() && !node.isSynthetic()) { + super.visitField node + } + } + + @Override + void visitProperty(PropertyNode node) { + if (!node.isStatic() && !node.isSynthetic()) { + super.visitProperty node + } + } + + /** + * Shortcut method which avoids duplicating code for every type of loop. + * Actually wraps the loopBlock of different types of loop statements. + */ + private visitLoop(loopStatement) { + def statement = loopStatement.loopBlock + loopStatement.loopBlock = wrapBlock(statement) + } + + @Override + void visitForLoop(ForStatement forStatement) { + visitLoop(forStatement) + super.visitForLoop(forStatement) + } + + @Override + void visitDoWhileLoop(final DoWhileStatement doWhileStatement) { + visitLoop(doWhileStatement) + super.visitDoWhileLoop(doWhileStatement) + } + + @Override + void visitWhileLoop(final WhileStatement whileStatement) { + visitLoop(whileStatement) + super.visitWhileLoop(whileStatement) + } + + @Override + void visitMethod(MethodNode node) { + if (checkOnMethodStart && !node.isSynthetic() && !node.isStatic() && !node.isAbstract()) { + def code = node.code + node.code = wrapBlock(code); + } + if (!node.isSynthetic() && !node.isStatic()) { + super.visitMethod(node) + } + } + + protected SourceUnit getSourceUnit() { + return source; + } + } +}
