/* * * 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.apache.flex.compiler.internal.tree.as; import static com.google.common.base.Predicates.and; import static com.google.common.collect.Collections2.filter; import java.io.IOException; import java.io.Reader; import java.io.StringReader; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.concurrent.locks.ReentrantLock; import org.apache.flex.compiler.common.ASImportTarget; import org.apache.flex.compiler.common.IImportTarget; import org.apache.flex.compiler.constants.IASLanguageConstants; import org.apache.flex.compiler.constants.INamespaceConstants; import org.apache.flex.compiler.definitions.IDefinition; import org.apache.flex.compiler.definitions.IFunctionDefinition.FunctionClassification; import org.apache.flex.compiler.definitions.IParameterDefinition; import org.apache.flex.compiler.definitions.references.IReference; import org.apache.flex.compiler.definitions.references.ReferenceFactory; import org.apache.flex.compiler.internal.definitions.FunctionDefinition; import org.apache.flex.compiler.internal.definitions.NamespaceDefinition; import org.apache.flex.compiler.internal.definitions.ParameterDefinition; import org.apache.flex.compiler.internal.definitions.VariableDefinition; import org.apache.flex.compiler.internal.parsing.as.ASParser; import org.apache.flex.compiler.internal.parsing.as.ASToken; import org.apache.flex.compiler.internal.parsing.as.ASTokenTypes; import org.apache.flex.compiler.internal.parsing.as.ConfigProcessor; import org.apache.flex.compiler.internal.scopes.ASFileScope; import org.apache.flex.compiler.internal.scopes.ASScope; import org.apache.flex.compiler.internal.scopes.ClosureScope; import org.apache.flex.compiler.internal.scopes.FunctionScope; import org.apache.flex.compiler.internal.semantics.PostProcessStep; import org.apache.flex.compiler.internal.tree.as.parts.FunctionContentsPart; import org.apache.flex.compiler.internal.tree.as.parts.IFunctionContentsPart; import org.apache.flex.compiler.parsing.IASToken; import org.apache.flex.compiler.problems.CanNotInsertSemicolonProblem; import org.apache.flex.compiler.problems.ICompilerProblem; import org.apache.flex.compiler.problems.InternalCompilerProblem2; import org.apache.flex.compiler.scopes.IASScope; import org.apache.flex.compiler.tree.ASTNodeID; import org.apache.flex.compiler.tree.as.IASNode; import org.apache.flex.compiler.tree.as.ICommonClassNode; import org.apache.flex.compiler.tree.as.IContainerNode; import org.apache.flex.compiler.tree.as.IDefinitionNode; import org.apache.flex.compiler.tree.as.IExpressionNode; import org.apache.flex.compiler.tree.as.IFunctionNode; import org.apache.flex.compiler.tree.as.INamespaceDecorationNode; import org.apache.flex.compiler.tree.as.IParameterNode; import org.apache.flex.compiler.tree.as.IScopedNode; import org.apache.flex.compiler.tree.as.ITypeNode; import org.apache.flex.compiler.tree.as.IVariableNode; import org.apache.flex.compiler.workspaces.IWorkspace; import com.google.common.base.Predicate; /** * ActionScript parse tree node representing a function definition */ public class FunctionNode extends BaseTypedDefinitionNode implements IFunctionNode { /** * Constructor. * <p> * Creates a {@code FunctionNode} from the "new" keyword token and the * function name node. * * @param functionKeyword function keyword * @param nameNode node containing the name of this function */ public FunctionNode(IASToken functionKeyword, IdentifierNode nameNode) { init(nameNode); if (functionKeyword != null) contentsPart.setKeywordNode(new KeywordNode(functionKeyword)); } /** * Constructor. * <p> * Creates a new FunctionNode with a custom part for this functions * contents. * * @param node the name of the node * @param part the {@link IFunctionContentsPart} */ public FunctionNode(IdentifierNode node, IFunctionContentsPart part) { super.init(node); contentsPart = part; } /** * Contents of the function, including args, etc */ protected IFunctionContentsPart contentsPart; /** * Does this method need a Definition added for "arguments". This will be * set to true during scope building if we encounter an IdentifierNode that * refers to "arguments". */ boolean needsArguments = false; /** * The configuration processor used to re-parse the function body. */ private ConfigProcessor configProcessor = null; /** * The open curly token of the function body. */ private ASToken openT = null; /** * True if the function body is a deferred node. */ private boolean isBodyDeferred = false; /** * Lock used when parsing the function body */ private ReentrantLock deferredBodyParsingLock = new ReentrantLock(); /** * A count of the number of calls to parse the function body. The body * won't actually be thrown away until the count reaches zero again. */ private int deferredBodyParsingReferenceCount = 0; /** * Cached function body text. If it's empty, the function body has to be * reloaded from the file using a seek-able reader, which might be slow for * large file. */ private String functionBodyText; /** * Save the problems until later if we were parsed from somewhere we don't have a problems collection */ private Collection<ICompilerProblem> parseProblems; // // NodeBase overrides // @Override public ASTNodeID getNodeID() { return ASTNodeID.FunctionID; } @Override public int getSpanningStart() { return getNodeStartForTooling(); } @Override protected void setChildren(boolean fillInOffsets) { addDecorationChildren(fillInOffsets); addChildInOrder(contentsPart.getFunctionKeywordNode(), fillInOffsets); addChildInOrder(nameNode, fillInOffsets); addChildInOrder(contentsPart.getParametersNode(), fillInOffsets); addChildInOrder(typeNode, fillInOffsets); addChildInOrder(contentsPart.getContents(), fillInOffsets); } @Override public void normalize(boolean fillInOffsets) { super.normalize(fillInOffsets); contentsPart.optimize(); } @Override protected void analyze(EnumSet<PostProcessStep> set, ASScope scope, Collection<ICompilerProblem> problems) { if (set.contains(PostProcessStep.POPULATE_SCOPE)) { final IFunctionNode parentFunctionNode = (IFunctionNode)getAncestorOfType(IFunctionNode.class); if (parentFunctionNode != null) parentFunctionNode.rememberLocalFunction(this); FunctionDefinition definition = buildDefinition(); setDefinition(definition); // if the parent is an anonymous function, then don't add the function definition to the scope // we have already added the anonymous function to the scope and this function definition does not // belong to the scope if (this.getParent() instanceof FunctionObjectNode) { String funcName = definition.getBaseName(); if (funcName.length() == 0) scope.setAsContainingScopeOfAnonymousFunction(definition); else { // If the parent is an "anonymous" function with a name, then // the name will have to be visibly within the function body, so it can // call itself recursively. So make a special closure scope // Add a closure scope below the containing scope ASScope closureScope = new ClosureScope(scope); scope = closureScope; // now build the function scope below this.. scope.addDefinition(definition); // This sets the containing scope of the def } } else scope.addDefinition(definition); // This sets the containing scope of the def ScopedBlockNode contents = contentsPart.getContents(); if (contents != null) { ASScope localScope = new FunctionScope(scope, contents); definition.setContainedScope(localScope); scope = localScope; } } if (set.contains(PostProcessStep.RECONNECT_DEFINITIONS)) { reconnectDef(scope); final FunctionDefinition functionDef = this.getDefinition(); if (functionDef != null) { scope = functionDef.getContainedScope(); ScopedBlockNode contents = contentsPart.getContents(); // scope can be null for generated binding wrappers of // getters and setters if (contents != null && scope != null) { contents.reconnectScope(scope); } } } // Recurse on the function parameters. ContainerNode parameters = contentsPart.getParametersNode(); if (parameters != null) { parameters.analyze(set, scope, problems); if (set.contains(PostProcessStep.POPULATE_SCOPE)) { // Set the parameters. IParameterNode[] argumentNodes = getParameterNodes(); int n = argumentNodes.length; ParameterDefinition[] arguments = new ParameterDefinition[n]; for (int i = 0; i < n; i++) { if (argumentNodes[i] instanceof ParameterNode) arguments[i] = (ParameterDefinition)((ParameterNode)argumentNodes[i]).getDefinition(); } ((FunctionDefinition)this.definition).setParameters(arguments); } } // Recurse on the function block. BlockNode contents = contentsPart.getContents(); if (contents != null) contents.analyze(set, scope, problems); if (set.contains(PostProcessStep.POPULATE_SCOPE)) tryAddDefaultArgument(); } /* * For debugging only. * Builds a string such as <code>"doSomething(int, String):void"</code> * from the signature of the function being defined. */ @Override protected boolean buildInnerString(StringBuilder sb) { sb.append(getName()); sb.append('('); IVariableNode[] args = getParameterNodes(); for (int i = 0; i < args.length; i++) { IVariableNode arg = args[i]; sb.append(arg.getVariableType()); if (i < args.length - 1) sb.append(", "); } sb.append(')'); if (getReturnType().length() > 0) sb.append(":" + getReturnType()); return true; } // // TreeNode overrides // @Override protected int getInitialChildCount() { return 4; } // // BaseDefinitionNode overrides overrides // @Override protected void init(ExpressionNodeBase idNode) { super.init(idNode); contentsPart = createContentsPart(); } @Override public INamespaceDecorationNode getNamespaceNode() { INamespaceDecorationNode namespaceNode = super.getNamespaceNode(); if (isConstructor()) { if (namespaceNode != null && namespaceNode.getName().equals(INamespaceConstants.public_)) return namespaceNode; NamespaceIdentifierNode pub = new NamespaceIdentifierNode(INamespaceConstants.public_); pub.span(-1, -1, -1, -1); pub.setDecorationTarget(this); return pub; } return namespaceNode; } @Override public String getNamespace() { INamespaceDecorationNode ns = getNamespaceNode(); if (ns != null) { String nameString = ns.getName(); // If public, just return it. if (nameString.equals(INamespaceConstants.public_)) return nameString; // Otherwise, check to see if we are a constructor. if (isConstructor()) return INamespaceConstants.public_; // Just return the value. return nameString; } // If we are null, make sure to check if we are a constructor. if (isConstructor()) return INamespaceConstants.public_; return null; } @Override public boolean hasNamespace(String namespace) { if (isConstructor()) return namespace.compareTo(INamespaceConstants.public_) == 0; return super.hasNamespace(namespace); } @Override public FunctionDefinition getDefinition() { return (FunctionDefinition)super.getDefinition(); } @Override protected void setDefinition(IDefinition def) { assert def instanceof FunctionDefinition; super.setDefinition(def); } // // IFunctionNode implementations // @Override public boolean isImplicit() { if (getParent() != null) { if (getParent().getParent() instanceof ClassNode) { ClassNode containingClass = (ClassNode)getParent().getParent(); if (containingClass.getDefaultConstructorNode() == this) return true; } else if (getParent().getParent() instanceof InterfaceNode) { InterfaceNode containingInterface = (InterfaceNode)getParent().getParent(); if (containingInterface.getCastFunctionNode() == this) return true; } } return false; } @Override public String getQualifiedName() { String qualifiedName = null; if (isPackageLevelFunction()) { IImportTarget importTarget = ASImportTarget.buildImportFromPackageName(getWorkspace(), getPackageName()); qualifiedName = importTarget.getQualifiedName(getName()); } if (qualifiedName == null) qualifiedName = getName(); return qualifiedName; } @Override public String getShortName() { return getName(); } @Override public final ScopedBlockNode getScopedNode() { return contentsPart.getContents(); } @Override public FunctionClassification getFunctionClassification() { IScopedNode scopedNode = getScopeNode(); IASNode node = scopedNode; if (node instanceof ICommonClassNode || node.getParent() instanceof ICommonClassNode) return FunctionClassification.CLASS_MEMBER; if (node.getParent() instanceof InterfaceNode) return FunctionClassification.INTERFACE_MEMBER; if (node.getParent() instanceof PackageNode) return FunctionClassification.PACKAGE_MEMBER; if (node instanceof FileNode)// this is an include return FunctionClassification.FILE_MEMBER; return FunctionClassification.LOCAL; } @Override public boolean isGetter() { return this instanceof GetterNode; } @Override public boolean isSetter() { return this instanceof SetterNode; } @Override public boolean isConstructor() { String name = getName(); String returnType = getReturnType(); // Allow constructors that have a (bogus) return type if (!returnType.equals("") && !returnType.equals(name) && !isAnyType() && !isVoidType()) { return false; } if (getParent() != null && getParent().getParent() != null && (getParent().getParent() instanceof ClassNode || getParent().getParent() instanceof InterfaceNode)) { if (name.equals(((IDefinitionNode) getParent().getParent()).getShortName())) return true; } return false; } @Override public boolean isCastFunction() { String name = getName(); String returnType = getReturnType(); if (!returnType.equals("") && !returnType.equals(name)) return false; if (getParent() != null && getParent().getParent() != null && getParent().getParent() instanceof ITypeNode) { if (name.equals(((ITypeNode)getParent().getParent()).getShortName())) return true; } return false; } @Override public ContainerNode getParametersContainerNode() { return contentsPart.getParametersNode(); } @Override public IParameterNode[] getParameterNodes() { IParameterNode[] variables = {}; ContainerNode arguments = contentsPart.getParametersNode(); if (arguments != null) { int argumentscount = arguments.getChildCount(); variables = new IParameterNode[argumentscount]; for (int i = 0; i < argumentscount; i++) { IASNode argument = arguments.getChild(i); if (argument instanceof IParameterNode) variables[i] = (IParameterNode)argument; } } return variables; } @Override public IExpressionNode getReturnTypeNode() { return getTypeNode(); } @Override public String getReturnType() { return getTypeName(); } @Override public boolean hasBody() { ScopedBlockNode sbn = getScopedNode(); return sbn.getChildCount() > 0 || sbn.getContainerType() != IContainerNode.ContainerType.SYNTHESIZED; } // // Other methods // protected IFunctionContentsPart createContentsPart() { return new FunctionContentsPart(); } private void tryAddDefaultArgument() { FunctionDefinition def = getDefinition(); ASScope funcScope = def.getContainedScope(); if (needsArguments && funcScope.getLocalDefinitionSetByName(IASLanguageConstants.arguments) == null) { // Add the arguments Array to the function scope // only do this if there is not already a local property, or parameter that is named arguments // and something in the function body references arguments - this should avoid creating the // definition when it's not needed. VariableDefinition argumentsDef = new VariableDefinition(IASLanguageConstants.arguments); argumentsDef.setNamespaceReference(NamespaceDefinition.getDefaultNamespaceDefinition(funcScope)); argumentsDef.setTypeReference(ReferenceFactory.builtinReference(IASLanguageConstants.BuiltinType.ARRAY)); argumentsDef.setImplicit(); funcScope.addDefinition(argumentsDef); } } private void setConstructorIfNeeded(FunctionDefinition funcDef) { if (isConstructor()) { IASNode parentParent = getParent().getParent(); if( parentParent instanceof ClassNode) { ClassNode classNode = (ClassNode)parentParent; if (classNode.getConstructorNode() == null) { if (nameNode instanceof IdentifierNode) { ((IdentifierNode)nameNode).setReferenceValue(classNode.getDefinition()); classNode.constructorNode = this; } } } funcDef.setNamespaceReference(NamespaceDefinition.getCodeModelImplicitDefinitionNamespace()); } } FunctionDefinition buildDefinition() { String definitionName = getName(); // System.out.println("buildDefinition: " + definitionName); FunctionDefinition definition = createFunctionDefinition(definitionName); definition.setNode(this); fillInNamespaceAndModifiers(definition); fillInMetadata(definition); // Set the return type. If a type annotation doesn't appear in the source, // the return type in the definition will be "". IReference returnType = typeNode != null ? typeNode.computeTypeReference() : null; definition.setReturnTypeReference(returnType); definition.setTypeReference(ReferenceFactory.builtinReference(IASLanguageConstants.BuiltinType.FUNCTION)); setConstructorIfNeeded(definition); return definition; } /** * Method to create the correct Definition class - needed so we can create * different Definition types for getters, setters, and plain old functions * * @return A Definition object to represent this function */ protected FunctionDefinition createFunctionDefinition(String name) { return new FunctionDefinition(name); } /** * Get the function keyword * * @return node containing the function keyword */ public KeywordNode getFunctionKeywordNode() { return contentsPart.getFunctionKeywordNode(); } /** * Determine whether this is a package-level function (i.e. a function * defined in a package, as opposed to a class or some other scope) * * @return true if this is a package-level function */ public boolean isPackageLevelFunction() { // regular package-level function IASNode parent = getParent(); IASNode parent2 = parent.getParent(); if (parent instanceof BlockNode && parent2 instanceof PackageNode) return true; // constructor if (parent2 != null) { IASNode parent3 = parent2.getParent(); if (isConstructor() && parent2 instanceof ClassNode && parent3 instanceof BlockNode && parent3.getParent() instanceof PackageNode) { return true; } } return false; } /** * Get the real namespace node, or null if there isn't one. Used by * semantics. This differs from getNamespaceNode above, as it will not * construct an implicit public namespace if one is missing */ public INamespaceDecorationNode getActualNamespaceNode() { return super.getNamespaceNode(); } /** * Is this a constructor of the specified class (assumes that this function * definition is actually located in the body of the specified class) */ public boolean isConstructorOf(ClassNode classNode) { return this.equals(classNode.getConstructorNode()); } /** * Get any saved parsing problems from lazily parsing the function body. * This list will be nulled out after this method is called, * so we don't start leaking parser problems. * * @return Collection of problems encountered while parsing the function * or an empty list if there were none. * */ public Collection<ICompilerProblem> getParsingProblems() { if (parseProblems != null) { Collection<ICompilerProblem> problems = parseProblems; parseProblems = null; return problems; } return Collections.emptyList(); } /** * Build AST for the function body from the buffered function body text. * <p> * Make sure {@link PostProcessStep#POPULATE_SCOPE} has been applied to the * containing {@code FileNode} This method always populate scopes of the * rebuilt function node. If the scopes for the containing nodes weren't * initialized, the rebuilt scopes can attach itself to it's parent. */ public final void parseFunctionBody(final Collection<ICompilerProblem> problems) { if (!isBodyDeferred) return; deferredBodyParsingLock.lock(); try { deferredBodyParsingReferenceCount++; assert problems != null : "Problems collection can't be null"; final ScopedBlockNode contents = contentsPart.getContents(); assert contents != null : "Function body node can't be null: function " + getName(); // Only re-parse if the function body text is not empty, and the // function body node doesn't have any children. if (contents.getChildCount() > 0) return; assert deferredBodyParsingReferenceCount == 1; assert openT != null : "Expected '{' token."; final String sourcePath = getSourcePath(); assert sourcePath != null && !sourcePath.isEmpty() : "Souce path not set."; final FileNode fileNode = (FileNode)getAncestorOfType(FileNode.class); assert fileNode != null : "FileNode not found: function " + getName(); final IWorkspace workspace = fileNode.getWorkspace(); ASFileScope fileScope = fileNode.getFileScope(); fileScope.addParsedFunctionBodies(this); try { // select function body source final Reader sourceReader; if (functionBodyText != null) { // from cached body text sourceReader = new StringReader(functionBodyText); } else { // from file using offset sourceReader = workspace.getFileSpecification(sourcePath).createReader(); sourceReader.skip(openT.getLocalEnd()); } assert !anyNonParametersInScope(contents); // rebuild function body AST // The incoming "problems" collection might be under modifications from // other code generation threads. In order to filter problems after parsing, // a local problem collection is created and added to the main problem // collection later. final List<ICompilerProblem> functionLocalProblems = new ArrayList<ICompilerProblem>(); ASParser.parseFunctionBody( contents, sourceReader, sourcePath, openT, functionLocalProblems, workspace, fileNode, configProcessor); filterObsoleteProblems(fileNode, functionLocalProblems); problems.addAll(functionLocalProblems); // dispose cached function body info functionBodyText = null; // We should release "openT" as well. However, incremental compilation // needs this to redo code-generation. // openT = null; // connect function and its body scope final EnumSet<PostProcessStep> postProcess = EnumSet.of( PostProcessStep.CALCULATE_OFFSETS, PostProcessStep.POPULATE_SCOPE, PostProcessStep.RECONNECT_DEFINITIONS); problems.addAll(contents.runPostProcess(postProcess, contents.getASScope())); // add implicit "arguments" argument to the local scope tryAddDefaultArgument(); } catch (IOException e) { problems.add(new InternalCompilerProblem2(this.getSourcePath(), e, "function body parser")); } } finally { deferredBodyParsingLock.unlock(); } } /** * {@link BaseASParser} is stateful. The state is essential in compiler * problem creation. However, when re-parsing a deferred function body, the * parser state can't be fully restored. As a result, some compiler problems * can be obsolete. This function removes those unwanted problems. * * @param fileNode AST root node. * @param problems compiler problems found in the deferred function body. */ private void filterObsoleteProblems(FileNode fileNode, Collection<ICompilerProblem> localProblems) { final int functionStartLine = this.getLine(); final Collection<ICompilerProblem> problems = fileNode.getProblems(); final Collection<ICompilerProblem> filteredLocalProblems = filter( localProblems, and(problemAtLine(functionStartLine), problemOfType(CanNotInsertSemicolonProblem.class))); // If the function signature has syntax error, the first "unterminated statement" problem // from function body is then obsolete. if (!filteredLocalProblems.isEmpty()) { final Collection<ICompilerProblem> functionSignatureProblems = filter(problems, problemAtLine(functionStartLine)); if (!functionSignatureProblems.isEmpty()) { localProblems.removeAll(filteredLocalProblems); } } } /** * IFilter {@link ICompilerProblem} collections by line number. */ private static Predicate<ICompilerProblem> problemAtLine(final int line) { return new Predicate<ICompilerProblem>() { @Override public boolean apply(ICompilerProblem problem) { return problem.getLine() == line; } }; } /** * IFilter {@link ICompilerProblem} collections by class. */ private static Predicate<ICompilerProblem> problemOfType(final Class<? extends ICompilerProblem> problemClass) { return new Predicate<ICompilerProblem>() { @Override public boolean apply(ICompilerProblem problem) { return problemClass.isInstance(problem); } }; } private static boolean anyNonParametersInScope(ScopedBlockNode contents) { IASScope sc = contents.getScope(); Collection<IDefinition> ldfs = sc.getAllLocalDefinitions(); for (IDefinition def : ldfs) { if (!(def instanceof IParameterDefinition)) return true; } return false; } /** * Delete all children nodes in a function body. */ public final void discardFunctionBody() { if (!isBodyDeferred || containsLocalFunctions()) return; deferredBodyParsingLock.lock(); try { deferredBodyParsingReferenceCount--; // only discard the body once there are 0 reference to it if (deferredBodyParsingReferenceCount > 0) return; final ScopedBlockNode contents = getScopedNode(); if (contents.getChildCount() > 0) { final FileNode fileNode = (FileNode)getAncestorOfType(FileNode.class); ASFileScope fileScope = fileNode.getFileScope(); fileScope.removeParsedFunctionBodies(this); contents.removeAllChildren(); // Now we need to remove all the definitions in this function scope, except // we keep the parameters. This is because the initial "skeleton parse" goes as // far as the parameters. // So when we throw away the body, we still need to keep the parameter definitions IASScope functionScope = contents.getScope(); Collection<IDefinition> localDefs = functionScope.getAllLocalDefinitions(); for (IDefinition def : localDefs) { if (! (def instanceof IParameterDefinition)) { ASScope asScope = (ASScope)functionScope; asScope.removeDefinition(def); } } } assert (contents.getScope() == null) || (!anyNonParametersInScope(contents)); } finally { deferredBodyParsingLock.unlock(); } } public final boolean hasBeenParsed() { if (!isBodyDeferred) return true; deferredBodyParsingLock.lock(); try { return deferredBodyParsingReferenceCount > 0; } finally { deferredBodyParsingLock.unlock(); } } /** * Store the function body text on the function node so that the AST nodes * can be rebuilt later. */ public final void setFunctionBodyInfo(ASToken openT, ASToken lastTokenInBody, ConfigProcessor configProcessor, StringBuilder bodyCache) { assert openT != null : "Open curly token can't be null"; assert openT.getType() == ASTokenTypes.TOKEN_BLOCK_OPEN : "Expected '{' token."; assert lastTokenInBody != null : "Last token in function body can't be null."; assert configProcessor != null : "Project config variables can't be null."; this.openT = openT.clone(); this.configProcessor = configProcessor; this.isBodyDeferred = true; if (bodyCache == null) this.functionBodyText = null; else this.functionBodyText = bodyCache.toString(); } private ArrayList<IFunctionNode> localFunctions; @Override public List<IFunctionNode> getLocalFunctions() { return localFunctions; } @Override public boolean containsLocalFunctions() { return localFunctions != null; } @Override public void rememberLocalFunction(IFunctionNode value) { if (localFunctions == null) localFunctions = new ArrayList<IFunctionNode>(); localFunctions.add(value); } private boolean emitLocalFunctions; @Override public boolean getEmittingLocalFunctions() { return emitLocalFunctions; } @Override public void setEmittingLocalFunctions(boolean emit) { emitLocalFunctions = emit; } }