blob: b4ecaac69cbdaf5633626b9764adc7e6fe7f81cd [file] [log] [blame]
/*
*
* 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.royale.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.royale.compiler.common.IFileSpecificationGetter;
import org.apache.royale.compiler.filespecs.FileSpecification;
import org.apache.royale.compiler.filespecs.IFileSpecification;
import org.apache.royale.compiler.internal.parsing.mxml.MXMLScopeBuilder;
import org.apache.royale.compiler.internal.projects.CompilerProject;
import org.apache.royale.compiler.mxml.IMXMLUnitData;
import org.apache.royale.compiler.projects.IASProject;
import org.apache.royale.compiler.units.ICompilationUnit;
import org.apache.royale.utils.FilenameNormalization;
import com.google.common.base.MoreObjects;
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 MoreObjects
.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);
}
}
}