diff --git a/src/PowerShellEditorServices.Host/Program.cs b/src/PowerShellEditorServices.Host/Program.cs index 448705d83..d6ac8f619 100644 --- a/src/PowerShellEditorServices.Host/Program.cs +++ b/src/PowerShellEditorServices.Host/Program.cs @@ -16,6 +16,7 @@ class Program [STAThread] static void Main(string[] args) { +#if DEBUG // In the future, a more robust argument parser will be added here bool waitForDebugger = args.Any( @@ -28,11 +29,15 @@ static void Main(string[] args) // Should we wait for the debugger before starting? if (waitForDebugger) { - while (!Debugger.IsAttached) + // Wait for 15 seconds and then continue + int waitCountdown = 15; + while (!Debugger.IsAttached && waitCountdown > 0) { - Thread.Sleep(500); + Thread.Sleep(1000); + waitCountdown--; } } +#endif // TODO: Select host, console host, and transport based on command line arguments diff --git a/src/PowerShellEditorServices.Transport.Stdio/Event/DiagnosticEvent.cs b/src/PowerShellEditorServices.Transport.Stdio/Event/DiagnosticEvent.cs index 25aa70e7a..e0dfd07ba 100644 --- a/src/PowerShellEditorServices.Transport.Stdio/Event/DiagnosticEvent.cs +++ b/src/PowerShellEditorServices.Transport.Stdio/Event/DiagnosticEvent.cs @@ -61,6 +61,7 @@ public static DiagnosticEventBody Create( new Diagnostic { Text = diagnosticMarker.Message, + Severity = (int)diagnosticMarker.Level + 1, Start = new Location { Line = diagnosticMarker.ScriptRegion.StartLineNumber, @@ -97,5 +98,7 @@ public class Diagnostic public Location End { get; set; } public string Text { get; set; } + + public int Severity { get; set; } } } diff --git a/src/PowerShellEditorServices.Transport.Stdio/Request/DeclarationRequest.cs b/src/PowerShellEditorServices.Transport.Stdio/Request/DeclarationRequest.cs index 900de6419..14c70b88f 100644 --- a/src/PowerShellEditorServices.Transport.Stdio/Request/DeclarationRequest.cs +++ b/src/PowerShellEditorServices.Transport.Stdio/Request/DeclarationRequest.cs @@ -18,21 +18,31 @@ public override void ProcessMessage( MessageWriter messageWriter) { ScriptFile scriptFile = this.GetScriptFile(editorSession); - - GetDefinitionResult definition = - editorSession.LanguageService.GetDefinitionInFile( + SymbolReference foundSymbol = + editorSession.LanguageService.FindSymbolAtLocation( scriptFile, this.Arguments.Line, this.Arguments.Offset); - - if (definition != null) + + GetDefinitionResult definition = null; + if (foundSymbol != null) { - DefinitionResponse defResponse = - DefinitionResponse.Create(definition.FoundDefinition, this.Arguments.File); + definition = + editorSession.LanguageService.GetDefinitionOfSymbol( + scriptFile, + foundSymbol, + editorSession.Workspace); + + } - messageWriter.WriteMessage( - this.PrepareResponse(defResponse)); + DefinitionResponse defResponse = DefinitionResponse.Create(); + if (definition != null && definition.FoundDefinition != null) + { + defResponse = DefinitionResponse.Create(definition.FoundDefinition); } + + messageWriter.WriteMessage( + this.PrepareResponse(defResponse)); } } } diff --git a/src/PowerShellEditorServices.Transport.Stdio/Request/ErrorRequest.cs b/src/PowerShellEditorServices.Transport.Stdio/Request/ErrorRequest.cs index db6eb197c..098670afa 100644 --- a/src/PowerShellEditorServices.Transport.Stdio/Request/ErrorRequest.cs +++ b/src/PowerShellEditorServices.Transport.Stdio/Request/ErrorRequest.cs @@ -33,14 +33,9 @@ public override void ProcessMessage( // Get the requested files foreach (string filePath in this.Arguments.Files) { - ScriptFile scriptFile = null; - - if (!editorSession.TryGetFile(filePath, out scriptFile)) - { - // Skip this file and log the file load error - // TODO: Trace out the error message - continue; - } + ScriptFile scriptFile = + editorSession.Workspace.GetFile( + filePath); var semanticMarkers = editorSession.AnalysisService.GetSemanticMarkers( diff --git a/src/PowerShellEditorServices.Transport.Stdio/Request/FileRequest.cs b/src/PowerShellEditorServices.Transport.Stdio/Request/FileRequest.cs index f2b31c650..0db64ed4b 100644 --- a/src/PowerShellEditorServices.Transport.Stdio/Request/FileRequest.cs +++ b/src/PowerShellEditorServices.Transport.Stdio/Request/FileRequest.cs @@ -13,20 +13,9 @@ public abstract class FileRequest : RequestBase { protected ScriptFile GetScriptFile(EditorSession editorSession) { - ScriptFile scriptFile = null; - - if(!editorSession.TryGetFile( - this.Arguments.File, - out scriptFile)) - { - // TODO: Throw an exception that the message loop can create a response out of - - throw new FileNotFoundException( - "A ScriptFile with the following path was not found in the EditorSession: {0}", + return + editorSession.Workspace.GetFile( this.Arguments.File); - } - - return scriptFile; } } diff --git a/src/PowerShellEditorServices.Transport.Stdio/Request/OpenFileRequest.cs b/src/PowerShellEditorServices.Transport.Stdio/Request/OpenFileRequest.cs index 4fd581994..f0a466bab 100644 --- a/src/PowerShellEditorServices.Transport.Stdio/Request/OpenFileRequest.cs +++ b/src/PowerShellEditorServices.Transport.Stdio/Request/OpenFileRequest.cs @@ -27,7 +27,7 @@ public override void ProcessMessage( MessageWriter messageWriter) { // Open the file in the current session - editorSession.OpenFile(this.Arguments.File); + editorSession.Workspace.GetFile(this.Arguments.File); } } } diff --git a/src/PowerShellEditorServices.Transport.Stdio/Request/ReferencesRequest.cs b/src/PowerShellEditorServices.Transport.Stdio/Request/ReferencesRequest.cs index 0f175db7d..e5609c7d4 100644 --- a/src/PowerShellEditorServices.Transport.Stdio/Request/ReferencesRequest.cs +++ b/src/PowerShellEditorServices.Transport.Stdio/Request/ReferencesRequest.cs @@ -18,13 +18,17 @@ public override void ProcessMessage( MessageWriter messageWriter) { ScriptFile scriptFile = this.GetScriptFile(editorSession); - - FindReferencesResult referencesResult = - editorSession.LanguageService.FindReferencesInFile( + SymbolReference foundSymbol = + editorSession.LanguageService.FindSymbolAtLocation( scriptFile, this.Arguments.Line, this.Arguments.Offset); + FindReferencesResult referencesResult = + editorSession.LanguageService.FindReferencesOfSymbol( + foundSymbol, + editorSession.Workspace.ExpandScriptReferences(scriptFile)); + ReferencesResponse referencesResponse = ReferencesResponse.Create(referencesResult, this.Arguments.File); diff --git a/src/PowerShellEditorServices.Transport.Stdio/Response/DefinitionResponse.cs b/src/PowerShellEditorServices.Transport.Stdio/Response/DefinitionResponse.cs index a3b315e2f..a50871914 100644 --- a/src/PowerShellEditorServices.Transport.Stdio/Response/DefinitionResponse.cs +++ b/src/PowerShellEditorServices.Transport.Stdio/Response/DefinitionResponse.cs @@ -12,7 +12,7 @@ namespace Microsoft.PowerShell.EditorServices.Transport.Stdio.Response [MessageTypeName("definition")] public class DefinitionResponse : ResponseBase { - public static DefinitionResponse Create(SymbolReference result, string thisFile) + public static DefinitionResponse Create(SymbolReference result) { if (result != null) { @@ -31,7 +31,7 @@ public static DefinitionResponse Create(SymbolReference result, string thisFile) Line = result.ScriptRegion.EndLineNumber, Offset = result.ScriptRegion.EndColumnNumber }, - File = thisFile, + File = result.FilePath }); return new DefinitionResponse { @@ -46,5 +46,12 @@ public static DefinitionResponse Create(SymbolReference result, string thisFile) }; } } + public static DefinitionResponse Create() + { + return new DefinitionResponse + { + Body = null + }; + } } } diff --git a/src/PowerShellEditorServices.Transport.Stdio/Response/ReferencesResponse.cs b/src/PowerShellEditorServices.Transport.Stdio/Response/ReferencesResponse.cs index 788ced37e..a7b6e9dc6 100644 --- a/src/PowerShellEditorServices.Transport.Stdio/Response/ReferencesResponse.cs +++ b/src/PowerShellEditorServices.Transport.Stdio/Response/ReferencesResponse.cs @@ -35,7 +35,7 @@ List referenceItems Offset = reference.ScriptRegion.EndColumnNumber }, IsWriteAccess = true, - File = thisFile, + File = reference.FilePath, LineText = reference.SourceLine }); } diff --git a/src/PowerShellEditorServices/Analysis/AnalysisService.cs b/src/PowerShellEditorServices/Analysis/AnalysisService.cs index 181360154..572dc50ca 100644 --- a/src/PowerShellEditorServices/Analysis/AnalysisService.cs +++ b/src/PowerShellEditorServices/Analysis/AnalysisService.cs @@ -40,7 +40,10 @@ public AnalysisService(Runspace analysisRunspace) this.scriptAnalyzer = new ScriptAnalyzer(); this.scriptAnalyzer.Initialize( analysisRunspace, - new AnalysisOutputWriter()); + new AnalysisOutputWriter(), + null, + null, + new string[] { "DscTestsPresent", "DscExamplesPresent" }); } #endregion @@ -55,26 +58,34 @@ public AnalysisService(Runspace analysisRunspace) /// An array of ScriptFileMarkers containing semantic analysis results. public ScriptFileMarker[] GetSemanticMarkers(ScriptFile file) { - // TODO: This is a temporary fix until we can change how - // ScriptAnalyzer invokes their async tasks. - Task analysisTask = - Task.Factory.StartNew( - () => - { - return - this.scriptAnalyzer - .AnalyzeSyntaxTree( - file.ScriptAst, - file.ScriptTokens, - file.FilePath) - .Select(ScriptFileMarker.FromDiagnosticRecord) - .ToArray(); - }, - CancellationToken.None, - TaskCreationOptions.None, - TaskScheduler.Default); + if (file.IsAnalysisEnabled) + { + // TODO: This is a temporary fix until we can change how + // ScriptAnalyzer invokes their async tasks. + Task analysisTask = + Task.Factory.StartNew( + () => + { + return + this.scriptAnalyzer + .AnalyzeSyntaxTree( + file.ScriptAst, + file.ScriptTokens, + file.FilePath) + .Select(ScriptFileMarker.FromDiagnosticRecord) + .ToArray(); + }, + CancellationToken.None, + TaskCreationOptions.None, + TaskScheduler.Default); - return analysisTask.Result; + return analysisTask.Result; + } + else + { + // Return an empty marker list + return new ScriptFileMarker[0]; + } } #endregion diff --git a/src/PowerShellEditorServices/Language/AstOperations.cs b/src/PowerShellEditorServices/Language/AstOperations.cs index d44100759..93b130d26 100644 --- a/src/PowerShellEditorServices/Language/AstOperations.cs +++ b/src/PowerShellEditorServices/Language/AstOperations.cs @@ -3,7 +3,9 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using System; using System.Collections.Generic; +using System.Linq; using System.Management.Automation; using System.Management.Automation.Language; using System.Management.Automation.Runspaces; @@ -107,20 +109,50 @@ static public SymbolReference FindCommandAtPosition(Ast scriptAst, int lineNumbe } /// - /// Finds all references in a script of the given symbol + /// Finds all references (including aliases) in a script for the given symbol /// /// The abstract syntax tree of the given script /// The symbol that we are looking for referneces of - /// A collection of SymbolReference objects that are refrences to the symbolRefrence - static public IEnumerable FindReferencesOfSymbol(Ast scriptAst, SymbolReference symbolReference) + /// Dictionary maping cmdlets to aliases for finding alias references + /// Dictionary maping aliases to cmdlets for finding alias references + /// + static public IEnumerable FindReferencesOfSymbol( + Ast scriptAst, + SymbolReference symbolReference, + Dictionary> CmdletToAliasDictionary, + Dictionary AliasToCmdletDictionary) { // find the symbol evaluators for the node types we are handling - FindReferencesVisitor referencesVisitor = new FindReferencesVisitor(symbolReference); + FindReferencesVisitor referencesVisitor = + new FindReferencesVisitor( + symbolReference, + CmdletToAliasDictionary, + AliasToCmdletDictionary); scriptAst.Visit(referencesVisitor); return referencesVisitor.FoundReferences; } + /// + /// Finds all references (not including aliases) in a script for the given symbol + /// + /// The abstract syntax tree of the given script + /// The symbol that we are looking for referneces of + /// If this reference search needs aliases. + /// This should always be false and used for occurence requests + /// A collection of SymbolReference objects that are refrences to the symbolRefrence + /// not including aliases + static public IEnumerable FindReferencesOfSymbol( + ScriptBlockAst scriptAst, + SymbolReference foundSymbol, + bool needsAliases) + { + FindReferencesVisitor referencesVisitor = + new FindReferencesVisitor(foundSymbol); + scriptAst.Visit(referencesVisitor); + + return referencesVisitor.FoundReferences; + } /// /// Finds the definition of the symbol @@ -128,12 +160,29 @@ static public IEnumerable FindReferencesOfSymbol(Ast scriptAst, /// The abstract syntax tree of the given script /// The symbol that we are looking for the definition of /// A SymbolReference of the definition of the symbolReference - static public SymbolReference FindDefinitionOfSymbol(Ast scriptAst, SymbolReference symbolReference) + static public SymbolReference FindDefinitionOfSymbol( + Ast scriptAst, + SymbolReference symbolReference) { - FindDeclartionVisitor declarationVisitor = new FindDeclartionVisitor(symbolReference); + FindDeclartionVisitor declarationVisitor = + new FindDeclartionVisitor( + symbolReference); scriptAst.Visit(declarationVisitor); return declarationVisitor.FoundDeclartion; } + + /// + /// Finds all files dot sourced in a script + /// + /// The abstract syntax tree of the given script + /// + static public string[] FindDotSourcedIncludes(Ast scriptAst) + { + FindDotSourcedVisitor dotSourcedVisitor = new FindDotSourcedVisitor(); + scriptAst.Visit(dotSourcedVisitor); + + return dotSourcedVisitor.DotSourcedFiles.ToArray(); + } } } diff --git a/src/PowerShellEditorServices/Language/FindCommandVisitor.cs b/src/PowerShellEditorServices/Language/FindCommandVisitor.cs index 349081f15..d2db440e2 100644 --- a/src/PowerShellEditorServices/Language/FindCommandVisitor.cs +++ b/src/PowerShellEditorServices/Language/FindCommandVisitor.cs @@ -33,6 +33,8 @@ public FindCommandVisitor(int lineNumber, int columnNumber) public override AstVisitAction VisitCommand(CommandAst commandAst) { Ast commandNameAst = commandAst.CommandElements[0]; + + // Only want commands that are using a trigger character, which requires at least 2 cmd elements if (!(commandAst.CommandElements.Count > 1)) { return base.VisitCommand(commandAst); @@ -46,8 +48,7 @@ public override AstVisitAction VisitCommand(CommandAst commandAst) this.FoundCommandReference = new SymbolReference( SymbolType.Function, - commandNameAst.Extent, - string.Empty); + commandNameAst.Extent); return AstVisitAction.StopVisit; } diff --git a/src/PowerShellEditorServices/Language/FindDeclartionVisitor.cs b/src/PowerShellEditorServices/Language/FindDeclartionVisitor.cs index af15b0f70..388989765 100644 --- a/src/PowerShellEditorServices/Language/FindDeclartionVisitor.cs +++ b/src/PowerShellEditorServices/Language/FindDeclartionVisitor.cs @@ -3,6 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using System; using System.Management.Automation.Language; namespace Microsoft.PowerShell.EditorServices.Language @@ -31,6 +32,8 @@ public FindDeclartionVisitor(SymbolReference symbolRef) /// or a decision to continue if it wasn't found public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst) { + // Get the start column number of the function name, + // instead of the the start column of 'function' and create new extent for the functionName int startColumnNumber = functionDefinitionAst.Extent.Text.IndexOf( functionDefinitionAst.Name) + 1; @@ -40,17 +43,17 @@ public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst fun Text = functionDefinitionAst.Name, StartLineNumber = functionDefinitionAst.Extent.StartLineNumber, StartColumnNumber = startColumnNumber, + EndLineNumber = functionDefinitionAst.Extent.StartLineNumber, EndColumnNumber = startColumnNumber + functionDefinitionAst.Name.Length }; if (symbolRef.SymbolType.Equals(SymbolType.Function) && - nameExtent.Text.Equals(symbolRef.ScriptRegion.Text)) + nameExtent.Text.Equals(symbolRef.ScriptRegion.Text, StringComparison.InvariantCultureIgnoreCase)) { this.FoundDeclartion = new SymbolReference( SymbolType.Function, - nameExtent, - string.Empty); + nameExtent); return AstVisitAction.StopVisit; } @@ -69,13 +72,12 @@ public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst fun public override AstVisitAction VisitVariableExpression(VariableExpressionAst variableExpressionAst) { if(symbolRef.SymbolType.Equals(SymbolType.Variable) && - variableExpressionAst.Extent.Text.Equals(symbolRef.SymbolName)) + variableExpressionAst.Extent.Text.Equals(symbolRef.SymbolName, StringComparison.InvariantCultureIgnoreCase)) { this.FoundDeclartion = new SymbolReference( SymbolType.Variable, - variableExpressionAst.Extent, - string.Empty); + variableExpressionAst.Extent); return AstVisitAction.StopVisit; } diff --git a/src/PowerShellEditorServices/Language/FindDotSourcedVisitor.cs b/src/PowerShellEditorServices/Language/FindDotSourcedVisitor.cs new file mode 100644 index 000000000..7748d3026 --- /dev/null +++ b/src/PowerShellEditorServices/Language/FindDotSourcedVisitor.cs @@ -0,0 +1,43 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.Management.Automation.Language; + +namespace Microsoft.PowerShell.EditorServices.Language +{ + /// + /// The vistor used to find the dont sourced files in an AST + /// + internal class FindDotSourcedVisitor : AstVisitor + { + /// + /// A hash set of the dot sourced files (because we don't want duplicates) + /// + public HashSet DotSourcedFiles { get; private set; } + + public FindDotSourcedVisitor() + { + this.DotSourcedFiles = new HashSet(); + } + + /// + /// Checks to see if the command invocation is a dot + /// in order to find a dot sourced file + /// + /// A CommandAst object in the script's AST + /// A descion to stop searching if the right commandAst was found, + /// or a decision to continue if it wasn't found + public override AstVisitAction VisitCommand(CommandAst commandAst) + { + if (commandAst.InvocationOperator.Equals(TokenKind.Dot)) + { + string fileName = commandAst.CommandElements[0].Extent.Text; + DotSourcedFiles.Add(fileName); + } + return base.VisitCommand(commandAst); + } + } +} diff --git a/src/PowerShellEditorServices/Language/FindReferencesVisitor.cs b/src/PowerShellEditorServices/Language/FindReferencesVisitor.cs index fa1f13b1f..af5fc534d 100644 --- a/src/PowerShellEditorServices/Language/FindReferencesVisitor.cs +++ b/src/PowerShellEditorServices/Language/FindReferencesVisitor.cs @@ -3,6 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using System; using System.Collections.Generic; using System.Management.Automation.Language; @@ -14,13 +15,47 @@ namespace Microsoft.PowerShell.EditorServices.Language internal class FindReferencesVisitor : AstVisitor { private SymbolReference symbolRef; + private Dictionary> CmdletToAliasDictionary; + private Dictionary AliasToCmdletDictionary; + private string symbolRefCommandName; + private bool needsAliases; public List FoundReferences { get; set; } - public FindReferencesVisitor(SymbolReference symbolRef) + /// + /// Constructor used when searching for aliases is needed + /// + /// The found symbolReference that other symbols are being compared to + /// Dictionary maping cmdlets to aliases for finding alias references + /// Dictionary maping aliases to cmdlets for finding alias references + public FindReferencesVisitor( + SymbolReference symbolReference, + Dictionary> CmdletToAliasDictionary, + Dictionary AliasToCmdletDictionary) { - this.symbolRef = symbolRef; + this.symbolRef = symbolReference; this.FoundReferences = new List(); + this.needsAliases = true; + this.CmdletToAliasDictionary = CmdletToAliasDictionary; + this.AliasToCmdletDictionary = AliasToCmdletDictionary; + + // Try to get the symbolReference's command name of an alias, + // if a command name does not exists (if the symbol isn't an alias to a command) + // set symbolRefCommandName to and empty string value + AliasToCmdletDictionary.TryGetValue(symbolReference.ScriptRegion.Text, out symbolRefCommandName); + if (symbolRefCommandName == null) { symbolRefCommandName = string.Empty; } + + } + + /// + /// Constructor used when searching for aliases is not needed + /// + /// The found symbolReference that other symbols are being compared to + public FindReferencesVisitor(SymbolReference foundSymbol) + { + this.symbolRef = foundSymbol; + this.FoundReferences = new List(); + this.needsAliases = false; } /// @@ -33,13 +68,51 @@ public FindReferencesVisitor(SymbolReference symbolRef) public override AstVisitAction VisitCommand(CommandAst commandAst) { Ast commandNameAst = commandAst.CommandElements[0]; - if(symbolRef.SymbolType.Equals(SymbolType.Function) && - commandNameAst.Extent.Text.Equals(symbolRef.ScriptRegion.Text)) + string commandName = commandNameAst.Extent.Text; + + if(symbolRef.SymbolType.Equals(SymbolType.Function)) { - this.FoundReferences.Add(new SymbolReference( - SymbolType.Function, - commandNameAst.Extent, - string.Empty)); + if (needsAliases) + { + // Try to get the commandAst's name and aliases, + // if a command does not exists (if the symbol isn't an alias to a command) + // set command to and empty string value string command + // if the aliases do not exist (if the symvol isn't a command that has aliases) + // set aliases to an empty List + string command; + List alaises; + CmdletToAliasDictionary.TryGetValue(commandName, out alaises); + AliasToCmdletDictionary.TryGetValue(commandName, out command); + if (alaises == null) { alaises = new List(); } + if (command == null) { command = string.Empty; } + + if (symbolRef.SymbolType.Equals(SymbolType.Function)) + { + // Check if the found symbol's name is the same as the commandAst's name OR + // if the symbol's name is an alias for this commandAst's name (commandAst is a cmdlet) OR + // if the symbol's name is the same as the commandAst's cmdlet name (commandAst is a alias) + if (commandName.Equals(symbolRef.SymbolName, StringComparison.InvariantCultureIgnoreCase) || + alaises.Contains(symbolRef.ScriptRegion.Text.ToLower()) || + command.Equals(symbolRef.ScriptRegion.Text, StringComparison.InvariantCultureIgnoreCase) || + (!command.Equals(string.Empty) && command.Equals(symbolRefCommandName, StringComparison.InvariantCultureIgnoreCase))) + { + this.FoundReferences.Add(new SymbolReference( + SymbolType.Function, + commandNameAst.Extent)); + } + } + + } + else // search does not include aliases + { + if (commandName.Equals(symbolRef.SymbolName, StringComparison.InvariantCultureIgnoreCase)) + { + this.FoundReferences.Add(new SymbolReference( + SymbolType.Function, + commandNameAst.Extent)); + } + } + } return base.VisitCommand(commandAst); } @@ -52,6 +125,8 @@ public override AstVisitAction VisitCommand(CommandAst commandAst) /// A visit action that continues the search for references public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst) { + // Get the start column number of the function name, + // instead of the the start column of 'function' and create new extent for the functionName int startColumnNumber = functionDefinitionAst.Extent.Text.IndexOf( functionDefinitionAst.Name) + 1; @@ -60,17 +135,17 @@ public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst fun { Text = functionDefinitionAst.Name, StartLineNumber = functionDefinitionAst.Extent.StartLineNumber, + EndLineNumber = functionDefinitionAst.Extent.StartLineNumber, StartColumnNumber = startColumnNumber, EndColumnNumber = startColumnNumber + functionDefinitionAst.Name.Length }; if (symbolRef.SymbolType.Equals(SymbolType.Function) && - nameExtent.Text.Equals(symbolRef.SymbolName)) + nameExtent.Text.Equals(symbolRef.SymbolName, StringComparison.InvariantCultureIgnoreCase)) { this.FoundReferences.Add(new SymbolReference( SymbolType.Function, - nameExtent, - string.Empty)); + nameExtent)); } return base.VisitFunctionDefinition(functionDefinitionAst); } @@ -84,12 +159,11 @@ public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst fun public override AstVisitAction VisitCommandParameter(CommandParameterAst commandParameterAst) { if (symbolRef.SymbolType.Equals(SymbolType.Parameter) && - commandParameterAst.Extent.Text.Equals(symbolRef.SymbolName)) + commandParameterAst.Extent.Text.Equals(symbolRef.SymbolName, StringComparison.InvariantCultureIgnoreCase)) { this.FoundReferences.Add(new SymbolReference( SymbolType.Parameter, - commandParameterAst.Extent, - string.Empty)); + commandParameterAst.Extent)); } return AstVisitAction.Continue; } @@ -103,12 +177,11 @@ public override AstVisitAction VisitCommandParameter(CommandParameterAst command public override AstVisitAction VisitVariableExpression(VariableExpressionAst variableExpressionAst) { if(symbolRef.SymbolType.Equals(SymbolType.Variable) && - variableExpressionAst.Extent.Text.Equals(symbolRef.SymbolName)) + variableExpressionAst.Extent.Text.Equals(symbolRef.SymbolName, StringComparison.InvariantCultureIgnoreCase)) { this.FoundReferences.Add(new SymbolReference( SymbolType.Variable, - variableExpressionAst.Extent, - string.Empty)); + variableExpressionAst.Extent)); } return AstVisitAction.Continue; } diff --git a/src/PowerShellEditorServices/Language/FindSymbolVisitor.cs b/src/PowerShellEditorServices/Language/FindSymbolVisitor.cs index b8e003fee..b8771ab16 100644 --- a/src/PowerShellEditorServices/Language/FindSymbolVisitor.cs +++ b/src/PowerShellEditorServices/Language/FindSymbolVisitor.cs @@ -38,8 +38,7 @@ public override AstVisitAction VisitCommand(CommandAst commandAst) this.FoundSymbolReference = new SymbolReference( SymbolType.Function, - commandNameAst.Extent, - string.Empty); + commandNameAst.Extent); return AstVisitAction.StopVisit; } @@ -63,6 +62,7 @@ public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst fun { Text = functionDefinitionAst.Name, StartLineNumber = functionDefinitionAst.Extent.StartLineNumber, + EndLineNumber = functionDefinitionAst.Extent.EndLineNumber, StartColumnNumber = startColumnNumber, EndColumnNumber = startColumnNumber + functionDefinitionAst.Name.Length }; @@ -72,8 +72,7 @@ public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst fun this.FoundSymbolReference = new SymbolReference( SymbolType.Function, - nameExtent, - string.Empty); + nameExtent); return AstVisitAction.StopVisit; } @@ -94,8 +93,7 @@ public override AstVisitAction VisitCommandParameter(CommandParameterAst command this.FoundSymbolReference = new SymbolReference( SymbolType.Parameter, - commandParameterAst.Extent, - string.Empty); + commandParameterAst.Extent); return AstVisitAction.StopVisit; } return AstVisitAction.Continue; @@ -114,8 +112,7 @@ public override AstVisitAction VisitVariableExpression(VariableExpressionAst var this.FoundSymbolReference = new SymbolReference( SymbolType.Variable, - variableExpressionAst.Extent, - string.Empty); + variableExpressionAst.Extent); return AstVisitAction.StopVisit; } diff --git a/src/PowerShellEditorServices/Language/LanguageService.cs b/src/PowerShellEditorServices/Language/LanguageService.cs index f53ea1d6f..775273bd4 100644 --- a/src/PowerShellEditorServices/Language/LanguageService.cs +++ b/src/PowerShellEditorServices/Language/LanguageService.cs @@ -10,6 +10,7 @@ namespace Microsoft.PowerShell.EditorServices.Language { using Microsoft.PowerShell.EditorServices.Utility; + using System; using System.Management.Automation; using System.Management.Automation.Runspaces; @@ -26,7 +27,8 @@ public class LanguageService private int mostRecentRequestLine; private int mostRecentRequestOffest; private string mostRecentRequestFile; - + private Dictionary> CmdletToAliasDictionary; + private Dictionary AliasToCmdletDictionary; #endregion #region Constructors @@ -43,6 +45,9 @@ public LanguageService(Runspace languageServiceRunspace) Validate.IsNotNull("languageServiceRunspace", languageServiceRunspace); this.runspace = languageServiceRunspace; + this.CmdletToAliasDictionary = new Dictionary>(StringComparer.OrdinalIgnoreCase); + this.AliasToCmdletDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + GetAliases(); } #endregion @@ -109,6 +114,7 @@ public CompletionDetails GetCompletionDetailsInFile( int columnNumber, string entryName) { + // Makes sure the most recent completions request was the same line and column as this request if (file.FilePath.Equals(mostRecentRequestFile) && lineNumber == mostRecentRequestLine && columnNumber == mostRecentRequestOffest) @@ -118,46 +124,81 @@ public CompletionDetails GetCompletionDetailsInFile( result => result.CompletionText.Equals(entryName)); return completionResult; } - else { return null; } + else + { + return null; + } } /// - /// Finds all the references of a symbol in the script given a file location + /// Finds the symbol in the script given a file location /// /// The details and contents of a open script file /// The line number of the cursor for the given script /// The coulumn number of the cursor for the given script - /// FindReferencesResult - public FindReferencesResult FindReferencesInFile( + /// A SymbolReference of the symbol found at the given location + /// or null if there is no symbol at that location + /// + public SymbolReference FindSymbolAtLocation( ScriptFile file, int lineNumber, int columnNumber) { - SymbolReference foundSymbol = + SymbolReference symbolReference = AstOperations.FindSymbolAtPosition( file.ScriptAst, lineNumber, columnNumber); + if (symbolReference != null) + { + symbolReference.FilePath = file.FilePath; + } + + return symbolReference; + } + /// + /// Finds all the references of a symbol + /// + /// The symbol to find all references for + /// An array of scriptFiles too search for references in + /// FindReferencesResult + public FindReferencesResult FindReferencesOfSymbol( + SymbolReference foundSymbol, + ScriptFile[] referencedFiles) + { if (foundSymbol != null) { - IEnumerable symbolReferences = + int symbolOffset = referencedFiles[0].GetOffsetAtPosition( + foundSymbol.ScriptRegion.StartLineNumber, + foundSymbol.ScriptRegion.StartColumnNumber); + List symbolReferences = new List(); + + foreach (ScriptFile file in referencedFiles) + { + IEnumerable symbolReferencesinFile = AstOperations .FindReferencesOfSymbol( file.ScriptAst, - foundSymbol) + foundSymbol, + CmdletToAliasDictionary, + AliasToCmdletDictionary) .Select( reference => { - reference.SourceLine = + reference.SourceLine = file.GetLine(reference.ScriptRegion.StartLineNumber); + reference.FilePath = file.FilePath; return reference; }); + symbolReferences.AddRange(symbolReferencesinFile); + } + return new FindReferencesResult { - SymbolFileOffset = file.GetOffsetAtPosition(lineNumber, columnNumber), + SymbolFileOffset = symbolOffset, SymbolName = foundSymbol.SymbolName, FoundReferences = symbolReferences }; @@ -166,33 +207,56 @@ public FindReferencesResult FindReferencesInFile( } /// - /// Finds the definition of a symbol in the script given a file location + /// Finds the definition of a symbol in the script file or any of the + /// files that it references. /// - /// The details and contents of a open script file - /// The line number of the cursor for the given script - /// The coulumn number of the cursor for the given script - /// GetDefinitionResult - public GetDefinitionResult GetDefinitionInFile( - ScriptFile file, - int lineNumber, - int columnNumber) + /// The initial script file to be searched for the symbol's definition. + /// The symbol for which a definition will be found. + /// The Workspace to which the ScriptFile belongs. + /// The resulting GetDefinitionResult for the symbol's definition. + public GetDefinitionResult GetDefinitionOfSymbol( + ScriptFile sourceFile, + SymbolReference foundSymbol, + Workspace workspace) { - SymbolReference foundSymbol = - AstOperations.FindSymbolAtPosition( - file.ScriptAst, - lineNumber, - columnNumber); + Validate.IsNotNull("sourceFile", sourceFile); + Validate.IsNotNull("foundSymbol", foundSymbol); + Validate.IsNotNull("workspace", workspace); - if (foundSymbol != null) + ScriptFile[] referencedFiles = + workspace.ExpandScriptReferences( + sourceFile); + + // look through the referenced files until definition is found + // or there are no more file to look through + SymbolReference foundDefinition = null; + for (int i = 0; i < referencedFiles.Length; i++) { - SymbolReference foundDefinition = + foundDefinition = AstOperations.FindDefinitionOfSymbol( - file.ScriptAst, - foundSymbol); + referencedFiles[i].ScriptAst, + foundSymbol); + + if (foundDefinition != null) + { + foundDefinition.FilePath = referencedFiles[i].FilePath; + break; + } + } - return new GetDefinitionResult(foundDefinition); + // if definition is not found in referenced files + // look for it in the builtin commands + if (foundDefinition == null) + { + CommandInfo cmdInfo = GetCommandInfo(foundSymbol.SymbolName); + foundDefinition = + FindDeclarationForBuiltinCommand( + cmdInfo, + foundSymbol, + workspace); } - else { return null; } + + return new GetDefinitionResult(foundDefinition); } /// @@ -212,13 +276,16 @@ public FindOccurrencesResult FindOccurrencesInFile( file.ScriptAst, lineNumber, columnNumber); + if (foundSymbol != null) { + // find all references, and indicate that looking for aliases is not needed IEnumerable symbolOccurrences = AstOperations .FindReferencesOfSymbol( file.ScriptAst, - foundSymbol); + foundSymbol, + false); return new FindOccurrencesResult @@ -226,7 +293,10 @@ public FindOccurrencesResult FindOccurrencesInFile( FoundOccurrences = symbolOccurrences }; } - else { return null; } + else + { + return null; + } } /// @@ -258,13 +328,43 @@ public ParameterSetSignatures FindParameterSetsInFile( return new ParameterSetSignatures(commandInfoSet, foundSymbol); } - else { return null; } + else + { + return null; + } + } + else + { + return null; } - else { return null; } } #endregion + #region Private Fields + + /// + /// Gets all aliases found in the runspace + /// + private void GetAliases() + { + CommandInvocationIntrinsics invokeCommand = runspace.SessionStateProxy.InvokeCommand; + IEnumerable aliases = invokeCommand.GetCommands("*", CommandTypes.Alias, true); + foreach (AliasInfo aliasInfo in aliases) + { + if (!CmdletToAliasDictionary.ContainsKey(aliasInfo.Definition)) + { + CmdletToAliasDictionary.Add(aliasInfo.Definition, new List() { aliasInfo.Name }); + } + else + { + CmdletToAliasDictionary[aliasInfo.Definition].Add(aliasInfo.Name); + } + + AliasToCmdletDictionary.Add(aliasInfo.Name, aliasInfo.Definition); + } + } + private CommandInfo GetCommandInfo(string commandName) { CommandInfo commandInfo = null; @@ -279,5 +379,78 @@ private CommandInfo GetCommandInfo(string commandName) return commandInfo; } + + private ScriptFile[] GetBuiltinCommandScriptFiles( + PSModuleInfo moduleInfo, + Workspace workspace) + { + // if there is module info for this command + if (moduleInfo != null) + { + string modPath = moduleInfo.Path; + List scriptFiles = new List(); + ScriptFile newFile; + + // find any files where the moduleInfo's path ends with ps1 or psm1 + // and add it to allowed script files + if (modPath.EndsWith(@".ps1") || modPath.EndsWith(@".psm1")) + { + newFile = workspace.GetFile(modPath); + newFile.IsAnalysisEnabled = false; + scriptFiles.Add(newFile); + } + if (moduleInfo.NestedModules.Count > 0) + { + foreach (PSModuleInfo nestedInfo in moduleInfo.NestedModules) + { + string nestedModPath = nestedInfo.Path; + if (nestedModPath.EndsWith(@".ps1") || nestedModPath.EndsWith(@".psm1")) + { + newFile = workspace.GetFile(nestedModPath); + newFile.IsAnalysisEnabled = false; + scriptFiles.Add(newFile); + } + } + } + + return scriptFiles.ToArray(); + } + + return new List().ToArray(); + } + + private SymbolReference FindDeclarationForBuiltinCommand( + CommandInfo cmdInfo, + SymbolReference foundSymbol, + Workspace workspace) + { + SymbolReference foundDefinition = null; + if (cmdInfo != null) + { + int index = 0; + ScriptFile[] nestedModuleFiles; + + nestedModuleFiles = + GetBuiltinCommandScriptFiles( + GetCommandInfo(foundSymbol.SymbolName).Module, + workspace); + + while (foundDefinition == null && index < nestedModuleFiles.Length) + { + foundDefinition = + AstOperations.FindDefinitionOfSymbol( + nestedModuleFiles[index].ScriptAst, + foundSymbol); + if (foundDefinition != null) + { + foundDefinition.FilePath = nestedModuleFiles[index].FilePath; + } + index++; + } + } + + return foundDefinition; + } + #endregion } } diff --git a/src/PowerShellEditorServices/Language/SymbolReference.cs b/src/PowerShellEditorServices/Language/SymbolReference.cs index 84e1609b3..538f752b8 100644 --- a/src/PowerShellEditorServices/Language/SymbolReference.cs +++ b/src/PowerShellEditorServices/Language/SymbolReference.cs @@ -59,6 +59,12 @@ public class SymbolReference /// Gets the contents of the line the given symbol is on /// public string SourceLine { get; internal set; } + + /// + /// Gets the path of the file in which the symbol was found. + /// + public string FilePath { get; internal set; } + #endregion /// @@ -66,13 +72,15 @@ public class SymbolReference /// /// The higher level type of the symbol /// The script extent of the symbol + /// The file path of the symbol /// The line contents of the given symbol (defaults to empty string) - public SymbolReference(SymbolType symbolType, IScriptExtent scriptExtent, string sourceLine = "") + public SymbolReference(SymbolType symbolType, IScriptExtent scriptExtent, string filePath = "", string sourceLine = "") { // TODO: Verify params this.SymbolType = symbolType; this.SymbolName = scriptExtent.Text; this.ScriptRegion = ScriptRegion.Create(scriptExtent); + this.FilePath = filePath; this.SourceLine = sourceLine; // TODO: Make sure end column number usage is correct diff --git a/src/PowerShellEditorServices/PowerShellEditorServices.csproj b/src/PowerShellEditorServices/PowerShellEditorServices.csproj index 0e376e5a7..cce64db17 100644 --- a/src/PowerShellEditorServices/PowerShellEditorServices.csproj +++ b/src/PowerShellEditorServices/PowerShellEditorServices.csproj @@ -72,6 +72,7 @@ + @@ -88,6 +89,7 @@ + diff --git a/src/PowerShellEditorServices/Session/EditorSession.cs b/src/PowerShellEditorServices/Session/EditorSession.cs index 21161ad40..45bca21d6 100644 --- a/src/PowerShellEditorServices/Session/EditorSession.cs +++ b/src/PowerShellEditorServices/Session/EditorSession.cs @@ -6,13 +6,8 @@ using Microsoft.PowerShell.EditorServices.Analysis; using Microsoft.PowerShell.EditorServices.Console; using Microsoft.PowerShell.EditorServices.Language; -using Microsoft.PowerShell.EditorServices.Utility; -using System; -using System.Collections.Generic; -using System.IO; using System.Management.Automation; using System.Management.Automation.Runspaces; -using System.Text; using System.Threading; namespace Microsoft.PowerShell.EditorServices.Session @@ -26,12 +21,16 @@ public class EditorSession #region Private Fields private Runspace languageRunspace; - private Dictionary workspaceFiles = new Dictionary(); #endregion #region Properties + /// + /// Gets the Workspace instance for this session. + /// + public Workspace Workspace { get; private set; } + /// /// Gets the LanguageService instance for this session. /// @@ -63,6 +62,9 @@ public void StartSession(IConsoleHost consoleHost) { InitialSessionState initialSessionState = InitialSessionState.CreateDefault2(); + // Create a workspace to contain open files + this.Workspace = new Workspace(); + // Create a runspace to share between the language and analysis services this.languageRunspace = RunspaceFactory.CreateRunspace(initialSessionState); this.languageRunspace.ApartmentState = ApartmentState.STA; @@ -76,75 +78,6 @@ public void StartSession(IConsoleHost consoleHost) this.ConsoleService = new ConsoleService(consoleHost, initialSessionState); } - /// - /// Opens a script file with the given file path. - /// - /// The file path at which the script resides. - /// - /// is not found. - /// - /// - /// has already been loaded in the session. - /// - /// - /// contains a null or empty string. - /// - public void OpenFile(string filePath) - { - Validate.IsNotNullOrEmptyString("filePath", filePath); - - // Make sure the file isn't already loaded into the session - if (!this.workspaceFiles.ContainsKey(filePath)) - { - // This method allows FileNotFoundException to bubble up - // if the file isn't found. - - using (StreamReader streamReader = new StreamReader(filePath, Encoding.UTF8)) - { - ScriptFile newFile = new ScriptFile(filePath, streamReader); - this.workspaceFiles.Add(filePath, newFile); - } - } - else - { - throw new ArgumentException( - "The specified file has already been loaded: " + filePath, - "filePath"); - } - } - - /// - /// Closes a currently open script file with the given file path. - /// - /// The file path at which the script resides. - public void CloseFile(ScriptFile scriptFile) - { - Validate.IsNotNull("scriptFile", scriptFile); - - this.workspaceFiles.Remove(scriptFile.FilePath); - } - - /// - /// Attempts to get a currently open script file with the given file path. - /// - /// The file path at which the script resides. - /// The output variable in which the ScriptFile will be stored. - /// A ScriptFile instance - public bool TryGetFile(string filePath, out ScriptFile scriptFile) - { - scriptFile = null; - return this.workspaceFiles.TryGetValue(filePath, out scriptFile); - } - - /// - /// Gets all open files in the session. - /// - /// A collection of all open ScriptFiles in the session. - public IEnumerable GetOpenFiles() - { - return this.workspaceFiles.Values; - } - #endregion #region IDisposable Implementation diff --git a/src/PowerShellEditorServices/Session/ScriptFile.cs b/src/PowerShellEditorServices/Session/ScriptFile.cs index 584619c18..560fb7bbd 100644 --- a/src/PowerShellEditorServices/Session/ScriptFile.cs +++ b/src/PowerShellEditorServices/Session/ScriptFile.cs @@ -3,6 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using Microsoft.PowerShell.EditorServices.Language; using Microsoft.PowerShell.EditorServices.Utility; using System; using System.Collections.Generic; @@ -31,6 +32,13 @@ public class ScriptFile /// public string FilePath { get; private set; } + /// + /// Gets or sets a boolean that determines whether + /// semantic analysis should be enabled for this file. + /// For internal use only. + /// + internal bool IsAnalysisEnabled { get; set; } + /// /// Gets a string containing the full contents of the file. /// @@ -78,6 +86,15 @@ public Token[] ScriptTokens get { return this.scriptTokens; } } + /// + /// Gets the array of filepaths dot sourced in this ScriptFile + /// + public string[] ReferencedFiles + { + get; + private set; + } + #endregion #region Constructors @@ -91,6 +108,8 @@ public Token[] ScriptTokens public ScriptFile(string filePath, TextReader textReader) { this.FilePath = filePath; + this.IsAnalysisEnabled = true; + this.ReadFile(textReader); } @@ -208,19 +227,15 @@ public int GetOffsetAtPosition(int lineNumber, int columnNumber) /// A TextReader to use for reading file contents. private void ReadFile(TextReader textReader) { - this.FileLines = new List(); + string fileContents = textReader.ReadToEnd(); - // Read the file contents line by line - string fileLine = null; - do - { - fileLine = textReader.ReadLine(); - if (fileLine != null) - { - FileLines.Add(fileLine); - } - } - while (fileLine != null); + // Split the file contents into lines and trim + // any carriage returns from the strings. + this.FileLines = + fileContents + .Split('\n') + .Select(line => line.TrimEnd('\r')) + .ToList(); // Parse the contents to get syntax tree and errors this.ParseFileContents(); @@ -260,6 +275,10 @@ private void ParseFileContents() parseErrors .Select(ScriptFileMarker.FromParseError) .ToArray(); + + //Get all dot sourced referenced files and store them + this.ReferencedFiles = + AstOperations.FindDotSourcedIncludes(this.ScriptAst); } #endregion diff --git a/src/PowerShellEditorServices/Session/Workspace.cs b/src/PowerShellEditorServices/Session/Workspace.cs new file mode 100644 index 000000000..f63eafdca --- /dev/null +++ b/src/PowerShellEditorServices/Session/Workspace.cs @@ -0,0 +1,178 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Utility; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace Microsoft.PowerShell.EditorServices.Session +{ + /// + /// Manages a "workspace" of script files that are open for a particular + /// editing session. Also helps to navigate references between ScriptFiles. + /// + public class Workspace + { + #region Private Fields + + private Dictionary workspaceFiles = new Dictionary(); + + #endregion + + #region Public Methods + + /// + /// Gets an open file in the workspace. If the file isn't open but + /// exists on the filesystem, load and return it. + /// + /// The file path at which the script resides. + /// + /// is not found. + /// + /// + /// contains a null or empty string. + /// + public ScriptFile GetFile(string filePath) + { + Validate.IsNotNullOrEmptyString("filePath", filePath); + + // Resolve the full file path after making sure that slashes + // are in the right form + string resolvedFilePath = + this.ResolveFilePath( + filePath.Replace('/', '\\')); + + string keyName = resolvedFilePath.ToLower(); + + // Make sure the file isn't already loaded into the workspace + ScriptFile scriptFile = null; + if (!this.workspaceFiles.TryGetValue(keyName, out scriptFile)) + { + // This method allows FileNotFoundException to bubble up + // if the file isn't found. + + using (StreamReader streamReader = new StreamReader(resolvedFilePath, Encoding.UTF8)) + { + scriptFile = new ScriptFile(resolvedFilePath, streamReader); + this.workspaceFiles.Add(keyName, scriptFile); + } + } + + return scriptFile; + } + + /// + /// Closes a currently open script file with the given file path. + /// + /// The file path at which the script resides. + public void CloseFile(ScriptFile scriptFile) + { + Validate.IsNotNull("scriptFile", scriptFile); + + this.workspaceFiles.Remove(scriptFile.FilePath); + } + + /// + /// Gets all file references by recursively searching + /// through referenced files in a scriptfile + /// + /// Contains the details and contents of an open script file + /// A scriptfile array where the first file + /// in the array is the "root file" of the search + public ScriptFile[] ExpandScriptReferences(ScriptFile scriptFile) + { + Dictionary referencedScriptFiles = new Dictionary(); + List expandedReferences = new List(); + + // add original file so it's not searched for, then find all file references + referencedScriptFiles.Add(scriptFile.FilePath, scriptFile); + RecursivelyFindReferences(scriptFile, referencedScriptFiles); + + // remove original file from referened file and add it as the first element of the + // expanded referenced list to maintain order so the original file is always first in the list + referencedScriptFiles.Remove(scriptFile.FilePath); + expandedReferences.Add(scriptFile); + + if (referencedScriptFiles.Count > 0) + { + expandedReferences.AddRange(referencedScriptFiles.Values); + } + + return expandedReferences.ToArray(); + } + + #endregion + + #region Private Methods + + /// + /// Recusrively searches through referencedFiles in scriptFiles + /// and builds a Dictonary of the file references + /// + /// Details an contents of "root" script file + /// A Dictionary of referenced script files + private void RecursivelyFindReferences( + ScriptFile scriptFile, + Dictionary referencedScriptFiles) + { + ScriptFile referencedFile; + foreach (string referencedFileName in scriptFile.ReferencedFiles) + { + string resolvedScriptPath = + this.ResolveRelativeScriptPath( + scriptFile.FilePath, + referencedFileName); + + // make sure file exists before trying to get the file + if (File.Exists(resolvedScriptPath)) + { + // Get the referenced file if it's not already in referencedScriptFiles + referencedFile = this.GetFile(resolvedScriptPath); + if (!referencedScriptFiles.ContainsKey(resolvedScriptPath)) + { + referencedScriptFiles.Add(resolvedScriptPath, referencedFile); + RecursivelyFindReferences(referencedFile, referencedScriptFiles); + } + } + } + } + + private string ResolveFilePath(string scriptPath) + { + return Path.GetFullPath(scriptPath); + } + + private string ResolveRelativeScriptPath(string originalScriptPath, string relativePath) + { + if (!Path.IsPathRooted(originalScriptPath)) + { + // TODO: Assert instead? + throw new InvalidOperationException( + string.Format( + "Must provide a full path for originalScriptPath: {0}", + originalScriptPath)); + } + + if (Path.IsPathRooted(relativePath)) + { + return relativePath; + } + + // Get the directory of the original script file, combine it + // with the given path and then resolve the absolute file path. + string combinedPath = + Path.GetFullPath( + Path.Combine( + Path.GetDirectoryName(originalScriptPath), + relativePath)); + + return combinedPath; + } + + #endregion + } +} diff --git a/test/PowerShellEditorServices.Test.Host/LanguageServiceManager.cs b/test/PowerShellEditorServices.Test.Host/LanguageServiceManager.cs index a3e12e052..c728dc7db 100644 --- a/test/PowerShellEditorServices.Test.Host/LanguageServiceManager.cs +++ b/test/PowerShellEditorServices.Test.Host/LanguageServiceManager.cs @@ -40,7 +40,7 @@ public void Start() { FileName = "Microsoft.PowerShell.EditorServices.Host.exe", Arguments = languageServiceArguments, - CreateNoWindow = false, + CreateNoWindow = true, UseShellExecute = false, RedirectStandardInput = true, RedirectStandardOutput = true, diff --git a/test/PowerShellEditorServices.Test.Host/ScenarioTests.cs b/test/PowerShellEditorServices.Test.Host/ScenarioTests.cs index 976cf7d22..3b4fffffc 100644 --- a/test/PowerShellEditorServices.Test.Host/ScenarioTests.cs +++ b/test/PowerShellEditorServices.Test.Host/ScenarioTests.cs @@ -216,7 +216,7 @@ public void FindsNoReferencesOfEmptyLine() Arguments = new FileLocationRequestArgs { File = "TestFiles\\FindReferences.ps1", - Line = 9, + Line = 10, Offset = 1, } }); @@ -256,7 +256,7 @@ public void FindsReferencesOnCommand() Arguments = new FileLocationRequestArgs { File = "TestFiles\\FindReferences.ps1", - Line = 10, + Line = 9, Offset = 2, } }); @@ -297,7 +297,7 @@ public void FindsNoDefinitionOfBuiltinCommand() Arguments = new FileLocationRequestArgs { File = "TestFiles\\FindReferences.ps1", - Line = 12, + Line = 11, Offset = 10, } }); @@ -315,7 +315,7 @@ public void FindsDefintionOfVariable() Arguments = new FileLocationRequestArgs { File = "TestFiles\\FindReferences.ps1", - Line = 10, + Line = 9, Offset = 14, } }); @@ -358,7 +358,7 @@ public void GetsParameterHintsOnCommand() Arguments = new SignatureHelpRequestArgs { File = "TestFiles\\FindReferences.ps1", - Line = 14, + Line = 13, Offset = 15, } }); diff --git a/test/PowerShellEditorServices.Test.Host/TestFiles/FindReferences.ps1 b/test/PowerShellEditorServices.Test.Host/TestFiles/FindReferences.ps1 index 9373c10f4..c9ae71c5b 100644 --- a/test/PowerShellEditorServices.Test.Host/TestFiles/FindReferences.ps1 +++ b/test/PowerShellEditorServices.Test.Host/TestFiles/FindReferences.ps1 @@ -6,7 +6,6 @@ function My-Function ($myInput) $things = 4 $things - My-Function $things Write-Output "Hi"; diff --git a/test/PowerShellEditorServices.Test.Shared/Definition/FindsFunctionDefinitionInDotSourceReference.cs b/test/PowerShellEditorServices.Test.Shared/Definition/FindsFunctionDefinitionInDotSourceReference.cs new file mode 100644 index 000000000..0e2bce134 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Definition/FindsFunctionDefinitionInDotSourceReference.cs @@ -0,0 +1,20 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Session; + +namespace Microsoft.PowerShell.EditorServices.Test.Shared.Definition +{ + public class FindsFunctionDefinitionInDotSourceReference + { + public static readonly ScriptRegion SourceDetails = + new ScriptRegion + { + File = @"References\FileWithReferences.ps1", + StartLineNumber = 3, + StartColumnNumber = 6 + }; + } +} diff --git a/test/PowerShellEditorServices.Test.Shared/PowerShellEditorServices.Test.Shared.csproj b/test/PowerShellEditorServices.Test.Shared/PowerShellEditorServices.Test.Shared.csproj index ae967bbcd..4c676a9f5 100644 --- a/test/PowerShellEditorServices.Test.Shared/PowerShellEditorServices.Test.Shared.csproj +++ b/test/PowerShellEditorServices.Test.Shared/PowerShellEditorServices.Test.Shared.csproj @@ -42,6 +42,7 @@ + @@ -49,12 +50,15 @@ + + - - + + PreserveNewest + @@ -63,14 +67,18 @@ - + PreserveNewest - + - + + + + + PreserveNewest - + diff --git a/test/PowerShellEditorServices.Test.Shared/References/FileWithReferences.ps1 b/test/PowerShellEditorServices.Test.Shared/References/FileWithReferences.ps1 new file mode 100644 index 000000000..fb39070e8 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/References/FileWithReferences.ps1 @@ -0,0 +1,3 @@ +. .\SimpleFile.ps1 + +My-Function "test" diff --git a/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesOnBuiltInCommandWithAlias.cs b/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesOnBuiltInCommandWithAlias.cs new file mode 100644 index 000000000..ac18ec9f2 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesOnBuiltInCommandWithAlias.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Language; +using Microsoft.PowerShell.EditorServices.Session; + +namespace Microsoft.PowerShell.EditorServices.Test.Shared.References +{ + public class FindsReferencesOnBuiltInCommandWithAlias + { + public static readonly ScriptRegion SourceDetails = + new ScriptRegion + { + File = @"References\SimpleFile.ps1", + StartLineNumber = 14, + StartColumnNumber = 3 + }; + } + public class FindsReferencesOnBuiltInAlias + { + public static readonly ScriptRegion SourceDetails = + new ScriptRegion + { + File = @"References\SimpleFile.ps1", + StartLineNumber = 15, + StartColumnNumber = 2 + }; + } +} diff --git a/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesOnFunctionMultiFileDotSource.cs b/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesOnFunctionMultiFileDotSource.cs new file mode 100644 index 000000000..bcbd70327 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesOnFunctionMultiFileDotSource.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Language; +using Microsoft.PowerShell.EditorServices.Session; + +namespace Microsoft.PowerShell.EditorServices.Test.Shared.References +{ + public class FindsReferencesOnFunctionMultiFileDotSourceFileB + { + public static readonly ScriptRegion SourceDetails = + new ScriptRegion + { + File = @"References\ReferenceFileB.ps1", + StartLineNumber = 5, + StartColumnNumber = 8 + }; + } + public class FindsReferencesOnFunctionMultiFileDotSourceFileC + { + public static readonly ScriptRegion SourceDetails = + new ScriptRegion + { + File = @"References\ReferenceFileC.ps1", + StartLineNumber = 4, + StartColumnNumber = 10 + }; + } +} diff --git a/test/PowerShellEditorServices.Test.Shared/References/ReferenceFileA.ps1 b/test/PowerShellEditorServices.Test.Shared/References/ReferenceFileA.ps1 new file mode 100644 index 000000000..a43f59fb2 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/References/ReferenceFileA.ps1 @@ -0,0 +1,9 @@ +. .\ReferenceFileA.ps1 +. .\ReferenceFileB.ps1 +. .\ReferenceFileC.ps1 + +function My-Function ($myInput) +{ + My-Function $myInput +} +Get-ChildItem \ No newline at end of file diff --git a/test/PowerShellEditorServices.Test.Shared/References/ReferenceFileB.ps1 b/test/PowerShellEditorServices.Test.Shared/References/ReferenceFileB.ps1 new file mode 100644 index 000000000..add70c1d6 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/References/ReferenceFileB.ps1 @@ -0,0 +1,5 @@ +. .\ReferenceFileC.ps1 + +Get-ChildItem + +My-Function "testb" \ No newline at end of file diff --git a/test/PowerShellEditorServices.Test.Shared/References/ReferenceFileC.ps1 b/test/PowerShellEditorServices.Test.Shared/References/ReferenceFileC.ps1 new file mode 100644 index 000000000..e17fd096f --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/References/ReferenceFileC.ps1 @@ -0,0 +1,4 @@ +. .\ReferenceFileA.ps1 +Get-ChildItem + +My-Function "testc" \ No newline at end of file diff --git a/test/PowerShellEditorServices.Test.Shared/References/SimpleFile.ps1 b/test/PowerShellEditorServices.Test.Shared/References/SimpleFile.ps1 index 43d36b86a..7e9022cf2 100644 --- a/test/PowerShellEditorServices.Test.Shared/References/SimpleFile.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/References/SimpleFile.ps1 @@ -9,4 +9,12 @@ $things My-Function $things -Write-Output "Hello World"; \ No newline at end of file +Write-Output "Hello World"; + +Get-ChildItem +ls +gci +dir +LS +Write-Host +Get-ChildItem \ No newline at end of file diff --git a/test/PowerShellEditorServices.Test.Shared/Utility/ResourceFileLoader.cs b/test/PowerShellEditorServices.Test.Shared/Utility/ResourceFileLoader.cs deleted file mode 100644 index 66636d1ba..000000000 --- a/test/PowerShellEditorServices.Test.Shared/Utility/ResourceFileLoader.cs +++ /dev/null @@ -1,42 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using Microsoft.PowerShell.EditorServices.Session; -using System.IO; -using System.Reflection; - -namespace Microsoft.PowerShell.EditorServices.Test.Shared.Utility -{ - public class ResourceFileLoader - { - private Assembly resourceAssembly; - - public ResourceFileLoader(Assembly resourceAssembly = null) - { - if (resourceAssembly == null) - { - resourceAssembly = Assembly.GetExecutingAssembly(); - } - - this.resourceAssembly = resourceAssembly; - } - - public ScriptFile LoadFile(string fileName) - { - // Convert the filename to the proper format - string resourceName = - string.Format( - "{0}.{1}", - resourceAssembly.GetName().Name, - fileName.Replace('\\', '.')); - - using (Stream stream = resourceAssembly.GetManifestResourceStream(resourceName)) - using (StreamReader streamReader = new StreamReader(stream)) - { - return new ScriptFile(fileName, streamReader); - } - } - } -} diff --git a/test/PowerShellEditorServices.Test/Language/LanguageServiceTests.cs b/test/PowerShellEditorServices.Test/Language/LanguageServiceTests.cs index f05bda85e..aeb9a5377 100644 --- a/test/PowerShellEditorServices.Test/Language/LanguageServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Language/LanguageServiceTests.cs @@ -6,31 +6,28 @@ using Microsoft.PowerShell.EditorServices.Language; using Microsoft.PowerShell.EditorServices.Session; using Microsoft.PowerShell.EditorServices.Test.Shared.Completion; -using Microsoft.PowerShell.EditorServices.Test.Shared.Utility; +using Microsoft.PowerShell.EditorServices.Test.Shared.Definition; +using Microsoft.PowerShell.EditorServices.Test.Shared.Occurrences; +using Microsoft.PowerShell.EditorServices.Test.Shared.ParameterHint; +using Microsoft.PowerShell.EditorServices.Test.Shared.References; using System; +using System.IO; +using System.Linq; using System.Management.Automation.Runspaces; using System.Threading; -using System.Linq; using Xunit; -using Microsoft.PowerShell.EditorServices.Test.Shared.ParameterHint; -using Microsoft.PowerShell.EditorServices.Test.Shared.Definition; -using Microsoft.PowerShell.EditorServices.Test.Shared.References; -using Microsoft.PowerShell.EditorServices.Test.Shared.Occurrences; namespace Microsoft.PowerShell.EditorServices.Test.Language { public class LanguageServiceTests : IDisposable { - private ResourceFileLoader fileLoader; + private Workspace workspace; private Runspace languageServiceRunspace; private LanguageService languageService; public LanguageServiceTests() { - // Load script files from the shared assembly - this.fileLoader = - new ResourceFileLoader( - typeof(CompleteCommandInFile).Assembly); + this.workspace = new Workspace(); this.languageServiceRunspace = RunspaceFactory.CreateRunspace(); this.languageServiceRunspace.ApartmentState = ApartmentState.STA; @@ -122,40 +119,33 @@ public void LanguageServiceFindsFunctionDefinition() } [Fact] - public void LanguageServiceFindsVariableDefinition() + public void LanguageServiceFindsFunctionDefinitionInDotSourceReference() { GetDefinitionResult definitionResult = this.GetDefinition( - FindsVariableDefinition.SourceDetails); + FindsFunctionDefinitionInDotSourceReference.SourceDetails); SymbolReference definition = definitionResult.FoundDefinition; - Assert.Equal(6, definition.ScriptRegion.StartLineNumber); - Assert.Equal(1, definition.ScriptRegion.StartColumnNumber); - Assert.Equal("$things", definition.SymbolName); - } - - [Fact] - public void LanguageServiceFindsFunctionReferences() - { - FindReferencesResult referencesResult = - this.GetReferences( - FindsReferencesOnFunction.SourceDetails); - - Assert.Equal(3, referencesResult.FoundReferences.Count()); - Assert.Equal(1, referencesResult.FoundReferences.First().ScriptRegion.StartLineNumber); - Assert.Equal(10, referencesResult.FoundReferences.First().ScriptRegion.StartColumnNumber); + Assert.True( + definitionResult.FoundDefinition.FilePath.EndsWith( + FindsFunctionDefinition.SourceDetails.File), + "Unexpected reference file: " + definitionResult.FoundDefinition.FilePath); + Assert.Equal(1, definition.ScriptRegion.StartLineNumber); + Assert.Equal(10, definition.ScriptRegion.StartColumnNumber); + Assert.Equal("My-Function", definition.SymbolName); } [Fact] - public void LanguageServiceFindsVariableReferences() + public void LanguageServiceFindsVariableDefinition() { - FindReferencesResult referencesResult = - this.GetReferences( - FindsReferencesOnVariable.SourceDetails); + GetDefinitionResult definitionResult = + this.GetDefinition( + FindsVariableDefinition.SourceDetails); - Assert.Equal(3, referencesResult.FoundReferences.Count()); - Assert.Equal(10, referencesResult.FoundReferences.Last().ScriptRegion.StartLineNumber); - Assert.Equal(13, referencesResult.FoundReferences.Last().ScriptRegion.StartColumnNumber); + SymbolReference definition = definitionResult.FoundDefinition; + Assert.Equal(6, definition.ScriptRegion.StartLineNumber); + Assert.Equal(1, definition.ScriptRegion.StartColumnNumber); + Assert.Equal("$things", definition.SymbolName); } [Fact] @@ -182,10 +172,64 @@ public void LanguageServiceFindsOccurrencesOnParameter() Assert.Equal(3, occurrencesResult.FoundOccurrences.Last().ScriptRegion.StartLineNumber); } - private ScriptFile GetScriptFile(ScriptRegion scriptRegion){ - return this.fileLoader.LoadFile(scriptRegion.File); + [Fact] + public void LanguageServiceFindsReferencesOnCommandWithAlias() + { + FindReferencesResult refsResult = + this.GetReferences( + FindsReferencesOnBuiltInCommandWithAlias.SourceDetails); + + Assert.Equal(6, refsResult.FoundReferences.Count()); + Assert.Equal("Get-ChildItem", refsResult.FoundReferences.Last().SymbolName); + Assert.Equal("ls", refsResult.FoundReferences.ToArray()[1].SymbolName); + } + + [Fact] + public void LanguageServiceFindsReferencesOnAlias() + { + FindReferencesResult refsResult = + this.GetReferences( + FindsReferencesOnBuiltInCommandWithAlias.SourceDetails); + + Assert.Equal(6, refsResult.FoundReferences.Count()); + Assert.Equal("Get-ChildItem", refsResult.FoundReferences.Last().SymbolName); + Assert.Equal("gci", refsResult.FoundReferences.ToArray()[2].SymbolName); + Assert.Equal("LS", refsResult.FoundReferences.ToArray()[4].SymbolName); + } + + [Fact] + public void LanguageServiceFindsReferencesOnFileWithReferencesFileB() + { + FindReferencesResult refsResult = + this.GetReferences( + FindsReferencesOnFunctionMultiFileDotSourceFileB.SourceDetails); + + Assert.Equal(4, refsResult.FoundReferences.Count()); + } + + [Fact] + public void LanguageServiceFindsReferencesOnFileWithReferencesFileC() + { + FindReferencesResult refsResult = + this.GetReferences( + FindsReferencesOnFunctionMultiFileDotSourceFileC.SourceDetails); + Assert.Equal(4, refsResult.FoundReferences.Count()); } + + private ScriptFile GetScriptFile(ScriptRegion scriptRegion) + { + const string baseSharedScriptPath = + @"..\..\..\PowerShellEditorServices.Test.Shared\"; + + string resolvedPath = + Path.Combine( + baseSharedScriptPath, + scriptRegion.File); + return + this.workspace.GetFile( + resolvedPath); + } private CompletionResults GetCompletionResults(ScriptRegion scriptRegion) { @@ -208,20 +252,39 @@ private ParameterSetSignatures GetParamSetSignatures(ScriptRegion scriptRegion) private GetDefinitionResult GetDefinition(ScriptRegion scriptRegion) { - return - this.languageService.GetDefinitionInFile( - GetScriptFile(scriptRegion), + ScriptFile scriptFile = GetScriptFile(scriptRegion); + + SymbolReference symbolReference = + this.languageService.FindSymbolAtLocation( + scriptFile, scriptRegion.StartLineNumber, scriptRegion.StartColumnNumber); + + Assert.NotNull(symbolReference); + + return + this.languageService.GetDefinitionOfSymbol( + scriptFile, + symbolReference, + this.workspace); } private FindReferencesResult GetReferences(ScriptRegion scriptRegion) { - return - this.languageService.FindReferencesInFile( - GetScriptFile(scriptRegion), + ScriptFile scriptFile = GetScriptFile(scriptRegion); + + SymbolReference symbolReference = + this.languageService.FindSymbolAtLocation( + scriptFile, scriptRegion.StartLineNumber, scriptRegion.StartColumnNumber); + + Assert.NotNull(symbolReference); + + return + this.languageService.FindReferencesOfSymbol( + symbolReference, + this.workspace.ExpandScriptReferences(scriptFile)); } private FindOccurrencesResult GetOccurrences(ScriptRegion scriptRegion) diff --git a/test/PowerShellEditorServices.Test/PowerShellEditorServices.Test.csproj b/test/PowerShellEditorServices.Test/PowerShellEditorServices.Test.csproj index 1bf55103e..bc4a81fc8 100644 --- a/test/PowerShellEditorServices.Test/PowerShellEditorServices.Test.csproj +++ b/test/PowerShellEditorServices.Test/PowerShellEditorServices.Test.csproj @@ -63,7 +63,7 @@ - + diff --git a/test/PowerShellEditorServices.Test/Session/FileChangeTests.cs b/test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs similarity index 84% rename from test/PowerShellEditorServices.Test/Session/FileChangeTests.cs rename to test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs index ffef79453..826190604 100644 --- a/test/PowerShellEditorServices.Test/Session/FileChangeTests.cs +++ b/test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs @@ -123,6 +123,25 @@ public void CanApplyMultiLineDelete() }); } + [Fact] + public void FindsDotSourcedFiles() + { + string exampleScriptContents = + @". .\athing.ps1"+"\r\n"+ + @". .\somefile.ps1"+"\r\n" + + @". .\somefile.ps1"+"\r\n" + + @"Do-Stuff $uri"+"\r\n" + + @". simpleps.ps1"; + + using (StringReader stringReader = new StringReader(exampleScriptContents)) + { + ScriptFile scriptFile = new ScriptFile("DotSourceTestFile.ps1", stringReader); + Assert.Equal(3, scriptFile.ReferencedFiles.Length); + System.Console.Write("a" + scriptFile.ReferencedFiles[0]); + Assert.Equal(@".\athing.ps1", scriptFile.ReferencedFiles[0]); + } + } + private void AssertFileChange( string initialString, string expectedString,