/* * * 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.parsing.as; import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.List; import org.apache.commons.io.FilenameUtils; import org.apache.flex.compiler.common.IFileSpecificationGetter; import org.apache.flex.compiler.filespecs.FileSpecification; import org.apache.flex.compiler.filespecs.IFileSpecification; import org.apache.flex.compiler.internal.parsing.mxml.MXMLScopeBuilder; import org.apache.flex.compiler.internal.projects.CompilerProject; import org.apache.flex.compiler.mxml.IMXMLUnitData; import org.apache.flex.compiler.projects.IASProject; import org.apache.flex.compiler.units.ICompilationUnit; import org.apache.flex.utils.FilenameNormalization; import com.google.common.base.Objects; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; /** * Handler for include processing. Each source file has one * {@code IncludeHandler}. * <p> * {@code IncludeHandler} responsibilities: * <ul> * <li>Produce a {@link IFileSpecification} from an include statement.</li> * <li>Track include chain to prevent cyclic include.</li> * <li>Track and store a tree of include relationships.</li> * </ul> */ public class IncludeHandler { /** * Tree node type for storing include relationships. */ protected static final class Tree { private Tree(String filename, Tree parent) { this.children = new ArrayList<Tree>(); this.filename = filename; this.parent = parent; this.tokenEnd = 0; } protected final List<Tree> children; protected final String filename; private final Tree parent; private int tokenEnd; private Tree addChild(String filename) { final Tree child = new Tree(filename, this); children.add(child); return child; } @Override public String toString() { return Objects .toStringHelper(this) .add("end", tokenEnd) .add("file", new File(filename).getName()) .add("children", children.size()) .toString(); } private static void dfs(final Tree tree, final Collection<String> result) { for (final Tree child : tree.children) { dfs(child, result); } result.add(tree.filename); } private static Tree getRoot(final Tree tree) { if (tree == null) return null; Tree result = tree; while (result.parent != null) { result = result.parent; } return result; } } /** * Create an {@code IncludeHandler}. The {@code workspace} is used to find a * file specification from the workspace file specification pool. If the * workspace is null, * {@code getFileSpecificationForInclude(String, String)} will always * create a new {@link IFileSpecification} result. * * @param fileSpecGetter {@link IFileSpecificationGetter} that should be * used by the include hander to open included files. */ public IncludeHandler(final IFileSpecificationGetter fileSpecGetter) { this.fileSpecGetter = fileSpecGetter; this.currentNode = null; this.absoluteOffset = 0; this.offsetCueListBuilder = new ImmutableList.Builder<OffsetCue>(); this.timeStamp = 0l; } /** * Create an {@code IncludeHandler} with a given starting absolute offset. * This is useful for creating MXML AST nodes, because MXML node * construction routine doesn't track absolute offsets. Instead, it uses the * {@link OffsetLookup} created by {@link MXMLScopeBuilder} to recover the * absolute offset, and create a "one-time" {@code IncludeHandler} in order * to get the nodes with the same absolute offsets and the definitions. * * @param fileSpecGetter {@link IFileSpecificationGetter} that should be * used by the include hander to open included files. * @param startAbsoluteOffset starting absolute offset * @return IncludeHandler */ public static IncludeHandler create( final IFileSpecificationGetter fileSpecGetter, final int startAbsoluteOffset) { final IncludeHandler handler = new IncludeHandler(fileSpecGetter); handler.absoluteOffset = startAbsoluteOffset; return handler; } /** * Create an {@link IncludeHandler} that does not interception of requests * to open included files. * * @return An {@link IncludeHandler} that does not interception of requests * to open included files. */ public static IncludeHandler creatDefaultIncludeHandler() { return new IncludeHandler(null); } /** * Create an {@code IncludeHandler} with a given starting absolute offset. * This is useful for building MXML AST nodes, because MXML node * construction routine doesn't track absolute offsets. Instead, it uses the * {@link OffsetLookup} in {@link MXMLScopeBuilder} to recover the absolute * offset, and mock a "one-time" {@code IncludeHandler} in order to get the * nodes with the same absolute offsets and the definitions. * * @param sourcePath enter this file * @param localOffset starting local offset * @param absoluteOffset starting absolute offset * @return IncludeHandler IncludeHander */ public static IncludeHandler createForASTBuilding( final IFileSpecificationGetter fileSpecGetter, final String sourcePath, final int localOffset, final int absoluteOffset) { final IncludeHandler handler = new IncludeHandler(fileSpecGetter); handler.enterFile(sourcePath); handler.currentNode.tokenEnd = localOffset; handler.absoluteOffset = absoluteOffset; return handler; } private final IFileSpecificationGetter fileSpecGetter; /** * A tree of previously included file (including the first main file). This * helps detecting cyclic includes. * <p> * There can't be duplicated filename on the trail from the current node to * the tree root. */ private Tree currentNode; /** * Track the absolute offset of the current lexer session. */ private int absoluteOffset; /** * An {@code OffsetCue} is created every time {@link #enterFile(String)} or * {@link #leaveFile()} is called. */ private final ImmutableList.Builder<OffsetCue> offsetCueListBuilder; /** * A last modified timestamp on the tree of include files of this current * source */ private long timeStamp; /** * Containing compiler project. This field is null-able. */ private IASProject project; private ICompilationUnit compilationUnit; /** * True if this {@code IncludeHandler} tracks not only {@link ASToken} but * also {@link IMXMLUnitData}. This flag tells {@link #onNextToken(ASToken)} * not to check whether {@code currentNode.tokenEnd} increases * monotonically. Before CMP-1490 is fixed, this is a workaround for * CMP-1368. */ private boolean hasMXMLUnits = false; /** * Helper function to get an {@link IFileSpecification} from a filename * * @param path Normalized file path. * @return A {@link IFileSpecification} either provided by the workspace or * created if none exists. This method never returns null. */ private final IFileSpecification getFileSpec(final String path) { IFileSpecification results = null; if (fileSpecGetter != null) results = fileSpecGetter.getFileSpecification(path); if (results == null) results = new FileSpecification(path); return results; } /** * Given an the canonical path of a file that contains an include directive * with the specified string, return an {@link IFileSpecification} for the * file the include directive references. * <p> * <ol> * <li>If {@code includeString} is an absolute path, a * {@code IFileSpecification} for that location will be returned.</li> * <li>Otherwise, if this object has a reference to {@code IASProject}, * the included file path will be resolved firstly in the current directory * of the including file, then in each of the source folders on the project * until the first existing file is found.</li> * <li>If no on-disk file is found for the included file, a * {@code IFileSpecification} pointing to the intended default location will * be returned.</li> * </ol> * * @param includer The canonical path of a file that contains the include * directive. * @param includeString Unquoted string that refers to a file to be * included. * @return An {@link IFileSpecification} for the included file, or null if * can't resolve. */ protected IFileSpecification getFileSpecificationForInclude(String includer, String includeString) { if (Strings.isNullOrEmpty(includer) || Strings.isNullOrEmpty(includeString)) return null; if (new File(includeString).isAbsolute()) { // If included file path is already absolute, do not try to resolve // in any other folder contexts. return getFileSpec(FilenameNormalization.normalize(includeString)); } else { final String includingFolder = FilenameNormalization.normalize(FilenameUtils.getFullPath(includer)); final File includedFileInDefaultFolder = new File(includingFolder, includeString); if (includedFileInDefaultFolder.isFile()) { // Always resolve to the including file's containing source folder first. return getFileSpec(FilenameNormalization.normalizeFileToPath(includedFileInDefaultFolder)); } else if (project != null) { // Try other source folders if "project" was given. final String sourceFileFromSourcePath = project.getSourceFileFromSourcePath(includeString); if (sourceFileFromSourcePath != null && new File(sourceFileFromSourcePath).isFile()) return getFileSpec(sourceFileFromSourcePath); } /** * Can't resolve included file name in all source folders. Return * the IFileSpecification intended to resolve in the including * file's parent folder. This helps generate reasonable error * message in the IDE. */ return getFileSpec(FilenameNormalization.normalizeFileToPath(includedFileInDefaultFolder)); } } /** * @return Set of all files included by this file. */ public ImmutableSet<String> getIncludedFiles() { if (currentNode == null) return ImmutableSet.of(); final Tree root = Tree.getRoot(currentNode); final List<String> result = new ArrayList<String>(); Tree.dfs(root, result); // Exclude the root including file. result.remove(result.size() - 1); return ImmutableSet.copyOf(result); } /** * Reset the state of the include handler by clearing the include chain and * collection of included files. */ public void clear() { this.currentNode = null; } /** * Get the file on top of the include chain stack. * * @return The file on top of the include chain stack. */ protected String getIncludeStackTop() { if (currentNode == null) return null; else return currentNode.filename; } /** * Enter an included file. Push the filename to the include chain. * * @param filename Included file name. */ public void enterFile(final String filename) { assert filename != null : "Filename can't be null."; if (currentNode == null) currentNode = new Tree(filename, null); else currentNode = currentNode.addChild(filename); IFileSpecification filespec = getFileSpec(filename); long newTimeStamp = filespec.getLastModified(); timeStamp = Math.max(timeStamp, newTimeStamp); addOffsetCue(); } /** * propagates the lastModified timestamp from a {@link IncludeHandler} from a * child node to this one. * * @param childIncludeHandler The <code>include</code>-handler whose * timestamp will be used to set the time stap of this <code>include</code>-handler. */ public void propagateLastModified(IncludeHandler childIncludeHandler) { timeStamp = Math.max(timeStamp, childIncludeHandler.getLastModified()); } /** * This timestamp will be the latest modification time of all the root * source and all included files * * @return The timestamp of the root include file */ public long getLastModified() { return timeStamp; } /** * Make an {@code OffsetCue} object and add it to the buffer. */ private void addOffsetCue() { final int adjustment = absoluteOffset - currentNode.tokenEnd; final OffsetCue cue = new OffsetCue(currentNode.filename, absoluteOffset, adjustment); offsetCueListBuilder.add(cue); } /** * Leave the included file. */ public void leaveFile() { currentNode = currentNode.parent; if (currentNode != null) addOffsetCue(); } /** * Extend the end offset of the current file and leave the file. * * @param endOffset Local end offset of the current file, including all * trailing white spaces. */ public void leaveFile(final int endOffset) { final int advance = endOffset - currentNode.tokenEnd; if (advance > 0) absoluteOffset += advance; leaveFile(); } /** * Check if the file is already on the include chain. If true, including it * again will cause cyclic include problem. * * @param filename file name * @return True if including the file causes cyclic include problem. */ protected boolean isCyclicInclude(final String filename) { assert filename != null : "Filename can't be null."; Tree cursor = currentNode; while (cursor != null) { if (cursor.filename.equals(filename)) return true; cursor = cursor.parent; } return false; } /** * Get a tree of include relationship. * * @return A tree of include relationship. */ protected Tree getIncludeTree() { return Tree.getRoot(currentNode); } /** * Set the absolute offset on an {@link ASToken}. The absolute offset is the * token's "local" start offset translated into the absolute space. * <p> * Advance absolute offset recorded in the handler by one token. * * @param token The token to update. */ protected void onNextToken(ASToken token) { assert token != null : "ASToken can't be null."; if (currentNode != null) { // "hasMXMLUnits" shortcuts the token offset check. See Javadoc of "hasMXMLUnits" for details. assert hasMXMLUnits || (token.getEnd() >= currentNode.tokenEnd) : String.format( "Token [%s] (line:%d, col:%d) end at '%d', but last token end at '%d': %s", token.getText(), token.getLine(), token.getColumn(), token.getEnd(), currentNode.tokenEnd, currentNode.filename); // "current_absolute += y" final int advance = token.getEnd() - currentNode.tokenEnd; absoluteOffset += advance; // Advance local end offset stored on the tree node. currentNode.tokenEnd = token.getEnd(); // [LAST_TOKEN] [THIS_TOKEN] // ..............(x)........(y) // // "token.absolute = current_absolute + x" // From last token's end offset to this token's start offset. final int delta = token.getStart() - currentNode.tokenEnd; final int tokenAbsoluteStart = delta + absoluteOffset; // Override token offset with absolute offsets int tokenLength = token.getEnd() - token.getStart(); if (tokenLength < 0) tokenLength = 0; token.setStart(tokenAbsoluteStart); token.setEnd(tokenAbsoluteStart + tokenLength); } } /** * Update the {@code IncludeHandler}'s current offset counter with the next * {@code IMXMLUnitData} * * @param unitData Next {@code IMXMLUnitData} object. */ public void onNextMXMLUnitData(final IMXMLUnitData unitData) { assert unitData != null : "IMXMLUnitData can't be null."; hasMXMLUnits = true; if (currentNode != null) { final int contentEnd = unitData.getContentEnd(); // This assert is turned off because of CMP-1490. // assert contentEnd >= currentNode.tokenEnd : "MXMLUnitData end can't be smaller than last unit end."; final int advance = contentEnd - currentNode.tokenEnd; absoluteOffset += advance; // Advance local end offset stored on the tree node. currentNode.tokenEnd = contentEnd; } } /** * @return Absolute offset. */ protected int getAbsoluteOffset() { return absoluteOffset; } /** * Get all the {@link OffsetCue}'s. * * @return All the {@code OffsetCue} objects. */ public ImmutableList<OffsetCue> getOffsetCueList() { return offsetCueListBuilder.build(); } @Override public String toString() { final StringBuilder result = new StringBuilder(); Tree walker = currentNode; while (walker != null) { result.append(" ").append(walker).append("\n"); walker = walker.parent; } return result.toString(); } /** * Set project reference. The project reference is used to resolve included * files in other source folders in the project. * * @param project Current project. */ public final void setProjectAndCompilationUnit(IASProject project, ICompilationUnit compilationUnit) { this.project = project; this.compilationUnit = compilationUnit; } /** * When a client catches a file not found, they should call us back and let * us know. Only important in an incremental compilation workflow (like * Flash Buildler). NOt needed for command line compiles any most unit * tests. * * @param fileSpec A file specification. */ public void handleFileNotFound(IFileSpecification fileSpec) { if (this.project != null && this.compilationUnit != null) { ((CompilerProject)this.project).addUnfoundReferencedSourceFileDependency(fileSpec.getPath(), compilationUnit); } } }