/* * Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of Business Objects nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ /* * CodeAnalyser.java * Creation date: February 14, 2004 * By: Iulian Radu */ package org.openquark.cal.compiler; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.openquark.cal.compiler.TypeChecker.TypeCheckInfo; /** * This is a helper class that analyzes the code for code editors. It can analyze code that is passed in * by qualifying symbols, determining arguments and type-checking the code. The results returned are stored in the Results inner * class, which stores the compiler messages, as well as the found arguments and output type expression. * <p> * Creation Date: Oct 2 2002 * @author Ken Wong */ public class CodeAnalyser { /** * Wrapper class for offset compiler messages. Messages are generated * on fully qualified code, and are offset during code analysis to * match visible code. * <p> * For example, the code "sin True" is padded with a newline at its start, * and fully qualified, such that compiler messages are gathered from the expression: * <pre> * " * Prelude.sin Prelude.True * " * </pre> * The compiler produces the message: * "Type error during application..." at line 2, column 21 * <p> * The offset compiler message, produced after analysis will match visible code: * "Type error during application..." at line 1, column 5 * * @author Iulian Radu */ public static final class OffsetCompilerMessage { /** Original compiler message */ private final CompilerMessage message; /** New line position after offset */ private int offsetLine; /** New column position after offset */ private int offsetColumn; private OffsetCompilerMessage(CompilerMessage message, int lineOffset, int columnOffset) { if (message == null) { throw new IllegalArgumentException(); } this.message = message; SourcePosition oldPosition = message.getSourceRange().getStartSourcePosition(); if (oldPosition != null) { offsetLine = oldPosition.getLine(); offsetColumn = oldPosition.getColumn(); } else { offsetLine = -1; offsetColumn = -1; } addOffset(lineOffset, columnOffset); } /** Returns new line position of message */ public int getOffsetLine() { return offsetLine; } /** Returns new column position of message */ public int getOffsetColumn() { return offsetColumn; } /** * @return a source position representing the new line and column of the message. */ public SourcePosition getOffsetPosition() { return new SourcePosition(offsetLine, offsetColumn, ""); } /** True if message has a source position associated with it */ public boolean hasPosition() { return (message.getSourceRange() != null); } /** * Increment offset in position by the specified amount. * * @return False if this message does not have a position associated with it */ boolean addOffset (int lineOffset, int columnOffset) { if (hasPosition()) { offsetLine = offsetLine + lineOffset; offsetColumn = offsetColumn + columnOffset; return true; } else { return false; } } /** * @see java.lang.Object#toString() */ @Override public String toString() { String result = message.getMessage(); if (message.getSourceRange() != null) { result += " at line: " + message.getSourceRange().getStartLine() + " col: " + message.getSourceRange().getStartColumn(); result += " (was line: " + offsetLine + " col: " + offsetColumn + ")"; } return result; } /** * Comparator object to order Offset Compiler Messages by increasing offset source position. */ public static final SourcePositionComparator sourcePositionComparator = new SourcePositionComparator(); private static class SourcePositionComparator implements Comparator<OffsetCompilerMessage> { /** {@inheritDoc}*/ public int compare(OffsetCompilerMessage o1, OffsetCompilerMessage o2) { if ((o1 == null) || (o2 == null)) { throw new NullPointerException(); } int line1 = o1.getOffsetLine(); int line2 = o2.getOffsetLine(); // Put messages with no position at the end if (line2 == -1) { return -1; } else if (line1 == -1) { return 1; } int compareLines = SourcePositionComparator.compareInts (line1, line2); int column1 = o1.getOffsetColumn(); int column2 = o2.getOffsetColumn(); if (compareLines == 0) { return SourcePositionComparator.compareInts (column1, column2); } return compareLines; } private static int compareInts(int i1, int i2) { //note: it is a cute trick to return i1 - i2, but this doesn't work //for all ints due to overflow in int arithmetic. return (i1 < i2 ? -1 : (i1 == i2 ? 0 : 1)); } } // CompilerMessage methods public String getMessage() { return message.getMessage(); } public CompilerMessage.Severity getSeverity() { return message.getSeverity(); } public Exception getException() { return message.getException(); } } /** * Wrapper class for source identifiers. Analysed identifiers differ * from source identifiers in that they are adjusted to fit visible code, * they are associated to their module if possible during analysis, * and they track their qualification method. * <p> * For example, on inspection of the code * <pre> * "let x = and False * Prelude.True; * in arg x" * * We gather the source identifiers: * and - at line 2 col 9, of category SC_OR_METHOD, with no module * False - at line 2 col 13, of category DATA_CONSTRUCTOR, with no module * True - at line 3 col 21, of category DATA_CONSTRUCTOR, with module Prelude * arg - at line 4 col 4, of category SC_OR_METHOD * (note that line numbers are offset by 1, since the code is padded * prior to analysis) * * After analysis completes, we have the analysed identifiers (with the same categories): * and - at line 1 col 9, module Prelude, Unqualified Resolved Top Level Symbol * False - at line 1 col 13, module Prelude, Unqualified Resolved Top Level Symbol * True - at line 2 col 21, module Prelude, Qualified Resolved Top Level Symbol * arg - at line 3 col 4, no module, Unqualified Argument * </pre> * @author Iulian Radu */ public static final class AnalysedIdentifier { /** * Comparator object to order Analysed Identifier by increasing offset source position * of the unqualified name. */ public static final Comparator<AnalysedIdentifier> offsetPositionComparator = new OffsetPositionComparator(); private static class OffsetPositionComparator implements Comparator<AnalysedIdentifier> { public int compare(AnalysedIdentifier identifier1, AnalysedIdentifier identifier2) { if ((identifier1 == null) || (identifier2 == null)) { throw new NullPointerException(); } SourceRange identifierOffsetRange1 = identifier1.getOffsetRange(); SourceRange identifierOffsetRange2 = identifier2.getOffsetRange(); int compareLines = compareInts (identifierOffsetRange1.getStartLine(), identifierOffsetRange2.getStartLine()); if (compareLines != 0) { // Different lines; order by lines return compareLines; } else { // Positions on same line; order by column return compareInts (identifierOffsetRange1.getStartColumn(), identifierOffsetRange2.getStartColumn()); } } private static int compareInts(int i1, int i2) { //note: it is a cute trick to return i1 - i2, but this doesn't work //for all ints due to overflow in int arithmetic. return (i1 < i2 ? -1 : (i1 == i2 ? 0 : 1)); } } /** * Specifies the method of qualification for this identifier, * and whether this is an argument or top level symbol. * * @author Iulian Radu */ public static final class QualificationType { private final String enumType; private QualificationType(String type) { if (type == null) { throw new NullPointerException(); } enumType = type; } /** Indicates whether the identifier is successfully resolved to a top level symbol */ public boolean isResolvedTopLevelSymbol() { return (this == QualifiedResolvedTopLevelSymbol) || (this == UnqualifiedResolvedTopLevelSymbol); } /** Indicates whether the identifier appears in qualified form within the code */ public boolean isCodeQualified() { return (this == QualifiedResolvedTopLevelSymbol) || (this == QualifiedUnresolvedTopLevelSymbol); } /** An unqualified argument (eg: arg1). Note that arguments are always unqualified in CAL. **/ public static final QualificationType UnqualifiedArgument = new QualificationType("UnqualifiedArgument"); /** A top level symbol qualified in code, which resolves successfully (eg: Prelude.True)*/ public static final QualificationType QualifiedResolvedTopLevelSymbol = new QualificationType("QualifiedResolvedTopLevelSymbol"); /** A top level symbol qualified in code, which does not resolve successfully (eg: Prelude.NonExiSt3nt) */ public static final QualificationType QualifiedUnresolvedTopLevelSymbol = new QualificationType("QualifiedUnresolvedTopLevelSymbol"); /** An unqualified top level symbol, which was successfully qualified to the local or an imported module * (eg: 'not' in a module importing Prelude; 'not' in module Prelude)*/ public static final QualificationType UnqualifiedResolvedTopLevelSymbol = new QualificationType("UnqualifiedResolvedTopLevelSymbol"); /** An unqualified top level symbol, not qualified because it resolves to multiple external modules * (eg: 'test1' existing in modules M1 and M2) */ public static final QualificationType UnqualifiedAmbiguousTopLevelSymbol = new QualificationType("UnqualifiedAmbiguousTopLevelSymbol"); /** An unqualified top level symbol, which cannot be qualified to any module (eg: non3xisTent) */ public static final QualificationType UnqualifiedUnresolvedTopLevelSymbol = new QualificationType("UnqualifiedUnresolvedTopLevelSymbol"); /** A local variable appearing in unqualified form in the code. (eg: let localVar = .. in localVar) */ public static final QualificationType UnqualifiedLocalVariable = new QualificationType("UnqualifiedLocalVariable"); /** * Converts enumeration to string. */ @Override public String toString() { return enumType; } } /** Original identifier */ private final SourceIdentifier identifier; /** The offset affecting the line position of the identifier and module name*/ private int lineOffset; /** The offset affecting the column position of the identifier and its module name*/ private int columnOffset; /** Specifies way in which this identifier was qualified **/ private final QualificationType qualificationType; /** * Name of the module which the identifier qualifies to. * This name is filled for identifiers which are qualified through map, and * left null for others. */ private ModuleName newModuleName = null; /** * If {@link #newModuleName} is resolvable, then this is the minimally qualified module name that resolves to the same module. * Otherwise, this holds the same value as {@link #newModuleName}. * This name is filled for identifiers which are qualified through map, and * left null for others. */ private ModuleName minimimallyQualifiedModuleName = null; /** Reference to the definition of this identifier, if this is known */ private AnalysedIdentifier definitionIdentifier = null; /** Constructors */ private AnalysedIdentifier (SourceIdentifier identifier, QualificationType qualificationType) { if ((identifier == null) || (qualificationType == null)) { throw new IllegalArgumentException(); } this.identifier = identifier; this.qualificationType = qualificationType; lineOffset = 0; columnOffset = 0; } /** * Clones this object * Note: References to definition identifiers are not modified. */ AnalysedIdentifier makeCopy() { AnalysedIdentifier clone = new AnalysedIdentifier(this.identifier, this.qualificationType); clone.newModuleName = this.newModuleName; clone.minimimallyQualifiedModuleName = this.minimimallyQualifiedModuleName; clone.lineOffset = this.lineOffset; clone.columnOffset = this.columnOffset; clone.definitionIdentifier = this.definitionIdentifier; return clone; } /** * @return the new source range of the identifier. */ public SourceRange getOffsetRange() { return makeOffsetRange(identifier.getSourceRange()); } /** * Constructs a source range adjusted by the offset. * @param originalRange the original source range. * @return the source range adjusted by the offset. */ private SourceRange makeOffsetRange(SourceRange originalRange) { int startLine = originalRange.getStartLine() + lineOffset; int endLine = originalRange.getEndLine() + lineOffset; int startCol; int endCol; // If the source range starts on the same line as the start of the identifier, then start column needs the offset. // If the source range ends on the same line as the start of the identifier, then its end column needs the offset too. if (identifier.hasRawModuleSourceRange()) { if (startLine == identifier.getRawModuleSourceRange().getStartLine()) { startCol = originalRange.getStartColumn() + columnOffset; if (originalRange.getStartLine() == originalRange.getEndLine()) { endCol = originalRange.getEndColumn() + columnOffset; } else { endCol = originalRange.getEndColumn(); } } else { startCol = originalRange.getStartColumn(); endCol = originalRange.getEndColumn(); } } else { startCol = originalRange.getStartColumn() + columnOffset; if (originalRange.getStartLine() == originalRange.getEndLine()) { endCol = originalRange.getEndColumn() + columnOffset; } else { endCol = originalRange.getEndColumn(); } } return new SourceRange( new SourcePosition(startLine, startCol, originalRange.getStartSourcePosition().getSourceName()), new SourcePosition(endLine, endCol, originalRange.getEndSourcePosition().getSourceName())); } /** * @return the new source range of the module name in the identifier. */ public SourceRange getOffsetModuleNameRange() { SourceRange rawModuleSourceRange = identifier.getRawModuleSourceRange(); if (rawModuleSourceRange != null) { return makeOffsetRange(rawModuleSourceRange); } else { return null; } } /** * @return the original source range of the identifier. */ public SourceRange getOriginalRange() { return identifier.getSourceRange(); } /** Increment offset in position by the specified amount. */ void addOffset (int lineOffset, int columnOffset) { this.lineOffset += lineOffset; this.columnOffset += columnOffset; } public QualificationType getQualificationType() { return qualificationType; } public AnalysedIdentifier getDefinitionIdentifier() { return definitionIdentifier; } /** Sets the module name for an identifier qualified through map */ void setModuleName(ModuleName moduleName, ModuleName minimimallyQualifiedModuleName) { this.newModuleName = moduleName; this.minimimallyQualifiedModuleName = minimimallyQualifiedModuleName; } /** Sets the reference to definition identifier */ void setDefinitionIdentifier(AnalysedIdentifier definitionIdentifier) { this.definitionIdentifier = definitionIdentifier; } /** @return string representation of this object */ @Override public String toString() { String result = identifier + " analysed type: " + qualificationType + " module: " + (getResolvedModuleName() == null ? "(empty)" : getResolvedModuleName().toSourceText()); result += " at new range: " + getOffsetRange().toString(); return result; } // SourceIdentifier methods public ModuleName getResolvedModuleName() { if (newModuleName == null) { return identifier.getResolvedModuleName(); } else { return newModuleName; } } public ModuleName getMinimallyQualifiedModuleName() { if (minimimallyQualifiedModuleName == null) { return identifier.getMinimallyQualifiedModuleName(); } else { return minimimallyQualifiedModuleName; } } /** * @return the original module name, as it appears lexically. */ public ModuleName getRawModuleName() { return identifier.getRawModuleName(); } public String getName() { return identifier.getName(); } public SourceIdentifier.Category getCategory() { return identifier.getCategory(); } public boolean hasRawModuleSourceRange() { return identifier.hasRawModuleSourceRange(); } } /** * This class is a wrapper that encapsulates the results from the code analysis * @author Ken Wong * Creation Date Oct 2nd 2002 */ public static final class AnalysisResults { /** The type expression of the code */ private final TypeExpr typeExpr; /** * List (OffsetCompilerMessage) of compiler messages generated from the analysis. * These messages are adjusted to fit the visible (unqualified) code, and * are sorted by increasing source positions. * Line numbers are 1-index based. */ private final List<OffsetCompilerMessage> compilerMessages; /** The code qualification results */ private QualificationResults qualificationResults; /** * Default constructor for the Results * @param typeExpr the type expression of the code * @param compilerMessages the adjusted compiler messages generated from analysing the code * @param qualificationResults results from code qualification */ private AnalysisResults( TypeExpr typeExpr, List<OffsetCompilerMessage> compilerMessages, QualificationResults qualificationResults) { if (compilerMessages == null) { throw new IllegalArgumentException(); } this.typeExpr = typeExpr; this.compilerMessages = compilerMessages; this.qualificationResults = qualificationResults; } /** * Accessor methods for the typeExpr * @return TypeExpr the Type expression captured in the results */ public TypeExpr getTypeExpr() { return typeExpr; } /** * Accessor methods for the compiler message generated by compiler * @return list of OffsetCompilerMessage */ public List<OffsetCompilerMessage> getCompilerMessages() { return Collections.unmodifiableList(compilerMessages); } /** * Indicates whether code analysis was performed successfully * (ie: the code was successfully parsed, identifiers extracted, * and qualification map updated) * * If analysis is not successful, then this structure contains * only compiler messages. * * @return whether analysis was successful */ public boolean analysisSuccessful() { return (qualificationResults != null); } /** * Retrieves the unqualified names of all arguments found in the code * The arguments are ordered by first appearance in code, and duplicates are not included. * @return String[] */ public String[] getAllArgumentNames() { if (analysisSuccessful()) { return qualificationResults.getAllArgumentNames(); } else { return new String[0]; } } /** * Accessor method for qualifiedCode * If analysis was not successful, an empty string is returned. * @return fully qualified code text */ public String getQualifiedCode() { if (analysisSuccessful()) { return qualificationResults.getQualifiedCode(); } else { return ""; } } /** * Accessor method for qualification map * If analysis was not successful, an empty map is returned * @return mapping from unqualified to qualified names */ public CodeQualificationMap getQualificationMap() { if (analysisSuccessful()) { return qualificationResults.getQualificationMap().makeCopy(); } else { return new CodeQualificationMap(); } } /** * Accessor for analysedIdentifiers * If analysis was not successful, an empty list is returned. * @return list (AnalysedIdentifier) names, categories, positions of * qualified and unqualified identifiers from the original code, * ordered by their appearance in code. */ public List<AnalysedIdentifier> getAnalysedIdentifiers() { if (analysisSuccessful()) { return qualificationResults.getAnalysedIdentifiers(); } else { return new ArrayList<AnalysedIdentifier>(); } } } /** * This class is a wrapper that encapsulates the results from fully qualifying * a piece of code. * * @author Iulian Radu * Creation Date Feb 8th 2004 */ public static final class QualificationResults { /** * The fully qualified code text * This is null if parse errors were encountered while inspecting * the original code. */ private String qualifiedCode; /** * Mapping of unqualified to qualified names. * * Code arguments do not appear in this map, and can be * found in the argumentNames field. */ private CodeQualificationMap qualificationMap; /** Array containing the unqualified names of all arguments found in the code */ private String[] unqualifiedArgumentNames; /** * (AnalysedIdentifier) The list of identifiers, along with category, position, and * method of qualification, which are found in the code text. The list is ordered by * increasing source positions; identifiers with no positions are kept at the end of the list. * * The positions of these identifiers match the visible (unqualified) code. */ private List<AnalysedIdentifier> analysedIdentifiers; /** * Constructor for the Results * @param unqualifiedArgumentNames the unqualified names of arguments found in code * @param qualifiedCode the fully qualified code * @param qualificationMap map identifying qualifications * @param analysedIdentifiers occurrences of unqualified identifiers in code */ private QualificationResults( String[] unqualifiedArgumentNames, String qualifiedCode, CodeQualificationMap qualificationMap, List<AnalysedIdentifier> analysedIdentifiers) { if ((unqualifiedArgumentNames == null) || (qualifiedCode == null) || (qualificationMap == null) || (analysedIdentifiers == null)) { throw new IllegalArgumentException(); } this.unqualifiedArgumentNames = unqualifiedArgumentNames; this.qualifiedCode = qualifiedCode; this.qualificationMap = qualificationMap; this.analysedIdentifiers = analysedIdentifiers; } /** * @return String[] array of unqualified names of all arguments, * ordered by first appearance in code, not including duplicates */ public String[] getAllArgumentNames() { return unqualifiedArgumentNames; } /** * Removes all argument names. * This method is invoked by analyseCode when the * compiled code is not allowed to have arguments. */ private void resetArgumentNames() { unqualifiedArgumentNames = new String[0]; } /** Accessor method for qualifiedCode * @return fully qualified code text */ public String getQualifiedCode() { return qualifiedCode; } /** Accessor method for qualification map * @return mapping from unqualified to qualified names */ public CodeQualificationMap getQualificationMap() { return qualificationMap.makeCopy(); } /** * Accessor for analysedIdentifiers * @return list (AnalysedIdentifiers) names, categories, positions of qualified * and unqualified identifiers, ordered by their appearance in code */ public List<AnalysedIdentifier> getAnalysedIdentifiers() { return (analysedIdentifiers == null? null : Collections.unmodifiableList(analysedIdentifiers)); } /** * Sets the qualified code as specified. * This method is used by analyseCode for removing its added padding on the code text. * @param newQualifiedCode */ private void setQualifiedCode(String newQualifiedCode) { qualifiedCode = newQualifiedCode; } /** * Sets the analysed identifiers as specified. * This method is used by analyseCode for removing its added padding on the identifier positions. * @param analysedIdentifiers */ private void setAnalysedIdentifiers(List<AnalysedIdentifier> analysedIdentifiers) { this.analysedIdentifiers = analysedIdentifiers; } } /** * Results from intermediary symbol analysis by method extractArgumentsAndQualifications() * @author Iulian Radu */ private static class ExtractionResults { /** (AnalysedIdentifiers) Identifiers with proper qualification type and module name */ private final List<AnalysedIdentifier> analysedIdentifiers; /** Unqualified names of arguments which appear in the code */ private final Set<String> usedArgumentNames; /** Updated qualification map with resolved unqualified symbols */ private final CodeQualificationMap qualificationMap; ExtractionResults(List<AnalysedIdentifier> analysedIdentifiers, Set<String> usedArgumentNames, CodeQualificationMap qualificationMap) { if ((usedArgumentNames == null) || (qualificationMap == null) || (analysedIdentifiers == null)) { throw new IllegalArgumentException(); } this.analysedIdentifiers = analysedIdentifiers; this.usedArgumentNames = usedArgumentNames; this.qualificationMap = qualificationMap; } // Accessors /** * @return the list of analysed identifiers; this is ordered by * occurrences in code (ie: source position of identifiers). */ public List<AnalysedIdentifier> getAnalysedIdentifiers() { return analysedIdentifiers; } /** * @return the set of unqualified argument names which appear in the code, * ordered by first appearance, and not including duplicates. */ public Set<String> getArgumentNames() { return usedArgumentNames; } /** * @return code qualification map */ public CodeQualificationMap getQualificationMap() { return qualificationMap; } } /** Type information for the module which analysed code belongs to */ private ModuleTypeInfo moduleTypeInfo; /** Object to use for type checking source */ private TypeChecker typeChecker; /** Whether the code is allowed to have arguments */ private boolean allowNewArguments; /** Whether to qualify symbols which belong to multiple modules */ private boolean qualifyAmbiguousNames; /** * Construct code analyser * * @param typeChecker type checker to use * @param moduleTypeInfo information for the module the code belongs to * @param allowNewArguments whether the code is allowed to have new arguments * @param qualifyAmbiguousNames whether to qualify symbols which belong to multiple modules */ public CodeAnalyser(TypeChecker typeChecker, ModuleTypeInfo moduleTypeInfo, boolean allowNewArguments, boolean qualifyAmbiguousNames) { if ((typeChecker == null) || (moduleTypeInfo == null)) { throw new IllegalArgumentException(); } this.moduleTypeInfo = moduleTypeInfo; this.typeChecker = typeChecker; this.allowNewArguments = allowNewArguments; this.qualifyAmbiguousNames = qualifyAmbiguousNames; } /** * Analyses the code and extracts information about identifiers found in code, * identifies arguments, qualifies symbols, and determines the type expression * of the fully qualified code. * * @param codeText the code to be analysed * @param varNamesWhichAreArgs set (String) of variable names that should be arguments (assumed nonexistent if null) * @param previousQualificationMap mapping from unqualified to qualified names (assumed nonexistent if null) * @return CodeAnalyser.AnalysisResults */ public AnalysisResults analyseCode( String codeText, Set<String> varNamesWhichAreArgs, CodeQualificationMap previousQualificationMap) { if (codeText == null) { throw new IllegalArgumentException(); } if (codeText.length() == 0) { return new AnalysisResults(null, new ArrayList<OffsetCompilerMessage>(), new QualificationResults(new String[0], "", new CodeQualificationMap(), new ArrayList<AnalysedIdentifier>())); } // Build the object that we'll store the results in. CompilerMessageLogger logger = new MessageLogger (); // Add a linefeed at the end to assure parsing is done correctly. // Also add one in the front so that the extra code added in is on its own line. codeText = "\n" + codeText + "\n"; // Qualify code QualificationResults qualificationResults = qualifyExpression( codeText, varNamesWhichAreArgs, previousQualificationMap, logger); if (qualificationResults == null) { // Expression failed to be parsed. // Make sure messages are adjusted to visible code, and report them. List<CompilerMessage> compilerMessages = logger.getCompilerMessages(CompilerMessage.Severity.ERROR); List<OffsetCompilerMessage> offsetMessages = adjustMessagesToVisibleCode(compilerMessages, null, null); return new AnalysisResults(null, offsetMessages, null); } String qualifiedCode = qualificationResults.getQualifiedCode(); // Use the other unmatched names as arguments if (!allowNewArguments) { // If we are not allowed to have arguments we create an empty array. qualificationResults.resetArgumentNames(); } // Check and extract type of fully qualified code TypeCheckInfo info = typeChecker.getTypeCheckInfo(moduleTypeInfo.getModuleName()); TypeExpr typeExpr = CodeAnalyser.typeCheckSource(qualificationResults.getQualifiedCode(), qualificationResults.getAllArgumentNames(), info, logger); // Align positions of identifiers and messages with visible code List<AnalysedIdentifier> offsetIdentifiers = adjustIdentifiersToVisibleCode(qualificationResults.getAnalysedIdentifiers()); qualificationResults.setAnalysedIdentifiers(offsetIdentifiers); List<CompilerMessage> compilerMessages = logger.getCompilerMessages(CompilerMessage.Severity.ERROR); List<OffsetCompilerMessage> offsetMessages = adjustMessagesToVisibleCode( compilerMessages, offsetIdentifiers, qualificationResults.getQualificationMap()); // Adjust code by removing the padded beginning and end newline qualifiedCode = qualifiedCode.substring(1, qualifiedCode.length()-1); qualificationResults.setQualifiedCode(qualifiedCode); // Return results object return new AnalysisResults(typeExpr, offsetMessages, qualificationResults); } /** * Extract any free variables (to be treated as arguments) from the set of qualified and unqualified * symbol positions, and update the qualification map with entries if unqualified identifiers * can be qualified. If the qualification map supplied contains entries which are not used, * the respective entries are removed. * * The SourceIdentifier objects of the supplied list are converted to AnalysedIdentifier * objects, with proper qualification type. If the identifier belongs to the current module, * is fully qualified in code, or can be successfully resolved, then its module name * is also filled with the proper module. * * @param symbolPositions list (SourceIdentifiers) of qualified and unqualified symbols within the code * (code is assumed erroneous if this is null) * @param varNamesWhichAreArgs set (String) of variable names which are known arguments to the code * (assumed nonexistent if null) * @param previousQualificationMap previously used qualification map (assumed nonexistent if null) * @return CodeAnalyser.ExtractionResults containing used arguments, analysed identifiers and updated map */ private ExtractionResults extractArgumentsAndQualifications( List<SourceIdentifier> symbolPositions, Set<String> varNamesWhichAreArgs, CodeQualificationMap previousQualificationMap) { // Make sure we use a valid qualification map CodeQualificationMap qualificationMap; if (previousQualificationMap == null) { qualificationMap = new CodeQualificationMap(); } else { qualificationMap = previousQualificationMap.makeCopy(); } // Make sure we use a valid varNamesWhichAreArgs object if (varNamesWhichAreArgs == null) { varNamesWhichAreArgs = new LinkedHashSet<String>(); } // Do not continue if parsing did not succeed if (symbolPositions == null) { removeUnusedMapEntries(null, qualificationMap); return new ExtractionResults(new ArrayList<AnalysedIdentifier>(), new LinkedHashSet<String>(), qualificationMap); } // (String) These sets keep track of valid argument names found in the code Set<String> foundArgumentNames = new LinkedHashSet<String>(); // (SourceIdentifier -> AnalysedIdentifiers) Mapping between local variable definition // source and analysed identifiers Map<SourceIdentifier, AnalysedIdentifier> localDefinitionsMap = new HashMap<SourceIdentifier, AnalysedIdentifier>(); // (AnalysedIdentifier -> SourceIdentifier) // Mapping between reference analysed identifiers and their definition source identifiers. // // It is possible for references to come before their definitions (eg: "let a=b+1; b=2; in.."). // This list will contain analysed identifiers which are local variable references, but which // could not be linked to their definition analysed identifiers in the initial pass through the // source identifier list. Map<AnalysedIdentifier, SourceIdentifier> localUndefinedReferencesMap = new HashMap<AnalysedIdentifier, SourceIdentifier>(); // (AnalysedIdentifiers) will hold the analysed source identifiers List<AnalysedIdentifier> analysedIdentifiers = new ArrayList<AnalysedIdentifier>(); // Analyse all identifier positions found in code, and construct analysed identifiers for (int i = 0, n = symbolPositions.size(); i < n; i++) { SourceIdentifier symbolPosition = symbolPositions.get(i); // Is the symbol a local variable ? if (isLocalVariable(symbolPosition)) { analysedIdentifiers.add(analyseLocalVariable(symbolPosition, localDefinitionsMap, localUndefinedReferencesMap)); continue; } // Is the symbol already qualified in code ? if (isQualifiedSymbol(symbolPosition)) { analysedIdentifiers.add(analyseQualifiedSymbol(symbolPosition)); continue; } // Symbol is not qualified; was it found to be an argument ? if (isExistingUnqualifiedArgument(symbolPosition, qualificationMap, varNamesWhichAreArgs, foundArgumentNames)) { analysedIdentifiers.add(analyseExistingUnqualifiedArgument(symbolPosition, qualificationMap, foundArgumentNames)); continue; } // Finally, the unqualified symbol not a supplied argument, // so determine if it is locally or externally qualified, a new argument, or unknown. AnalysedIdentifier analysedIdentifier = analyseUnqualifiedNewArgumentOrTopLevelSymbol(symbolPosition, foundArgumentNames, qualificationMap); if (analysedIdentifier == null) { return null; } analysedIdentifiers.add(analysedIdentifier); } // Now link identifiers whose references come before their definitions for (final Map.Entry<AnalysedIdentifier, SourceIdentifier> entry : localUndefinedReferencesMap.entrySet()) { final AnalysedIdentifier analysedIdentifier = entry.getKey(); final SourceIdentifier sourceIdentifier = entry.getValue(); AnalysedIdentifier definitionIdentifier = localDefinitionsMap.get(sourceIdentifier); if (definitionIdentifier == null) { throw new IllegalStateException("Local reference identifier does not have a corresponding definition identifier"); } analysedIdentifier.setDefinitionIdentifier(definitionIdentifier); } // Remove unused entries from map removeUnusedMapEntries(analysedIdentifiers, qualificationMap); return new ExtractionResults(analysedIdentifiers, foundArgumentNames, qualificationMap); } /** * Qualifies the unqualified external top level symbols in the specified * code according to the qualification map. * * @param originalCode original (visible) code * @param identifiers locations of symbols within code. * Note: these locations must match the code supplied. * @param qualifyAllSymbols whether to qualify all symbols * (if false, only unqualified symbols mapped to external modules are qualified) * @return fully qualified code */ public String replaceUnqualifiedSymbols( String originalCode, List<AnalysedIdentifier> identifiers, boolean qualifyAllSymbols) { if (identifiers == null) { return originalCode; } // Qualify names by using a Renamer object with replacements // of each occurrence of unqualified identifiers. SourceModifier codeReplacements = new SourceModifier(); for (final AnalysedIdentifier identifier : identifiers) { String unqualifiedName = identifier.getName(); ModuleName moduleNameOrNull = identifier.getResolvedModuleName(); QualifiedName qualifiedName; if (!qualifyAllSymbols) { // Not qualifying all symbols, so just look at the non-local mapped ones if ((identifier.getQualificationType() != AnalysedIdentifier.QualificationType.UnqualifiedResolvedTopLevelSymbol) || (moduleTypeInfo.getModuleName().equals(moduleNameOrNull))) { continue; } if (moduleNameOrNull == null) { // Is not qualified through map continue; } qualifiedName = QualifiedName.make(moduleNameOrNull, unqualifiedName); } else { // Qualifying all symbols which have a module attached to them if (moduleNameOrNull == null) { // Is not qualified through map continue; } if (identifier.getQualificationType().isCodeQualified()) { if (identifier.getResolvedModuleName().equals(identifier.getRawModuleName())) { // Appears in fully qualified form in code continue; } } if (identifier.getCategory() == SourceIdentifier.Category.LOCAL_VARIABLE_DEFINITION) { // Do not qualify definitions continue; } if (identifier.getCategory() == SourceIdentifier.Category.LOCAL_VARIABLE) { // Do not qualify local variables continue; } qualifiedName = QualifiedName.make(moduleNameOrNull, unqualifiedName); } if (qualifiedName != null) { SourceRange identifierOffsetModuleNameRange = identifier.getOffsetModuleNameRange(); if (identifierOffsetModuleNameRange != null) { codeReplacements.addSourceModification(makeReplaceText(originalCode, identifierOffsetModuleNameRange, qualifiedName.getModuleName().toSourceText())); } else { SourceRange identifierOffsetUnqualifiedNameRange = identifier.getOffsetRange(); codeReplacements.addSourceModification(makeReplaceText(originalCode, identifierOffsetUnqualifiedNameRange, qualifiedName.getQualifiedName())); } } else { throw new IllegalStateException("Unqualified resolved symbol is not qualified through map"); } } return codeReplacements.apply(originalCode); } /** * Checks the unqualified identifier is contained and visible within the specified module. * * @param importedModuleTypeInfo type info for the module we are searching * @param unqualifiedName unqualified name of identifier * @param type identifier category * @param currentModuleTypeInfo this module must be either the same as, or import 'importedModuleTypeInfo'. * @return True if identifier contained and visible in the module; False otherwise. */ private static boolean moduleContainsIdentifier(ModuleTypeInfo importedModuleTypeInfo, String unqualifiedName, SourceIdentifier.Category type, ModuleTypeInfo currentModuleTypeInfo) { ScopedEntity entity = null; // Get entity, depending on its type if (type == SourceIdentifier.Category.TOP_LEVEL_FUNCTION_OR_CLASS_METHOD) { entity = importedModuleTypeInfo.getFunctionOrClassMethod(unqualifiedName); } else if (type == SourceIdentifier.Category.DATA_CONSTRUCTOR) { entity = importedModuleTypeInfo.getDataConstructor(unqualifiedName); } else if (type == SourceIdentifier.Category.TYPE_CONSTRUCTOR) { entity = importedModuleTypeInfo.getTypeConstructor(unqualifiedName); } else if (type == SourceIdentifier.Category.TYPE_CLASS) { entity = importedModuleTypeInfo.getTypeClass(unqualifiedName); } else { // Unknown type throw new IllegalArgumentException(); } if (entity == null) { return false; } // If entity exists and is visible, we can use it from this module return currentModuleTypeInfo.isEntityVisible(entity); } /** * Retrieves a list of modules containing the specified identifier. * If the symbol is contained within the current module, this module name * is at the front of the returned list. The rest of the module names are * stored in their import order. * * @param unqualifiedName * @param type * @param currentModuleTypeInfo * @return list (ModuleName) of module names containing this identifier */ public static List/*ModuleName*/<ModuleName> getModulesContainingIdentifier( String unqualifiedName, SourceIdentifier.Category type, ModuleTypeInfo currentModuleTypeInfo) { if (unqualifiedName == null || type == null || currentModuleTypeInfo == null) { throw new NullPointerException(); } List/*ModuleName*/<ModuleName> moduleNames = new ArrayList<ModuleName>(); // Check if current module contains the identifier if (moduleContainsIdentifier(currentModuleTypeInfo, unqualifiedName, type, currentModuleTypeInfo)) { moduleNames.add(currentModuleTypeInfo.getModuleName()); } // Check if any imported module contains the identifier int nImportedModules = currentModuleTypeInfo.getNImportedModules(); for (int i = 0; i < nImportedModules; ++i) { ModuleTypeInfo importedModuleTypeInfo = currentModuleTypeInfo.getNthImportedModule(i); if (moduleContainsIdentifier(importedModuleTypeInfo, unqualifiedName, type, currentModuleTypeInfo)) { moduleNames.add(importedModuleTypeInfo.getModuleName()); } } return moduleNames; } /** * Retrieve the entity object which corresponds to the qualified name, if accessible from * the specified module. * * @param qualifiedName qualified entity name * @param type entity type * @param moduleTypeInfo type info for the module searched * @return scoped entity; null if none found */ public static ScopedEntity getVisibleModuleEntity(QualifiedName qualifiedName, SourceIdentifier.Category type, ModuleTypeInfo moduleTypeInfo) { if ((qualifiedName == null) || (moduleTypeInfo == null)) { throw new IllegalArgumentException(); } ScopedEntity entity = null; // Get entity, depending on its type if (type == SourceIdentifier.Category.TOP_LEVEL_FUNCTION_OR_CLASS_METHOD) { entity = moduleTypeInfo.getVisibleFunction(qualifiedName); if (entity == null) { entity = moduleTypeInfo.getVisibleClassMethod(qualifiedName); } } else if (type == SourceIdentifier.Category.DATA_CONSTRUCTOR) { entity = moduleTypeInfo.getVisibleDataConstructor(qualifiedName); } else if (type == SourceIdentifier.Category.TYPE_CONSTRUCTOR) { entity = moduleTypeInfo.getVisibleTypeConstructor(qualifiedName); } else if (type == SourceIdentifier.Category.TYPE_CLASS) { entity = moduleTypeInfo.getVisibleTypeClass(qualifiedName); } else { // Unknown type throw new IllegalArgumentException(); } return entity; } /** * Resolve the specified unqualified name into a qualified name according to * CAL name resolution rules. In particular, if the named entity exists * within the specified module, the qualified name returned would reference * the specified module. Otherwise, the import statements of the specified * module are checked to see if the name appears in the appropriate using * clause. If so, then the qualified name would reference the module named * by the import statement. If the name is still not resolved, then null is * returned. * * @param unqualifiedName * the unqualified name to be qualified. * @param type * the type of the entity. * @param moduleTypeInfo * type info for the module checked. * @return a qualified name representing the named entity, or null if there * is no entity visible according to CAL name resolution rules by * that name. */ public static QualifiedName resolveEntityNameAccordingToImportUsingClauses(String unqualifiedName, SourceIdentifier.Category type, ModuleTypeInfo moduleTypeInfo) { if (unqualifiedName == null) { throw new NullPointerException(); } ModuleName resolvedModuleName = null; if (type == SourceIdentifier.Category.TOP_LEVEL_FUNCTION_OR_CLASS_METHOD) { if (moduleTypeInfo.getFunctionOrClassMethod(unqualifiedName) != null) { resolvedModuleName = moduleTypeInfo.getModuleName(); } else { resolvedModuleName = moduleTypeInfo.getModuleOfUsingFunctionOrClassMethod(unqualifiedName); } } else if (type == SourceIdentifier.Category.DATA_CONSTRUCTOR) { if (moduleTypeInfo.getDataConstructor(unqualifiedName) != null) { resolvedModuleName = moduleTypeInfo.getModuleName(); } else { resolvedModuleName = moduleTypeInfo.getModuleOfUsingDataConstructor(unqualifiedName); } } else if (type == SourceIdentifier.Category.TYPE_CONSTRUCTOR) { if (moduleTypeInfo.getTypeConstructor(unqualifiedName) != null) { resolvedModuleName = moduleTypeInfo.getModuleName(); } else { resolvedModuleName = moduleTypeInfo.getModuleOfUsingTypeConstructor(unqualifiedName); } } else if (type == SourceIdentifier.Category.TYPE_CLASS) { if (moduleTypeInfo.getTypeClass(unqualifiedName) != null) { resolvedModuleName = moduleTypeInfo.getModuleName(); } else { resolvedModuleName = moduleTypeInfo.getModuleOfUsingTypeClass(unqualifiedName); } } else { // Unknown type throw new IllegalArgumentException(); } if (resolvedModuleName != null) { return QualifiedName.make(resolvedModuleName, unqualifiedName); } else { return null; } } /** * Adjust positions of compiler messages to fit the visible (unqualified) code. * * @param compilerMessages list of original CompilerMessage objects * @param identifiers positions of symbols within original code (set to null if parsing failed) * @param qualificationMap qualification map, used for determining offsets (set to null if parsing failed) * @return List of OffsetCompilerMessages sorted by increasing adjusted source position */ private List<OffsetCompilerMessage> adjustMessagesToVisibleCode( List<CompilerMessage> compilerMessages, List<AnalysedIdentifier> identifiers, CodeQualificationMap qualificationMap) { if (compilerMessages == null) { return null; } // Create a list of adjusted messages from the specified messages List<OffsetCompilerMessage> adjustedMessages = new ArrayList<OffsetCompilerMessage>(); for (final CompilerMessage message : compilerMessages) { // The code was padded with one newline, so messages must be shifted up adjustedMessages.add(new OffsetCompilerMessage(message, -1, 0)); } Collections.sort(adjustedMessages, OffsetCompilerMessage.sourcePositionComparator); // Adjust position of messages by accounting for each qualified symbol. if (identifiers == null) { return adjustedMessages; } for (final AnalysedIdentifier symbol : identifiers) { String originalSymbolName = symbol.getName(); if ((symbol.getQualificationType() != AnalysedIdentifier.QualificationType.UnqualifiedResolvedTopLevelSymbol) || (moduleTypeInfo.getModuleName().equals(symbol.getResolvedModuleName()))) { // This symbol is was not qualified to an external module through map, so ignore it continue; } // Only mappings to different name lengths affect message positions QualifiedName qualifiedSymbolName = qualificationMap.getQualifiedName(originalSymbolName, symbol.getCategory()); if (qualifiedSymbolName != null && !qualifiedSymbolName.getModuleName().equals(symbol.getRawModuleName())) { // Symbol was mapped and compiler messages should be shifted. // Go through sorted compiler messages and update the necessary ones for (final OffsetCompilerMessage message : adjustedMessages) { // There might be messages with no associated position; the sorting // puts these at the end of the message list. if (!message.hasPosition()) { break; } // Look for compiler messages on the same line as the symbol; // stop iterating through messages once they exceed symbol line. SourceRange symbolOffsetRange = symbol.getOffsetRange(); // There is an assumption that the message was reported on a symbol that // does not contain whitespaces // e.g. A.B.C // instead of: // A . // B . C int symbolOffsetLine = symbolOffsetRange.getStartLine(); int symbolOffsetColumn = symbolOffsetRange.getStartColumn(); if (symbolOffsetLine < message.getOffsetLine()) { break; } if ((message.getOffsetLine() == symbolOffsetLine) && (symbolOffsetColumn < message.getOffsetColumn())) { // Symbol is on same line, to the left of the message // This offset holds the extra length generated by the replacement int offsetLength = qualifiedSymbolName.getQualifiedName().length() - originalSymbolName.length(); // If error hits on the module part of the name, if ((message.getOffsetColumn() >= symbolOffsetColumn) && (message.getOffsetColumn() < (symbolOffsetColumn + offsetLength))) { // Point error to the beginning of our name in visible code message.addOffset(0, symbolOffsetColumn - message.getOffsetColumn()); } else { // Error not pointing on our module, so shift error left by the offset message.addOffset(0, -offsetLength); } // adjust for the fact that the symbol might have originally been partially qualified (and span more than one line!) SourceRange symbolOffsetModuleNameRange = symbol.getOffsetModuleNameRange(); if (symbolOffsetModuleNameRange != null) { message.addOffset( symbolOffsetRange.getEndLine() - symbolOffsetModuleNameRange.getStartLine(), (symbolOffsetRange.getEndColumn() - originalSymbolName.length()) - symbolOffsetModuleNameRange.getStartColumn()); } } } } } // The appropriate compiler messages have been shifted, and // will be returned. return adjustedMessages; } /** * Adjust analysed source identifiers to fit the visible code * * @param analysedIdentifiers list of identifiers to be adjusted * @return List (AnalysedIdentifier) identifiers matching visible code */ private List<AnalysedIdentifier> adjustIdentifiersToVisibleCode(List<AnalysedIdentifier> analysedIdentifiers) { if (analysedIdentifiers == null) { return null; } Map<AnalysedIdentifier, AnalysedIdentifier> adjustedIdentifiers = new LinkedHashMap<AnalysedIdentifier, AnalysedIdentifier>(); // Copy and adjust identifiers to the proper position for (final AnalysedIdentifier oldIdentifier : analysedIdentifiers) { AnalysedIdentifier newIdentifier = oldIdentifier.makeCopy(); // The code is padded with one newline, so identifiers need to be shifted up newIdentifier.addOffset(-1,0); adjustedIdentifiers.put(oldIdentifier, newIdentifier); } // Now update references for identifiers which have links to definitions // (these were not updated in the previous copy pass since a reference may appear before a definition) for (final AnalysedIdentifier adjustedIdentifier : adjustedIdentifiers.values()) { AnalysedIdentifier oldDefinitionIdentifier = adjustedIdentifier.getDefinitionIdentifier(); if (oldDefinitionIdentifier != null) { AnalysedIdentifier newDefinitionIdentifier = adjustedIdentifiers.get(oldDefinitionIdentifier); if (newDefinitionIdentifier == null) { throw new IllegalStateException("Attempting to copy identifier reference whose definition was not copied"); } adjustedIdentifier.setDefinitionIdentifier(newDefinitionIdentifier); } } return new ArrayList<AnalysedIdentifier>(adjustedIdentifiers.values()); } /** * Checks that entries in qualification map are still valid * (ie: the qualifications contained still successfully resolve) * * @param qualificationMap map to check * @return True if entries are still valid; False if qualification map * needs to be revisited since entities do not qualify anymore. */ public boolean checkQualificationMapValidity(CodeQualificationMap qualificationMap) { if (qualificationMap == null) { throw new IllegalArgumentException(); } Set<QualifiedName> qualifiedFunctions = qualificationMap.getQualifiedNames(SourceIdentifier.Category.TOP_LEVEL_FUNCTION_OR_CLASS_METHOD); for (final QualifiedName name : qualifiedFunctions) { if ((moduleTypeInfo.getVisibleFunction(name) == null) && (moduleTypeInfo.getVisibleClassMethod(name) == null)) { return false; } } Set<QualifiedName> qualifiedConstructors = qualificationMap.getQualifiedNames(SourceIdentifier.Category.DATA_CONSTRUCTOR); for (final QualifiedName qualifiedName : qualifiedConstructors) { if (moduleTypeInfo.getVisibleDataConstructor(qualifiedName) == null) { return false; } } Set<QualifiedName> qualifiedTypes = qualificationMap.getQualifiedNames(SourceIdentifier.Category.TYPE_CONSTRUCTOR); for (final QualifiedName qualifiedName : qualifiedTypes) { if (moduleTypeInfo.getVisibleTypeConstructor(qualifiedName) == null) { return false; } } Set<QualifiedName> qualifiedClasses = qualificationMap.getQualifiedNames(SourceIdentifier.Category.TYPE_CLASS); for (final QualifiedName qualifiedName : qualifiedClasses) { if (moduleTypeInfo.getVisibleTypeClass(qualifiedName) == null) { return false; } } return true; } /** * Garbage collection function for removing unused map entries. * * Ensures that the map contains only entries which qualify * the specified identifier position set. If the positions * are empty, then all map entries are deleted. * * @param identifierPositions list (AnalysedIdentifiers) of analysed identifiers * @param qualificationMap map to simplify */ private static void removeUnusedMapEntries(List<AnalysedIdentifier> identifierPositions, CodeQualificationMap qualificationMap) { // Separate identifiers into type sets Set<String> usedFunctions = new HashSet<String>(); Set<String> usedTypes = new HashSet<String>(); Set<String> usedClasses = new HashSet<String>(); Set<String> usedConstructors = new HashSet<String>(); if (identifierPositions != null) { for (final AnalysedIdentifier identifier : identifierPositions) { String name = identifier.getName(); SourceIdentifier.Category type = identifier.getCategory(); if (identifier.getQualificationType() != AnalysedIdentifier.QualificationType.UnqualifiedResolvedTopLevelSymbol) { // Identifier not qualified through map; ignore it continue; } if (type == SourceIdentifier.Category.TOP_LEVEL_FUNCTION_OR_CLASS_METHOD) { usedFunctions.add(name); } else if (type == SourceIdentifier.Category.DATA_CONSTRUCTOR) { usedConstructors.add(name); } else if (type == SourceIdentifier.Category.TYPE_CONSTRUCTOR) { usedTypes.add(name); } else if (type == SourceIdentifier.Category.TYPE_CLASS) { usedClasses.add(name); } else { throw new IllegalArgumentException(); } } } // Now remove unnecessary entries from our maps Set<String> mapFunctions = qualificationMap.getUnqualifiedNames(SourceIdentifier.Category.TOP_LEVEL_FUNCTION_OR_CLASS_METHOD); mapFunctions.removeAll(usedFunctions); for (final String mapFunction : mapFunctions) { qualificationMap.removeQualification(mapFunction, SourceIdentifier.Category.TOP_LEVEL_FUNCTION_OR_CLASS_METHOD); } Set<String> mapClasses = qualificationMap.getUnqualifiedNames(SourceIdentifier.Category.TYPE_CLASS); mapClasses.removeAll(usedClasses); for (final String mapClass : mapClasses) { qualificationMap.removeQualification(mapClass, SourceIdentifier.Category.TYPE_CLASS); } Set<String> mapTypes = qualificationMap.getUnqualifiedNames(SourceIdentifier.Category.TYPE_CONSTRUCTOR); mapTypes.removeAll(usedTypes); for (final String mapType : mapTypes) { qualificationMap.removeQualification(mapType, SourceIdentifier.Category.TYPE_CONSTRUCTOR); } Set<String> mapConstructors = qualificationMap.getUnqualifiedNames(SourceIdentifier.Category.DATA_CONSTRUCTOR); mapConstructors.removeAll(usedConstructors); for (final String mapConstructor : mapConstructors) { qualificationMap.removeQualification(mapConstructor, SourceIdentifier.Category.DATA_CONSTRUCTOR); } } /** * Type check source code. * Creation date: (03/07/2001 10:44:37 AM) * @param source the source code to type check * @param argNames the names of the arguments in order of their needed appearance on the rhs of the sc definition. * @param info the info to use to type check the variables. * @param logger the message logger used in type checking * @return TypeExpr the resulting type of the source */ private static TypeExpr typeCheckSource(String source, String[] argNames, TypeCheckInfo info, CompilerMessageLogger logger) { // create a string with all the arguments in natural order StringBuilder args = new StringBuilder(); for (final String argName : argNames) { args.append(argName + " "); } // Now determine the type of the output of this function and set the type field StringBuilder candidateFunction = new StringBuilder ("cdInternal_typeCheckOnly "); candidateFunction.append(args); candidateFunction.append("= "); candidateFunction.append(source); candidateFunction.append(";\n"); return info.getTypeChecker().checkFunction( new AdjunctSource.FromText(candidateFunction.toString()), info.getModuleName(), logger); } /** * Fully qualifies the specified expression which may contain arguments, after * determining information about the identifiers found in code. * A message logger may be supplied to gather errors encountered in code parsing. * * @param codeText the code to be analysed * @param varNamesWhichAreArgs set (String) variable names that should be arguments (assumed nonexistent if null) * @param previousQualificationMap mapping from unqualified to qualified names (assumed nonexistent if null) * @param logger message logger for compiler results * @return CodeAnalyser.QualificationResults; if a parse error occurs, null is returned * and the logger contains the appropriate error */ public QualificationResults qualifyExpression( String codeText, Set<String> varNamesWhichAreArgs, CodeQualificationMap previousQualificationMap, CompilerMessageLogger logger) { return qualifyExpression(codeText, varNamesWhichAreArgs, previousQualificationMap, logger, false); } /** * Fully qualifies the specified expression which may contain arguments, after * determining information about the identifiers found in code. * A message logger may be supplied to gather errors encountered in code parsing. * * @param codeText the code to be analysed * @param varNamesWhichAreArgs set (String) variable names that should be arguments (assumed nonexistent if null) * @param previousQualificationMap mapping from unqualified to qualified names (assumed nonexistent if null) * @param logger message logger for compiler results * @param qualifyAllSymbols whether to qualify all symbols * @return CodeAnalyser.QualificationResults; if a parse error occurs, null is returned * and the logger contains the appropriate error */ public QualificationResults qualifyExpression( String codeText, Set<String> varNamesWhichAreArgs, CodeQualificationMap previousQualificationMap, CompilerMessageLogger logger, boolean qualifyAllSymbols) { if ( (codeText == null) || (typeChecker == null) || (moduleTypeInfo == null)) { throw new IllegalArgumentException(); } // Get all the identifiers that are used in the code gem. List<SourceIdentifier> identifierPositions = typeChecker.findIdentifiersInExpression(codeText, moduleTypeInfo.getModuleName(), moduleTypeInfo.getModuleNameResolver(), logger); if (identifierPositions == null) { return null; } // Retrieve arguments, review qualification map and update code with new qualifications ExtractionResults extractions = extractArgumentsAndQualifications(identifierPositions, varNamesWhichAreArgs, previousQualificationMap); if (extractions == null) { return null; } String qualifiedCode = replaceUnqualifiedSymbols(codeText, extractions.getAnalysedIdentifiers(), qualifyAllSymbols); // Convert arguments set to array Set<String> freeVarNames = extractions.getArgumentNames(); String[] unqualifiedArgumentNames; if (freeVarNames != null) { unqualifiedArgumentNames = new String[freeVarNames.size()]; freeVarNames.toArray(unqualifiedArgumentNames); } else { unqualifiedArgumentNames = new String[] {}; } // Order the analysed identifiers by source position List<AnalysedIdentifier> sortedAnalysedIdentifiers = extractions.getAnalysedIdentifiers(); Collections.sort(sortedAnalysedIdentifiers, AnalysedIdentifier.offsetPositionComparator); return new QualificationResults(unqualifiedArgumentNames, qualifiedCode, extractions.getQualificationMap(), sortedAnalysedIdentifiers); } /** * @param sourceIdentifier * @return Whether the specified identifier should be analysed as a local variable */ private boolean isLocalVariable(SourceIdentifier sourceIdentifier) { return ((sourceIdentifier.getCategory() == SourceIdentifier.Category.LOCAL_VARIABLE) || (sourceIdentifier.getCategory() == SourceIdentifier.Category.LOCAL_VARIABLE_DEFINITION)); } /** * @param sourceIdentifier * @param localDefinitionsMap (SourceIdentifier -> AnalysedIdentifier) mapping of converted local definitions * @param localUndefinedReferencesMap (AnalysedIdentifier -> SourceIdentifier) mapping from converted references to their definition identifiers * @return AnalysedIdentifier representing local variable */ private AnalysedIdentifier analyseLocalVariable(SourceIdentifier sourceIdentifier, Map<SourceIdentifier, AnalysedIdentifier> localDefinitionsMap, Map<AnalysedIdentifier, SourceIdentifier> localUndefinedReferencesMap) { AnalysedIdentifier analysedIdentifier = new AnalysedIdentifier(sourceIdentifier, AnalysedIdentifier.QualificationType.UnqualifiedLocalVariable); if (sourceIdentifier.getCategory() == SourceIdentifier.Category.LOCAL_VARIABLE_DEFINITION) { // Local variable definition. Map the source identifier to the analysed identifier of the definition. localDefinitionsMap.put(sourceIdentifier, analysedIdentifier); } else { // Local variable reference which has a definition. // Retrieve the analysed identifier for the definition and link if possible AnalysedIdentifier definitionIdentifier = localDefinitionsMap.get(sourceIdentifier.getDefinition()); if (definitionIdentifier != null) { analysedIdentifier.setDefinitionIdentifier(definitionIdentifier); } else { // This identifier is a reference without a definition; it will be resolved in the second pass localUndefinedReferencesMap.put(analysedIdentifier, sourceIdentifier.getDefinition()); } } return analysedIdentifier; } /** * @param sourceIdentifier * @return Whether the specified identifier should be analysed as a qualified symbol */ private boolean isQualifiedSymbol(SourceIdentifier sourceIdentifier) { return (sourceIdentifier.getResolvedModuleName() != null); } /** * Analyses the specified symbol which is fully qualified in code. * The symbol may be a resolved top level symbol or unresolved top level symbol. * * @param sourceIdentifier * @return new analysed identifier */ private AnalysedIdentifier analyseQualifiedSymbol( SourceIdentifier sourceIdentifier) { String symbolName = sourceIdentifier.getName(); ModuleName symbolModuleOrNull = sourceIdentifier.getResolvedModuleName(); SourceIdentifier.Category symbolType = sourceIdentifier.getCategory(); final boolean isExistingEntity; if (symbolModuleOrNull == null) { throw new IllegalStateException("expected a qualified source identifier, but it is unqualified"); } else { isExistingEntity = (CodeAnalyser.getVisibleModuleEntity(QualifiedName.make(symbolModuleOrNull, symbolName), symbolType, moduleTypeInfo) != null); } AnalysedIdentifier.QualificationType qualificationType; if (isExistingEntity) { // found a valid resolution qualificationType = AnalysedIdentifier.QualificationType.QualifiedResolvedTopLevelSymbol; } else { // Could not be resolved qualificationType = AnalysedIdentifier.QualificationType.QualifiedUnresolvedTopLevelSymbol; } return new AnalysedIdentifier(sourceIdentifier, qualificationType); } /** * Indicates whether the specified identifier is an existing argument * (that is, it has been specified as an argument, or was previously determined to * be one) * * @param sourceIdentifier * @param qualificationMap * @param varNamesWhichAreArgs * @param foundUnqualifiedArgumentNames * @return whether the identifier is an existing argument */ private boolean isExistingUnqualifiedArgument(SourceIdentifier sourceIdentifier, CodeQualificationMap qualificationMap, Set<String> varNamesWhichAreArgs, Set<String> foundUnqualifiedArgumentNames) { String symbolName = sourceIdentifier.getName(); SourceIdentifier.Category symbolType = sourceIdentifier.getCategory(); QualifiedName mapQualifiedName = qualificationMap.getQualifiedName(symbolName, symbolType); boolean locallyQualified = ((mapQualifiedName != null) && (mapQualifiedName.getModuleName().equals(moduleTypeInfo.getModuleName()))); return (symbolType == SourceIdentifier.Category.TOP_LEVEL_FUNCTION_OR_CLASS_METHOD) && ( foundUnqualifiedArgumentNames.contains(symbolName) || (varNamesWhichAreArgs.contains(symbolName) && ((mapQualifiedName == null) || locallyQualified))); } /** * Analyses the specified identifier which is an unqualified existing argument. * The is treated as being an unqualified local argument. * * @param sourceIdentifier * @param qualificationMap * @param foundArgumentNames ordered set (String) of unqualified names which have been found to be arguments of the expression; * * @return new analysed identifier */ private AnalysedIdentifier analyseExistingUnqualifiedArgument( SourceIdentifier sourceIdentifier, CodeQualificationMap qualificationMap, Set<String> foundArgumentNames) { QualifiedName mapQualifiedName = qualificationMap.getQualifiedName(sourceIdentifier.getName(), sourceIdentifier.getCategory()); boolean locallyQualified = ((mapQualifiedName != null) && (mapQualifiedName.getModuleName().equals(moduleTypeInfo.getModuleName()))); if ((mapQualifiedName == null) || locallyQualified) { foundArgumentNames.add(sourceIdentifier.getName()); } return new AnalysedIdentifier(sourceIdentifier, AnalysedIdentifier.QualificationType.UnqualifiedArgument); } /** * Analyses the specified unqualified symbol and determine if it is a new argument, or * resolvable local or external qualification. * * This method attempts to auto-qualify the symbol via the qualification map: * If qualification is possible, the symbol is treated as an unqualified resolvable top level symbol * and a module name is attached. * If the symbol is ambiguous and we do not allow ambiguities, the symbol is * an unqualified ambiguous top level symbol with no module. * Otherwise the symbol is termed a local argument (if allowed), * or unqualified unresolved top level symbol. * * @param sourceIdentifier * @param foundArgumentNames ordered set (String) of unqualified names which have been found to be arguments of the expression; * this is only used to maintain an ordered list of found arguments * @param qualificationMap * @return new analysed identifier */ private AnalysedIdentifier analyseUnqualifiedNewArgumentOrTopLevelSymbol(SourceIdentifier sourceIdentifier, Set<String> foundArgumentNames, CodeQualificationMap qualificationMap) { String symbolName = sourceIdentifier.getName(); SourceIdentifier.Category symbolType = sourceIdentifier.getCategory(); // Try to get qualification through the map QualifiedName qualifiedName = qualificationMap.getQualifiedName(symbolName, symbolType); if (qualifiedName == null) { // Identifier not mapped through map. See if it can be found in a module. // Note: If the symbol can be resolved in the current module, this module will be first. List/*ModuleName*/<ModuleName> matchingModuleNames = getModulesContainingIdentifier(symbolName, symbolType, moduleTypeInfo); if (!matchingModuleNames.isEmpty()) { if ((matchingModuleNames.size() > 1) && !matchingModuleNames.get(0).equals(moduleTypeInfo.getModuleName()) && !qualifyAmbiguousNames) { // The module list is ambiguous, // the identifier cannot be mapped to the current module, // and we do not allow ambiguities. // So mark this as an ambiguity without module. return new AnalysedIdentifier(sourceIdentifier, AnalysedIdentifier.QualificationType.UnqualifiedAmbiguousTopLevelSymbol); } else { // The identifier can be mapped to only one module, // the identifier can be found in the current module, // or ambiguities allowed ModuleName newModuleName = matchingModuleNames.get(0); ModuleName minimallyQualifiedModuleName = moduleTypeInfo.getModuleNameResolver().getMinimallyQualifiedModuleName(newModuleName); // So map this to the first module qualificationMap.putQualification( symbolName, newModuleName, symbolType); AnalysedIdentifier analysedIdentifier = new AnalysedIdentifier(sourceIdentifier, AnalysedIdentifier.QualificationType.UnqualifiedResolvedTopLevelSymbol); analysedIdentifier.setModuleName(newModuleName, minimallyQualifiedModuleName); return analysedIdentifier; } } else { // No module found; if allowed, treat this as a free argument if (allowNewArguments && (symbolType == SourceIdentifier.Category.TOP_LEVEL_FUNCTION_OR_CLASS_METHOD)) { foundArgumentNames.add(symbolName); return new AnalysedIdentifier(sourceIdentifier, AnalysedIdentifier.QualificationType.UnqualifiedArgument); } else { return new AnalysedIdentifier(sourceIdentifier, AnalysedIdentifier.QualificationType.UnqualifiedUnresolvedTopLevelSymbol); } } } else { // Symbol is qualified through the map. See if it is resolvable before marking. AnalysedIdentifier.QualificationType qualificationType; if (CodeAnalyser.getVisibleModuleEntity(qualifiedName, sourceIdentifier.getCategory(), moduleTypeInfo) != null) { qualificationType = AnalysedIdentifier.QualificationType.UnqualifiedResolvedTopLevelSymbol; } else { return null; } AnalysedIdentifier analysedIdentifier = new AnalysedIdentifier(sourceIdentifier, qualificationType); ModuleName newModuleName = qualifiedName.getModuleName(); ModuleName minimallyQualifiedModuleName = moduleTypeInfo.getModuleNameResolver().getMinimallyQualifiedModuleName(newModuleName); analysedIdentifier.setModuleName(newModuleName, minimallyQualifiedModuleName); return analysedIdentifier; } } /** * Constructs and returns a SoruceModification that replaces the text at sourceRange of sourceText with newText. * @param sourceText * @param sourceRange * @param newText * @return A SourceModification.ReplaceText */ private static SourceModification makeReplaceText(String sourceText, SourceRange sourceRange, String newText) { SourcePosition startPosition = sourceRange.getStartSourcePosition(); SourcePosition endPosition = sourceRange.getEndSourcePosition(); int startIndex = startPosition.getPosition(sourceText); int endIndex = endPosition.getPosition(sourceText, startPosition, startIndex); String oldText = sourceText.substring(startIndex, endIndex); return new SourceModification.ReplaceText(oldText, newText, startPosition); } }