diff --git a/src/PowerShellEditorServices/Extensions/Api/DocumentSymbolService.cs b/src/PowerShellEditorServices/Extensions/Api/DocumentSymbolService.cs deleted file mode 100644 index 58137ce49..000000000 --- a/src/PowerShellEditorServices/Extensions/Api/DocumentSymbolService.cs +++ /dev/null @@ -1,277 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Management.Automation.Language; -using Microsoft.PowerShell.EditorServices.Services; -using Microsoft.PowerShell.EditorServices.Services.TextDocument; - -using Internal = Microsoft.PowerShell.EditorServices.Services.Symbols; - -// TODO: This is currently disabled in the csproj -// Redesign this API and bring it back once it's fit for purpose - -namespace Microsoft.PowerShell.EditorServices.Extensions.Services -{ - /// - /// A way to define symbols on a higher level - /// - public enum SymbolType - { - /// - /// The symbol type is unknown - /// - Unknown = 0, - - /// - /// The symbol is a vairable - /// - Variable = 1, - - /// - /// The symbol is a function - /// - Function = 2, - - /// - /// The symbol is a parameter - /// - Parameter = 3, - - /// - /// The symbol is a DSC configuration - /// - Configuration = 4, - - /// - /// The symbol is a workflow - /// - Workflow = 5, - - /// - /// The symbol is a hashtable key - /// - HashtableKey = 6, - } - - /// - /// Interface to instantiate to create a provider of document symbols. - /// - public interface IDocumentSymbolProvider - { - /// - /// The unique ID of this provider. - /// - string ProviderId { get; } - - /// - /// Run this provider to provide symbols to PSES from the given file. - /// - /// The script file to provide symbols for. - /// Symbols about the file. - IEnumerable ProvideDocumentSymbols(IEditorScriptFile scriptFile); - } - - /// - /// A class that holds the type, name, script extent, and source line of a symbol - /// - [DebuggerDisplay("SymbolType = {SymbolType}, SymbolName = {SymbolName}")] - public class SymbolReference - { - /// - /// Constructs an instance of a 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) - : this(symbolType, scriptExtent.Text, scriptExtent) - { - } - - /// - /// Constructs and instance of a SymbolReference - /// - /// The higher level type of the symbol - /// The name 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, - string symbolName, - IScriptExtent scriptExtent) - : this(symbolType, scriptExtent, symbolName, filePath: string.Empty, sourceLine: string.Empty) - { - } - - public SymbolReference( - SymbolType symbolType, - IScriptExtent scriptExtent, - string symbolName, - string filePath) - : this(symbolType, scriptExtent, symbolName, filePath, sourceLine: string.Empty) - { - } - - public SymbolReference(SymbolType symbolType, IScriptExtent scriptExtent, string symbolName, string filePath, string sourceLine) - { - // TODO: Verify params - SymbolType = symbolType; - ScriptRegion = ScriptRegion.Create(scriptExtent); - SymbolName = symbolName; - FilePath = filePath; - SourceLine = sourceLine; - - // TODO: Make sure end column number usage is correct - } - - #region Properties - - /// - /// Gets the symbol's type - /// - public SymbolType SymbolType { get; } - - /// - /// Gets the name of the symbol - /// - public string SymbolName { get; } - - /// - /// Gets the script extent of the symbol - /// - public ScriptRegion ScriptRegion { get; } - - /// - /// Gets the contents of the line the given symbol is on - /// - public string SourceLine { get; } - - /// - /// Gets the path of the file in which the symbol was found. - /// - public string FilePath { get; internal set; } - - #endregion - } - - /// - /// Service for registration of document symbol providers in PSES. - /// - public interface IDocumentSymbolService - { - /// - /// Register a document symbol provider by its ID. - /// If another provider is already registered by the same ID, this will fail and return false. - /// - /// The document symbol provider to register. - /// True if the symbol provider was successfully registered, false otherwise. - bool RegisterDocumentSymbolProvider(IDocumentSymbolProvider documentSymbolProvider); - - /// - /// Deregister a symbol provider of the given ID. - /// - /// The ID of the provider to deregister. - /// True if a provider by the given ID was deregistered, false if no such provider was found. - bool DeregisterDocumentSymbolProvider(string providerId); - } - - internal class DocumentSymbolService : IDocumentSymbolService - { - private readonly SymbolsService _symbolsService; - - internal DocumentSymbolService(SymbolsService symbolsService) - { - _symbolsService = symbolsService; - } - - public bool RegisterDocumentSymbolProvider(IDocumentSymbolProvider documentSymbolProvider) - { - return _symbolsService.TryRegisterDocumentSymbolProvider(new ExternalDocumentSymbolProviderAdapter(documentSymbolProvider)); - } - - public bool DeregisterDocumentSymbolProvider(string providerId) - { - return _symbolsService.DeregisterCodeLensProvider(providerId); - } - } - - internal class ExternalDocumentSymbolProviderAdapter : Internal.IDocumentSymbolProvider - { - private readonly IDocumentSymbolProvider _symbolProvider; - - public ExternalDocumentSymbolProviderAdapter( - IDocumentSymbolProvider externalDocumentSymbolProvider) - { - _symbolProvider = externalDocumentSymbolProvider; - } - - public string ProviderId => _symbolProvider.ProviderId; - - public IEnumerable ProvideDocumentSymbols(ScriptFile scriptFile) - { - foreach (SymbolReference symbolReference in _symbolProvider.ProvideDocumentSymbols(new EditorScriptFile(scriptFile))) - { - yield return new ExternalSymbolReferenceAdapter(symbolReference); - } - } - } - - internal class ExternalSymbolReferenceAdapter : Internal.ISymbolReference - { - private readonly SymbolReference _symbolReference; - - public ExternalSymbolReferenceAdapter(SymbolReference symbolReference) - { - _symbolReference = symbolReference; - } - - public Internal.SymbolType SymbolType => _symbolReference.SymbolType.ToInternalSymbolType(); - - public string SymbolName => _symbolReference.SymbolName; - - public ScriptRegion ScriptRegion => _symbolReference.ScriptRegion; - - public string SourceLine => _symbolReference.SourceLine; - - public string FilePath => _symbolReference.FilePath; - } - - internal static class SymbolTypeExtensions - { - public static Internal.SymbolType ToInternalSymbolType(this SymbolType symbolType) - { - switch (symbolType) - { - case SymbolType.Unknown: - return Internal.SymbolType.Unknown; - - case SymbolType.Variable: - return Internal.SymbolType.Variable; - - case SymbolType.Function: - return Internal.SymbolType.Function; - - case SymbolType.Parameter: - return Internal.SymbolType.Parameter; - - case SymbolType.Configuration: - return Internal.SymbolType.Configuration; - - case SymbolType.Workflow: - return Internal.SymbolType.Workflow; - - case SymbolType.HashtableKey: - return Internal.SymbolType.HashtableKey; - - default: - throw new InvalidOperationException($"Unknown symbol type '{symbolType}'"); - } - } - } -} - diff --git a/src/PowerShellEditorServices/Extensions/Api/EditorExtensionServiceProvider.cs b/src/PowerShellEditorServices/Extensions/Api/EditorExtensionServiceProvider.cs index dc2809962..d750923fb 100644 --- a/src/PowerShellEditorServices/Extensions/Api/EditorExtensionServiceProvider.cs +++ b/src/PowerShellEditorServices/Extensions/Api/EditorExtensionServiceProvider.cs @@ -42,7 +42,6 @@ internal EditorExtensionServiceProvider(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; LanguageServer = new LanguageServerService(_serviceProvider.GetService()); - //DocumentSymbols = new DocumentSymbolService(_serviceProvider.GetService()); ExtensionCommands = new ExtensionCommandService(_serviceProvider.GetService()); Workspace = new WorkspaceService(_serviceProvider.GetService()); EditorContext = new EditorContextService(_serviceProvider.GetService()); diff --git a/src/PowerShellEditorServices/PowerShellEditorServices.csproj b/src/PowerShellEditorServices/PowerShellEditorServices.csproj index 9eefce9de..ef9b2696b 100644 --- a/src/PowerShellEditorServices/PowerShellEditorServices.csproj +++ b/src/PowerShellEditorServices/PowerShellEditorServices.csproj @@ -55,7 +55,6 @@ - diff --git a/src/PowerShellEditorServices/Services/CodeLens/PesterCodeLensProvider.cs b/src/PowerShellEditorServices/Services/CodeLens/PesterCodeLensProvider.cs index 881bbc037..50ddcfc22 100644 --- a/src/PowerShellEditorServices/Services/CodeLens/PesterCodeLensProvider.cs +++ b/src/PowerShellEditorServices/Services/CodeLens/PesterCodeLensProvider.cs @@ -7,7 +7,6 @@ using Microsoft.PowerShell.EditorServices.Services; using Microsoft.PowerShell.EditorServices.Services.Symbols; using Microsoft.PowerShell.EditorServices.Services.TextDocument; -using Microsoft.PowerShell.EditorServices.Utility; using Newtonsoft.Json.Linq; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Serialization; @@ -47,7 +46,7 @@ public PesterCodeLensProvider(ConfigurationService configurationService) private static CodeLens[] GetPesterLens(PesterSymbolReference pesterSymbol, ScriptFile scriptFile) { string word = pesterSymbol.Command == PesterCommandType.It ? "test" : "tests"; - CodeLens[] codeLensResults = new CodeLens[] + return new CodeLens[] { new CodeLens() { @@ -92,8 +91,6 @@ private static CodeLens[] GetPesterLens(PesterSymbolReference pesterSymbol, Scri } } }; - - return codeLensResults; } /// @@ -120,7 +117,7 @@ public CodeLens[] ProvideCodeLenses(ScriptFile scriptFile, CancellationToken can continue; } - // Skip codelense for setup/teardown block + // Skip CodeLens for setup/teardown block if (!PesterSymbolReference.IsPesterTestCommand(pesterSymbol.Command)) { continue; diff --git a/src/PowerShellEditorServices/Services/CodeLens/ReferencesCodeLensProvider.cs b/src/PowerShellEditorServices/Services/CodeLens/ReferencesCodeLensProvider.cs index eb50ce2d5..6e1e4cc81 100644 --- a/src/PowerShellEditorServices/Services/CodeLens/ReferencesCodeLensProvider.cs +++ b/src/PowerShellEditorServices/Services/CodeLens/ReferencesCodeLensProvider.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.Collections.Generic; using System.Text; using System.Threading; @@ -9,7 +8,6 @@ using Microsoft.PowerShell.EditorServices.Services; using Microsoft.PowerShell.EditorServices.Services.Symbols; using Microsoft.PowerShell.EditorServices.Services.TextDocument; -using Microsoft.PowerShell.EditorServices.Utility; using Newtonsoft.Json.Linq; using OmniSharp.Extensions.LanguageServer.Protocol; using OmniSharp.Extensions.LanguageServer.Protocol.Models; @@ -22,8 +20,6 @@ namespace Microsoft.PowerShell.EditorServices.CodeLenses /// internal class ReferencesCodeLensProvider : ICodeLensProvider { - private static readonly Location[] s_emptyLocationArray = Array.Empty(); - /// /// The document symbol provider to supply symbols to generate the code lenses. /// @@ -57,14 +53,19 @@ public ReferencesCodeLensProvider(WorkspaceService workspaceService, SymbolsServ /// /// The PowerShell script file to get code lenses for. /// - /// An array of CodeLenses describing all functions in the given script file. + /// An array of CodeLenses describing all functions, classes and enums in the given script file. public CodeLens[] ProvideCodeLenses(ScriptFile scriptFile, CancellationToken cancellationToken) { List acc = new(); - foreach (SymbolReference sym in _symbolProvider.ProvideDocumentSymbols(scriptFile)) + foreach (SymbolReference symbol in _symbolProvider.ProvideDocumentSymbols(scriptFile)) { cancellationToken.ThrowIfCancellationRequested(); - if (sym.SymbolType == SymbolType.Function) + // TODO: Can we support more here? + if (symbol.IsDeclaration && + symbol.Type is + SymbolType.Function or + SymbolType.Class or + SymbolType.Enum) { acc.Add(new CodeLens { @@ -73,7 +74,7 @@ public CodeLens[] ProvideCodeLenses(ScriptFile scriptFile, CancellationToken can Uri = scriptFile.DocumentUri, ProviderId = nameof(ReferencesCodeLensProvider) }, LspSerializer.Instance.JsonSerializer), - Range = sym.ScriptRegion.ToRange(), + Range = symbol.NameRegion.ToRange(), }); } } @@ -82,68 +83,49 @@ public CodeLens[] ProvideCodeLenses(ScriptFile scriptFile, CancellationToken can } /// - /// Take a codelens and create a new codelens object with updated references. + /// Take a CodeLens and create a new CodeLens object with updated references. /// /// The old code lens to get updated references for. /// /// - /// A new code lens object describing the same data as the old one but with updated references. + /// A new CodeLens object describing the same data as the old one but with updated references. public async Task ResolveCodeLens( CodeLens codeLens, ScriptFile scriptFile, CancellationToken cancellationToken) { - ScriptFile[] references = _workspaceService.ExpandScriptReferences( - scriptFile); - - SymbolReference foundSymbol = SymbolsService.FindFunctionDefinitionAtLocation( + SymbolReference foundSymbol = SymbolsService.FindSymbolDefinitionAtLocation( scriptFile, codeLens.Range.Start.Line + 1, codeLens.Range.Start.Character + 1); - List referencesResult = await _symbolsService.FindReferencesOfSymbol( - foundSymbol, - references, - _workspaceService, - cancellationToken).ConfigureAwait(false); - - Location[] referenceLocations; - if (referencesResult == null) - { - referenceLocations = s_emptyLocationArray; - } - else + List acc = new(); + foreach (SymbolReference foundReference in await _symbolsService.ScanForReferencesOfSymbolAsync( + foundSymbol, cancellationToken).ConfigureAwait(false)) { - List acc = new(); - foreach (SymbolReference foundReference in referencesResult) + // We only show lenses on declarations, so we exclude those from the references. + if (foundReference.IsDeclaration) { - // This async method is pretty dense with synchronous code - // so it's helpful to add some yields. - await Task.Yield(); - cancellationToken.ThrowIfCancellationRequested(); - if (IsReferenceDefinition(foundSymbol, foundReference)) - { - continue; - } - - DocumentUri uri = DocumentUri.From(foundReference.FilePath); - // For any vscode-notebook-cell, we need to ignore the backing file on disk. - if (uri.Scheme == "file" && - scriptFile.DocumentUri.Scheme == "vscode-notebook-cell" && - uri.Path == scriptFile.DocumentUri.Path) - { - continue; - } + continue; + } - acc.Add(new Location - { - Uri = uri, - Range = foundReference.ScriptRegion.ToRange() - }); + DocumentUri uri = DocumentUri.From(foundReference.FilePath); + // For any vscode-notebook-cell, we need to ignore the backing file on disk. + if (uri.Scheme == "file" && + scriptFile.DocumentUri.Scheme == "vscode-notebook-cell" && + uri.Path == scriptFile.DocumentUri.Path) + { + continue; } - referenceLocations = acc.ToArray(); + + acc.Add(new Location + { + Uri = uri, + Range = foundReference.NameRegion.ToRange() + }); } + Location[] referenceLocations = acc.ToArray(); return new CodeLens { Data = codeLens.Data, @@ -163,27 +145,6 @@ public async Task ResolveCodeLens( }; } - /// - /// Check whether a SymbolReference is the actual definition of that symbol. - /// - /// The symbol definition that may be referenced. - /// The reference symbol to check. - /// True if the reference is not a reference to the definition, false otherwise. - private static bool IsReferenceDefinition( - SymbolReference definition, - SymbolReference reference) - { - // First check if we are in the same file as the definition. if we are... - // check if it's on the same line number. - - // TODO: Do we care about two symbol definitions of the same name? - // if we do, how could we possibly know that a reference in one file is a reference - // of a particular symbol definition? - return - definition.FilePath == reference.FilePath && - definition.ScriptRegion.StartLineNumber == reference.ScriptRegion.StartLineNumber; - } - /// /// Get the code lens header for the number of references on a definition, /// given the number of references. diff --git a/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs b/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs index 39e12974b..3c5e7a1d2 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Console/LegacyReadLine.cs @@ -35,7 +35,9 @@ public LegacyReadLine( _onIdleAction = onIdleAction; } +#pragma warning disable CA1502 // Cyclomatic complexity we don't care about public override string ReadLine(CancellationToken cancellationToken) +#pragma warning restore CA1502 { string inputBeforeCompletion = null; string inputAfterCompletion = null; diff --git a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs index 5a6c8fb31..419aebf8c 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs @@ -29,7 +29,9 @@ namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Host using Microsoft.PowerShell.EditorServices.Server; using OmniSharp.Extensions.DebugAdapter.Protocol.Server; +#pragma warning disable CA1506 // Coupling complexity we don't care about internal class PsesInternalHost : PSHost, IHostSupportsInteractiveSession, IRunspaceContext, IInternalPowerShellExecutionService +#pragma warning restore CA1506 { internal const string DefaultPrompt = "> "; diff --git a/src/PowerShellEditorServices/Services/PowerShell/Utility/CommandHelpers.cs b/src/PowerShellEditorServices/Services/PowerShell/Utility/CommandHelpers.cs index f5590a026..da4a89a34 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Utility/CommandHelpers.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Utility/CommandHelpers.cs @@ -14,6 +14,7 @@ namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility { /// /// Provides utility methods for working with PowerShell commands. + /// TODO: Handle the `fn ` prefix better. /// internal static class CommandHelpers { @@ -114,6 +115,12 @@ public static async Task GetCommandInfoAsync( Validate.IsNotNull(nameof(commandName), commandName); Validate.IsNotNull(nameof(executionService), executionService); + // Remove the bucket identifier from symbol references. + if (commandName.StartsWith("fn ")) + { + commandName = commandName.Substring(3); + } + // If we have a CommandInfo cached, return that. if (s_commandInfoCache.TryGetValue(commandName, out CommandInfo cmdInfo)) { @@ -239,11 +246,11 @@ public static async Task GetAliasesAsync( // TODO: When we move to netstandard2.1, we can use another overload which generates // static delegates and thus reduces allocations. s_cmdletToAliasCache.AddOrUpdate( - aliasInfo.Definition, - (_) => new List { aliasInfo.Name }, - (_, v) => { v.Add(aliasInfo.Name); return v; }); + "fn " + aliasInfo.Definition, + (_) => new List { "fn " + aliasInfo.Name }, + (_, v) => { v.Add("fn " + aliasInfo.Name); return v; }); - s_aliasToCmdletCache.TryAdd(aliasInfo.Name, aliasInfo.Definition); + s_aliasToCmdletCache.TryAdd("fn " + aliasInfo.Name, "fn " + aliasInfo.Definition); } return new AliasMap( diff --git a/src/PowerShellEditorServices/Services/Symbols/IDocumentSymbolProvider.cs b/src/PowerShellEditorServices/Services/Symbols/IDocumentSymbolProvider.cs index 3b01e382a..6abcdb162 100644 --- a/src/PowerShellEditorServices/Services/Symbols/IDocumentSymbolProvider.cs +++ b/src/PowerShellEditorServices/Services/Symbols/IDocumentSymbolProvider.cs @@ -20,6 +20,6 @@ internal interface IDocumentSymbolProvider /// The document for which SymbolReferences should be provided. /// /// An IEnumerable collection of SymbolReferences. - IEnumerable ProvideDocumentSymbols(ScriptFile scriptFile); + IEnumerable ProvideDocumentSymbols(ScriptFile scriptFile); } } diff --git a/src/PowerShellEditorServices/Services/Symbols/ParameterSetSignatures.cs b/src/PowerShellEditorServices/Services/Symbols/ParameterSetSignatures.cs index aa094ce7b..8f8af2ca4 100644 --- a/src/PowerShellEditorServices/Services/Symbols/ParameterSetSignatures.cs +++ b/src/PowerShellEditorServices/Services/Symbols/ParameterSetSignatures.cs @@ -46,8 +46,8 @@ public ParameterSetSignatures(IEnumerable commandInfoSe paramSetSignatures.Add(new ParameterSetSignature(setInfo)); } Signatures = paramSetSignatures.ToArray(); - CommandName = foundSymbol.ScriptRegion.Text; - ScriptRegion = foundSymbol.ScriptRegion; + CommandName = foundSymbol.NameRegion.Text; + ScriptRegion = foundSymbol.NameRegion; } } diff --git a/src/PowerShellEditorServices/Services/Symbols/PesterDocumentSymbolProvider.cs b/src/PowerShellEditorServices/Services/Symbols/PesterDocumentSymbolProvider.cs index 3c6469bc6..f38e72372 100644 --- a/src/PowerShellEditorServices/Services/Symbols/PesterDocumentSymbolProvider.cs +++ b/src/PowerShellEditorServices/Services/Symbols/PesterDocumentSymbolProvider.cs @@ -17,7 +17,7 @@ internal class PesterDocumentSymbolProvider : IDocumentSymbolProvider { string IDocumentSymbolProvider.ProviderId => nameof(PesterDocumentSymbolProvider); - IEnumerable IDocumentSymbolProvider.ProvideDocumentSymbols( + IEnumerable IDocumentSymbolProvider.ProvideDocumentSymbols( ScriptFile scriptFile) { if (!scriptFile.FilePath.EndsWith( @@ -75,6 +75,8 @@ private static bool IsPesterCommand(CommandAst commandAst) return true; } + private static readonly char[] DefinitionTrimChars = new char[] { ' ', '{' }; + /// /// Convert a CommandAst known to represent a Pester command and a reference to the scriptfile /// it is in into symbol representing a Pester call for code lens @@ -84,7 +86,11 @@ private static bool IsPesterCommand(CommandAst commandAst) /// a symbol representing the Pester call containing metadata for CodeLens to use private static PesterSymbolReference ConvertPesterAstToSymbolReference(ScriptFile scriptFile, CommandAst pesterCommandAst) { - string testLine = scriptFile.GetLine(pesterCommandAst.Extent.StartLineNumber); + string symbolName = scriptFile + .GetLine(pesterCommandAst.Extent.StartLineNumber) + .TrimStart() + .TrimEnd(DefinitionTrimChars); + PesterCommandType? commandName = PesterSymbolReference.GetCommandType(pesterCommandAst.GetCommandName()); if (commandName == null) { @@ -126,7 +132,7 @@ private static PesterSymbolReference ConvertPesterAstToSymbolReference(ScriptFil return new PesterSymbolReference( scriptFile, commandName.Value, - testLine, + symbolName, testName, pesterCommandAst.Extent ); @@ -196,7 +202,7 @@ internal enum PesterCommandType /// Provides a specialization of SymbolReference containing /// extra information about Pester test symbols. /// - internal class PesterSymbolReference : SymbolReference + internal record PesterSymbolReference : SymbolReference { /// /// Lookup for Pester keywords we support. Ideally we could extract these from Pester itself @@ -206,10 +212,9 @@ internal class PesterSymbolReference : SymbolReference .Cast() .ToDictionary(pct => pct.ToString(), pct => pct, StringComparer.OrdinalIgnoreCase); - private static readonly char[] DefinitionTrimChars = new char[] { ' ', '{' }; - /// /// Gets the name of the test + /// TODO: We could get rid of this and use DisplayName now, but first attempt didn't work great. /// public string TestName { get; } @@ -221,15 +226,17 @@ internal class PesterSymbolReference : SymbolReference internal PesterSymbolReference( ScriptFile scriptFile, PesterCommandType commandType, - string testLine, + string symbolName, string testName, IScriptExtent scriptExtent) : base( SymbolType.Function, - testLine.TrimStart().TrimEnd(DefinitionTrimChars), + symbolName, + symbolName + " { }", + scriptExtent, scriptExtent, - scriptFile.FilePath, - testLine) + scriptFile, + isDeclaration: true) { Command = commandType; TestName = testName; diff --git a/src/PowerShellEditorServices/Services/Symbols/PsdDocumentSymbolProvider.cs b/src/PowerShellEditorServices/Services/Symbols/PsdDocumentSymbolProvider.cs index ff3ffe47b..a0a032578 100644 --- a/src/PowerShellEditorServices/Services/Symbols/PsdDocumentSymbolProvider.cs +++ b/src/PowerShellEditorServices/Services/Symbols/PsdDocumentSymbolProvider.cs @@ -17,13 +17,13 @@ internal class PsdDocumentSymbolProvider : IDocumentSymbolProvider { string IDocumentSymbolProvider.ProviderId => nameof(PsdDocumentSymbolProvider); - IEnumerable IDocumentSymbolProvider.ProvideDocumentSymbols( + IEnumerable IDocumentSymbolProvider.ProvideDocumentSymbols( ScriptFile scriptFile) { if ((scriptFile.FilePath?.EndsWith(".psd1", StringComparison.OrdinalIgnoreCase) == true) || IsPowerShellDataFileAst(scriptFile.ScriptAst)) { - FindHashtableSymbolsVisitor findHashtableSymbolsVisitor = new(); + FindHashtableSymbolsVisitor findHashtableSymbolsVisitor = new(scriptFile); scriptFile.ScriptAst.Visit(findHashtableSymbolsVisitor); return findHashtableSymbolsVisitor.SymbolReferences; } @@ -35,7 +35,7 @@ IEnumerable IDocumentSymbolProvider.ProvideDocumentSymbols( /// Checks if a given ast represents the root node of a *.psd1 file. /// /// The abstract syntax tree of the given script - /// true if the AST represts a *.psd1 file, otherwise false + /// true if the AST represents a *.psd1 file, otherwise false public static bool IsPowerShellDataFileAst(Ast ast) { // sometimes we don't have reliable access to the filename diff --git a/src/PowerShellEditorServices/Services/Symbols/ReferenceTable.cs b/src/PowerShellEditorServices/Services/Symbols/ReferenceTable.cs index 2c3242c83..620fe2ea1 100644 --- a/src/PowerShellEditorServices/Services/Symbols/ReferenceTable.cs +++ b/src/PowerShellEditorServices/Services/Symbols/ReferenceTable.cs @@ -7,8 +7,9 @@ using System.Collections.Concurrent; using System.Management.Automation.Language; using Microsoft.PowerShell.EditorServices.Services.TextDocument; -using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility; using Microsoft.PowerShell.EditorServices.Services.Symbols; +using System.Collections.Generic; +using System.Linq; namespace Microsoft.PowerShell.EditorServices.Services; @@ -19,14 +20,14 @@ internal sealed class ReferenceTable { private readonly ScriptFile _parent; - private readonly ConcurrentDictionary> _symbolReferences = new(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentDictionary> _symbolReferences = new(StringComparer.OrdinalIgnoreCase); private bool _isInited; public ReferenceTable(ScriptFile parent) => _parent = parent; /// - /// Clears the reference table causing it to rescan the source AST when queried. + /// Clears the reference table causing it to re-scan the source AST when queried. /// public void TagAsChanged() { @@ -34,15 +35,40 @@ public void TagAsChanged() _isInited = false; } - // Prefer checking if the dictionary has contents to determine if initialized. The field - // `_isInited` is to guard against rescanning files with no command references, but will - // generally be less reliable of a check. + /// + /// Prefer checking if the dictionary has contents to determine if initialized. The field + /// `_isInited` is to guard against re-scanning files with no command references, but will + /// generally be less reliable of a check. + /// private bool IsInitialized => !_symbolReferences.IsEmpty || _isInited; - internal bool TryGetReferences(string command, out ConcurrentBag? references) + internal IEnumerable TryGetReferences(SymbolReference? symbol) { EnsureInitialized(); - return _symbolReferences.TryGetValue(command, out references); + return symbol is not null + && _symbolReferences.TryGetValue(symbol.Id, out ConcurrentBag? bag) + ? bag + : Enumerable.Empty(); + } + + // Gets symbol whose name contains the position + internal SymbolReference? TryGetSymbolAtPosition(int line, int column) => GetAllReferences() + .FirstOrDefault(i => i.NameRegion.ContainsPosition(line, column)); + + // Gets symbol whose whole extent contains the position + internal SymbolReference? TryGetSymbolContainingPosition(int line, int column) => GetAllReferences() + .FirstOrDefault(i => i.ScriptRegion.ContainsPosition(line, column)); + + internal IEnumerable GetAllReferences() + { + EnsureInitialized(); + foreach (ConcurrentBag bag in _symbolReferences.Values) + { + foreach (SymbolReference symbol in bag) + { + yield return symbol; + } + } } internal void EnsureInitialized() @@ -52,67 +78,26 @@ internal void EnsureInitialized() return; } - _parent.ScriptAst.Visit(new ReferenceVisitor(this)); + _parent.ScriptAst.Visit(new SymbolVisitor(_parent, AddReference)); } - private void AddReference(string symbol, IScriptExtent extent) + private AstVisitAction AddReference(SymbolReference symbol) { + // We have to exclude implicit things like `$this` that don't actually exist. + if (symbol.ScriptRegion.IsEmpty()) + { + return AstVisitAction.Continue; + } + _symbolReferences.AddOrUpdate( - symbol, - _ => new ConcurrentBag { extent }, + symbol.Id, + _ => new ConcurrentBag { symbol }, (_, existing) => { - existing.Add(extent); + existing.Add(symbol); return existing; }); - } - private sealed class ReferenceVisitor : AstVisitor - { - private readonly ReferenceTable _references; - - public ReferenceVisitor(ReferenceTable references) => _references = references; - - public override AstVisitAction VisitCommand(CommandAst commandAst) - { - string? commandName = GetCommandName(commandAst); - if (string.IsNullOrEmpty(commandName)) - { - return AstVisitAction.Continue; - } - - _references.AddReference( - CommandHelpers.StripModuleQualification(commandName, out _), - commandAst.CommandElements[0].Extent); - - return AstVisitAction.Continue; - - static string? GetCommandName(CommandAst commandAst) - { - string commandName = commandAst.GetCommandName(); - if (!string.IsNullOrEmpty(commandName)) - { - return commandName; - } - - if (commandAst.CommandElements[0] is not ExpandableStringExpressionAst expandableStringExpressionAst) - { - return null; - } - - return AstOperations.TryGetInferredValue(expandableStringExpressionAst, out string value) ? value : null; - } - } - - public override AstVisitAction VisitVariableExpression(VariableExpressionAst variableExpressionAst) - { - // TODO: Consider tracking unscoped variable references only when they declared within - // the same function definition. - _references.AddReference( - $"${variableExpressionAst.VariablePath.UserPath}", - variableExpressionAst.Extent); - - return AstVisitAction.Continue; - } + return AstVisitAction.Continue; } } diff --git a/src/PowerShellEditorServices/Services/Symbols/ScriptDocumentSymbolProvider.cs b/src/PowerShellEditorServices/Services/Symbols/ScriptDocumentSymbolProvider.cs index 92b33c8de..c2f60f86b 100644 --- a/src/PowerShellEditorServices/Services/Symbols/ScriptDocumentSymbolProvider.cs +++ b/src/PowerShellEditorServices/Services/Symbols/ScriptDocumentSymbolProvider.cs @@ -2,8 +2,6 @@ // Licensed under the MIT License. using System.Collections.Generic; -using System.Linq; -using System.Management.Automation.Language; using Microsoft.PowerShell.EditorServices.Services.TextDocument; namespace Microsoft.PowerShell.EditorServices.Services.Symbols @@ -16,37 +14,7 @@ internal class ScriptDocumentSymbolProvider : IDocumentSymbolProvider { string IDocumentSymbolProvider.ProviderId => nameof(ScriptDocumentSymbolProvider); - IEnumerable IDocumentSymbolProvider.ProvideDocumentSymbols( - ScriptFile scriptFile) - { - // If we have an AST, then we know it's a PowerShell file - // so lets try to find symbols in the document. - return scriptFile?.ScriptAst != null - ? FindSymbolsInDocument(scriptFile.ScriptAst) - : Enumerable.Empty(); - } - - /// - /// Finds all symbols in a script - /// - /// The abstract syntax tree of the given script - /// A collection of SymbolReference objects - public static IEnumerable FindSymbolsInDocument(Ast scriptAst) - { - // TODO: Restore this when we figure out how to support multiple - // PS versions in the new PSES-as-a-module world (issue #276) - // if (powerShellVersion >= new Version(5,0)) - // { - //#if PowerShell v5 - // FindSymbolsVisitor2 findSymbolsVisitor = new FindSymbolsVisitor2(); - // scriptAst.Visit(findSymbolsVisitor); - // symbolReferences = findSymbolsVisitor.SymbolReferences; - //#endif - // } - // else - FindSymbolsVisitor findSymbolsVisitor = new(); - scriptAst.Visit(findSymbolsVisitor); - return findSymbolsVisitor.SymbolReferences; - } + IEnumerable IDocumentSymbolProvider.ProvideDocumentSymbols( + ScriptFile scriptFile) => scriptFile.References.GetAllReferences(); } } diff --git a/src/PowerShellEditorServices/Services/Symbols/ScriptExtent.cs b/src/PowerShellEditorServices/Services/Symbols/ScriptExtent.cs index 2224be725..1cd87f44f 100644 --- a/src/PowerShellEditorServices/Services/Symbols/ScriptExtent.cs +++ b/src/PowerShellEditorServices/Services/Symbols/ScriptExtent.cs @@ -91,6 +91,8 @@ public int EndOffset set; } + public override string ToString() => Text; + /// /// Gets the ending script position of the extent. /// diff --git a/src/PowerShellEditorServices/Services/Symbols/SymbolDetails.cs b/src/PowerShellEditorServices/Services/Symbols/SymbolDetails.cs index 80a1dd8b4..ddca0f0a7 100644 --- a/src/PowerShellEditorServices/Services/Symbols/SymbolDetails.cs +++ b/src/PowerShellEditorServices/Services/Symbols/SymbolDetails.cs @@ -12,6 +12,7 @@ namespace Microsoft.PowerShell.EditorServices.Services.Symbols { /// /// Provides detailed information for a given symbol. + /// TODO: Get rid of this and just use return documentation. /// [DebuggerDisplay("SymbolReference = {SymbolReference.SymbolType}/{SymbolReference.SymbolName}, DisplayString = {DisplayString}")] internal class SymbolDetails @@ -23,11 +24,6 @@ internal class SymbolDetails /// public SymbolReference SymbolReference { get; private set; } - /// - /// Gets the display string for this symbol. - /// - public string DisplayString { get; private set; } - /// /// Gets the documentation string for this symbol. Returns an /// empty string if the symbol has no documentation. @@ -48,43 +44,28 @@ internal static async Task CreateAsync( SymbolReference = symbolReference }; - switch (symbolReference.SymbolType) + if (symbolReference.Type is SymbolType.Function) { - case SymbolType.Function: - CommandInfo commandInfo = await CommandHelpers.GetCommandInfoAsync( - symbolReference.SymbolName, - currentRunspace, - executionService).ConfigureAwait(false); - - if (commandInfo != null) + CommandInfo commandInfo = await CommandHelpers.GetCommandInfoAsync( + symbolReference.Id, + currentRunspace, + executionService).ConfigureAwait(false); + + if (commandInfo is not null) + { + symbolDetails.Documentation = + await CommandHelpers.GetCommandSynopsisAsync( + commandInfo, + executionService).ConfigureAwait(false); + + if (commandInfo.CommandType == CommandTypes.Application) { - symbolDetails.Documentation = - await CommandHelpers.GetCommandSynopsisAsync( - commandInfo, - executionService).ConfigureAwait(false); - - if (commandInfo.CommandType == CommandTypes.Application) - { - symbolDetails.DisplayString = "(application) " + symbolReference.SymbolName; - return symbolDetails; - } + symbolDetails.SymbolReference = symbolReference with { Name = $"(application) ${symbolReference.Name}" }; } - - symbolDetails.DisplayString = "function " + symbolReference.SymbolName; - return symbolDetails; - - case SymbolType.Parameter: - // TODO: Get parameter help - symbolDetails.DisplayString = "(parameter) " + symbolReference.SymbolName; - return symbolDetails; - - case SymbolType.Variable: - symbolDetails.DisplayString = symbolReference.SymbolName; - return symbolDetails; - - default: - return symbolDetails; + } } + + return symbolDetails; } #endregion diff --git a/src/PowerShellEditorServices/Services/Symbols/SymbolReference.cs b/src/PowerShellEditorServices/Services/Symbols/SymbolReference.cs index 25cb15cc1..2c6c6ef8f 100644 --- a/src/PowerShellEditorServices/Services/Symbols/SymbolReference.cs +++ b/src/PowerShellEditorServices/Services/Symbols/SymbolReference.cs @@ -1,113 +1,70 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +#nullable enable + using System.Diagnostics; using System.Management.Automation.Language; using Microsoft.PowerShell.EditorServices.Services.TextDocument; namespace Microsoft.PowerShell.EditorServices.Services.Symbols { - internal interface ISymbolReference - { - /// - /// Gets the symbol's type - /// - SymbolType SymbolType { get; } - - /// - /// Gets the name of the symbol - /// - string SymbolName { get; } - - /// - /// Gets the script extent of the symbol - /// - ScriptRegion ScriptRegion { get; } - - /// - /// Gets the contents of the line the given symbol is on - /// - string SourceLine { get; } - - /// - /// Gets the path of the file in which the symbol was found. - /// - string FilePath { get; } - } - /// /// A class that holds the type, name, script extent, and source line of a symbol /// - [DebuggerDisplay("SymbolType = {SymbolType}, SymbolName = {SymbolName}")] - internal class SymbolReference : ISymbolReference + [DebuggerDisplay("Type = {Type}, Id = {Id}, Name = {Name}")] + internal record SymbolReference { - #region Properties + public SymbolType Type { get; init; } - /// - /// Gets the symbol's type - /// - public SymbolType SymbolType { get; } + public string Id { get; init; } - /// - /// Gets the name of the symbol - /// - public string SymbolName { get; } + public string Name { get; init; } - /// - /// Gets the script extent of the symbol - /// - public ScriptRegion ScriptRegion { get; } + public ScriptRegion NameRegion { get; init; } + + public ScriptRegion ScriptRegion { get; init; } - /// - /// 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 + public bool IsDeclaration { get; init; } /// /// Constructs and instance of a SymbolReference /// - /// The higher level type of the symbol - /// The name of the symbol + /// The higher level type of the symbol + /// The name of the symbol + /// The string used by outline, hover, etc. + /// The extent of the symbol's name /// The script extent of the symbol - /// The file path of the symbol - /// The line contents of the given symbol (defaults to empty string) + /// The script file that has the symbol + /// True if this reference is the definition of the symbol public SymbolReference( - SymbolType symbolType, - string symbolName, + SymbolType type, + string id, + string name, + IScriptExtent nameExtent, IScriptExtent scriptExtent, - string filePath = "", - string sourceLine = "") - { - // TODO: Verify params - SymbolType = symbolType; - SymbolName = symbolName; - ScriptRegion = ScriptRegion.Create(scriptExtent); - FilePath = filePath; - SourceLine = sourceLine; - - // TODO: Make sure end column number usage is correct - - // Build the display string - //this.DisplayString = - // string.Format( - // "{0} {1}") - } - - /// - /// Constructs an instance of a SymbolReference - /// - /// The higher level type of the symbol - /// The script extent of the symbol - public SymbolReference(SymbolType symbolType, IScriptExtent scriptExtent) - : this(symbolType, scriptExtent.Text, scriptExtent, scriptExtent.File, "") + ScriptFile file, + bool isDeclaration) { + Type = type; + Id = id; + Name = name; + NameRegion = new(nameExtent); + ScriptRegion = new(scriptExtent); + FilePath = file.FilePath; + try + { + SourceLine = file.GetLine(ScriptRegion.StartLineNumber); + } + catch (System.ArgumentOutOfRangeException) + { + SourceLine = string.Empty; + } + IsDeclaration = isDeclaration; } } } diff --git a/src/PowerShellEditorServices/Services/Symbols/SymbolType.cs b/src/PowerShellEditorServices/Services/Symbols/SymbolType.cs index 02778b106..6533e7726 100644 --- a/src/PowerShellEditorServices/Services/Symbols/SymbolType.cs +++ b/src/PowerShellEditorServices/Services/Symbols/SymbolType.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using OmniSharp.Extensions.LanguageServer.Protocol.Models; + namespace Microsoft.PowerShell.EditorServices.Services.Symbols { /// @@ -14,7 +16,7 @@ internal enum SymbolType Unknown = 0, /// - /// The symbol is a vairable + /// The symbol is a variable /// Variable, @@ -41,6 +43,62 @@ internal enum SymbolType /// /// The symbol is a hashtable key /// - HashtableKey + HashtableKey, + + /// + /// The symbol is a class + /// + Class, + + /// + /// The symbol is a enum + /// + Enum, + + /// + /// The symbol is a enum member/value + /// + EnumMember, + + /// + /// The symbol is a class property + /// + Property, + + /// + /// The symbol is a class method + /// + Method, + + /// + /// The symbol is a class constructor + /// + Constructor, + + /// + /// The symbol is a type reference + /// + Type, + } + + internal static class SymbolTypeUtils + { + internal static SymbolKind GetSymbolKind(SymbolType symbolType) + { + return symbolType switch + { + SymbolType.Function or SymbolType.Configuration or SymbolType.Workflow => SymbolKind.Function, + SymbolType.Enum => SymbolKind.Enum, + SymbolType.Class => SymbolKind.Class, + SymbolType.Constructor => SymbolKind.Constructor, + SymbolType.Method => SymbolKind.Method, + SymbolType.Property => SymbolKind.Property, + SymbolType.EnumMember => SymbolKind.EnumMember, + SymbolType.Variable or SymbolType.Parameter => SymbolKind.Variable, + SymbolType.HashtableKey => SymbolKind.Key, + SymbolType.Type => SymbolKind.TypeParameter, + SymbolType.Unknown or _ => SymbolKind.Object, + }; + } } } diff --git a/src/PowerShellEditorServices/Services/Symbols/SymbolsService.cs b/src/PowerShellEditorServices/Services/Symbols/SymbolsService.cs index f052520f0..cf4040642 100644 --- a/src/PowerShellEditorServices/Services/Symbols/SymbolsService.cs +++ b/src/PowerShellEditorServices/Services/Symbols/SymbolsService.cs @@ -1,16 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +#nullable enable + using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Collections.Specialized; -using System.IO; using System.Linq; using System.Management.Automation; using System.Management.Automation.Language; -using System.Runtime.InteropServices; -using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -42,8 +40,9 @@ internal class SymbolsService private readonly ConcurrentDictionary _codeLensProviders; private readonly ConcurrentDictionary _documentSymbolProviders; private readonly ConfigurationService _configurationService; - #endregion + private Task? _workspaceScanCompleted; + #endregion Private Fields #region Constructors /// @@ -72,12 +71,13 @@ public SymbolsService( if (configurationService.CurrentSettings.EnableReferencesCodeLens) { ReferencesCodeLensProvider referencesProvider = new(_workspaceService, this); - _codeLensProviders.TryAdd(referencesProvider.ProviderId, referencesProvider); + _ = _codeLensProviders.TryAdd(referencesProvider.ProviderId, referencesProvider); } PesterCodeLensProvider pesterProvider = new(configurationService); - _codeLensProviders.TryAdd(pesterProvider.ProviderId, pesterProvider); + _ = _codeLensProviders.TryAdd(pesterProvider.ProviderId, pesterProvider); + // TODO: Is this complication so necessary? _documentSymbolProviders = new ConcurrentDictionary(); IDocumentSymbolProvider[] documentSymbolProviders = new IDocumentSymbolProvider[] { @@ -85,13 +85,14 @@ public SymbolsService( new PsdDocumentSymbolProvider(), new PesterDocumentSymbolProvider(), }; + foreach (IDocumentSymbolProvider documentSymbolProvider in documentSymbolProviders) { - _documentSymbolProviders.TryAdd(documentSymbolProvider.ProviderId, documentSymbolProvider); + _ = _documentSymbolProviders.TryAdd(documentSymbolProvider.ProviderId, documentSymbolProvider); } } - #endregion + #endregion Constructors public bool TryRegisterCodeLensProvider(ICodeLensProvider codeLensProvider) => _codeLensProviders.TryAdd(codeLensProvider.ProviderId, codeLensProvider); @@ -106,268 +107,139 @@ public SymbolsService( public IEnumerable GetDocumentSymbolProviders() => _documentSymbolProviders.Values; /// - /// Finds all the symbols in a file. + /// Finds all the symbols in a file, through all document symbol providers. /// /// The ScriptFile in which the symbol can be located. - /// - public List FindSymbolsInFile(ScriptFile scriptFile) + public IEnumerable FindSymbolsInFile(ScriptFile scriptFile) { Validate.IsNotNull(nameof(scriptFile), scriptFile); - List foundOccurrences = new(); foreach (IDocumentSymbolProvider symbolProvider in GetDocumentSymbolProviders()) { - foreach (SymbolReference reference in symbolProvider.ProvideDocumentSymbols(scriptFile)) + foreach (SymbolReference symbol in symbolProvider.ProvideDocumentSymbols(scriptFile)) { - reference.SourceLine = scriptFile.GetLine(reference.ScriptRegion.StartLineNumber); - reference.FilePath = scriptFile.FilePath; - foundOccurrences.Add(reference); + yield return symbol; } } - - return foundOccurrences; } /// - /// Finds the 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 column number of the cursor for the given script - /// A SymbolReference of the symbol found at the given location - /// or null if there is no symbol at that location - /// - public static SymbolReference FindSymbolAtLocation( - ScriptFile scriptFile, - int lineNumber, - int columnNumber) + public static SymbolReference? FindSymbolAtLocation( + ScriptFile scriptFile, int line, int column) + { + Validate.IsNotNull(nameof(scriptFile), scriptFile); + return scriptFile.References.TryGetSymbolAtPosition(line, column); + } + + // Using a private method here to get a bit more readability and to avoid roslynator + // asserting we should use a giant nested ternary. + private static string[] GetIdentifiers(string symbolName, SymbolType symbolType, CommandHelpers.AliasMap aliases) { - SymbolReference symbolReference = - AstOperations.FindSymbolAtPosition( - scriptFile.ScriptAst, - lineNumber, - columnNumber); + if (symbolType is not SymbolType.Function) + { + return new[] { symbolName }; + } - if (symbolReference != null) + if (!aliases.CmdletToAliases.TryGetValue(symbolName, out List foundAliasList)) { - symbolReference.FilePath = scriptFile.FilePath; + return new[] { symbolName }; } - return symbolReference; + return foundAliasList.Prepend(symbolName) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); } /// - /// Finds all the references of a symbol + /// Finds all the references of a symbol in the workspace, resolving aliases. + /// TODO: One day use IAsyncEnumerable. /// - /// The symbol to find all references for - /// An array of scriptFiles too search for references in - /// The workspace that will be searched for symbols - /// - /// FindReferencesResult - public async Task> FindReferencesOfSymbol( - SymbolReference foundSymbol, - ScriptFile[] referencedFiles, - WorkspaceService workspace, + public async Task> ScanForReferencesOfSymbolAsync( + SymbolReference symbol, CancellationToken cancellationToken = default) { - if (foundSymbol == null) + if (symbol is null) { - return null; + return Enumerable.Empty(); } + // TODO: Should we handle aliases at a lower level? CommandHelpers.AliasMap aliases = await CommandHelpers.GetAliasesAsync( _executionService, cancellationToken).ConfigureAwait(false); - string targetName = foundSymbol.SymbolName; - if (foundSymbol.SymbolType is SymbolType.Function) + string targetName = symbol.Id; + if (symbol.Type is SymbolType.Function + && aliases.AliasToCmdlets.TryGetValue(symbol.Id, out string aliasDefinition)) { - targetName = CommandHelpers.StripModuleQualification(targetName, out _); - if (aliases.AliasToCmdlets.TryGetValue(foundSymbol.SymbolName, out string aliasDefinition)) - { - targetName = aliasDefinition; - } - } - - // We want to look for references first in referenced files, hence we use ordered dictionary - // TODO: File system case-sensitivity is based on filesystem not OS, but OS is a much cheaper heuristic - OrderedDictionary fileMap = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) - ? new OrderedDictionary() - : new OrderedDictionary(StringComparer.OrdinalIgnoreCase); - - foreach (ScriptFile scriptFile in referencedFiles) - { - fileMap[scriptFile.FilePath] = scriptFile; + targetName = aliasDefinition; } await ScanWorkspacePSFiles(cancellationToken).ConfigureAwait(false); - List symbolReferences = new(); - - // Using a nested method here to get a bit more readability and to avoid roslynator - // asserting we should use a giant nested ternary here. - static string[] GetIdentifiers(string symbolName, SymbolType symbolType, CommandHelpers.AliasMap aliases) - { - if (symbolType is not SymbolType.Function) - { - return new[] { symbolName }; - } - - if (!aliases.CmdletToAliases.TryGetValue(symbolName, out List foundAliasList)) - { - return new[] { symbolName }; - } - - return foundAliasList.Prepend(symbolName) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToArray(); - } - - string[] allIdentifiers = GetIdentifiers(targetName, foundSymbol.SymbolType, aliases); + List symbols = new(); + string[] allIdentifiers = GetIdentifiers(targetName, symbol.Type, aliases); foreach (ScriptFile file in _workspaceService.GetOpenedFiles()) { foreach (string targetIdentifier in allIdentifiers) { - if (!file.References.TryGetReferences(targetIdentifier, out ConcurrentBag references)) - { - continue; - } - - foreach (IScriptExtent extent in references.OrderBy(e => e.StartOffset)) - { - SymbolReference reference = new( - SymbolType.Function, - foundSymbol.SymbolName, - extent); - - try - { - reference.SourceLine = file.GetLine(reference.ScriptRegion.StartLineNumber); - } - catch (ArgumentOutOfRangeException e) - { - reference.SourceLine = string.Empty; - _logger.LogException("Found reference is out of range in script file", e); - } - reference.FilePath = file.FilePath; - symbolReferences.Add(reference); - } - await Task.Yield(); cancellationToken.ThrowIfCancellationRequested(); + symbols.AddRange(file.References.TryGetReferences(symbol with { Id = targetIdentifier })); } } - return symbolReferences; + return symbols; } /// - /// Finds all the occurrences of a symbol in the script given a file location + /// Finds all the occurrences of a symbol in the script given a file location. + /// TODO: Doesn't support aliases, is it worth it? /// - /// The details and contents of a open script file - /// The line number of the cursor for the given script - /// The column number of the cursor for the given script - /// FindOccurrencesResult - public static IReadOnlyList FindOccurrencesInFile( - ScriptFile file, - int symbolLineNumber, - int symbolColumnNumber) - { - SymbolReference foundSymbol = AstOperations.FindSymbolAtPosition( - file.ScriptAst, - symbolLineNumber, - symbolColumnNumber); - - if (foundSymbol == null) - { - return null; - } - - return AstOperations.FindReferencesOfSymbol(file.ScriptAst, foundSymbol).ToArray(); - } + public static IEnumerable FindOccurrencesInFile( + ScriptFile scriptFile, int line, int column) => scriptFile + .References + .TryGetReferences(FindSymbolAtLocation(scriptFile, line, column)); /// - /// Finds a function definition in the script given a file location + /// Finds the symbol at the location and returns it if it's a declaration. /// - /// The details and contents of a open script file - /// The line number of the cursor for the given script - /// The column number of the cursor for the given script - /// A SymbolReference of the symbol found at the given location - /// or null if there is no symbol at that location - /// - public static SymbolReference FindFunctionDefinitionAtLocation( - ScriptFile scriptFile, - int lineNumber, - int columnNumber) + public static SymbolReference? FindSymbolDefinitionAtLocation( + ScriptFile scriptFile, int line, int column) { - SymbolReference symbolReference = - AstOperations.FindSymbolAtPosition( - scriptFile.ScriptAst, - lineNumber, - columnNumber, - includeFunctionDefinitions: true); - - if (symbolReference != null) - { - symbolReference.FilePath = scriptFile.FilePath; - } - - return symbolReference; + SymbolReference? symbol = FindSymbolAtLocation(scriptFile, line, column); + return symbol?.IsDeclaration == true ? symbol : null; } /// /// Finds the details of the symbol at the given script file location. /// - /// The ScriptFile in which the symbol can be located. - /// The line number at which the symbol can be located. - /// The column number at which the symbol can be located. - /// - public Task FindSymbolDetailsAtLocationAsync( - ScriptFile scriptFile, - int lineNumber, - int columnNumber) + public Task FindSymbolDetailsAtLocationAsync( + ScriptFile scriptFile, int line, int column) { - SymbolReference symbolReference = - AstOperations.FindSymbolAtPosition( - scriptFile.ScriptAst, - lineNumber, - columnNumber); - - if (symbolReference == null) - { - return Task.FromResult(null); - } - - symbolReference.FilePath = scriptFile.FilePath; - return SymbolDetails.CreateAsync( - symbolReference, - _runspaceContext.CurrentRunspace, - _executionService); + SymbolReference? symbol = FindSymbolAtLocation(scriptFile, line, column); + return symbol is null + ? Task.FromResult(null) + : SymbolDetails.CreateAsync(symbol, _runspaceContext.CurrentRunspace, _executionService); } /// /// Finds the parameter set hints of a specific command (determined by a given file location) /// - /// The details and contents of a open script file - /// The line number of the cursor for the given script - /// The column number of the cursor for the given script - /// ParameterSetSignatures - public async Task FindParameterSetsInFileAsync( - ScriptFile file, - int lineNumber, - int columnNumber) + public async Task FindParameterSetsInFileAsync( + ScriptFile scriptFile, int line, int column) { - SymbolReference foundSymbol = - AstOperations.FindCommandAtPosition( - file.ScriptAst, - lineNumber, - columnNumber); + // This needs to get by whole extent, not just the name, as it completes e.g. + // `Get-Process -` (after the dash). + SymbolReference? symbol = scriptFile.References.TryGetSymbolContainingPosition(line, column); // If we are not possibly looking at a Function, we don't // need to continue because we won't be able to get the // CommandInfo object. - if (foundSymbol?.SymbolType is not SymbolType.Function + if (symbol?.Type is not SymbolType.Function and not SymbolType.Unknown) { return null; @@ -375,19 +247,20 @@ public async Task FindParameterSetsInFileAsync( CommandInfo commandInfo = await CommandHelpers.GetCommandInfoAsync( - foundSymbol.SymbolName, + symbol.Id, _runspaceContext.CurrentRunspace, _executionService).ConfigureAwait(false); - if (commandInfo == null) + if (commandInfo is null) { return null; } try { + // TODO: We should probably look at 'Parameters' instead of 'ParameterSets' IEnumerable commandParamSets = commandInfo.ParameterSets; - return new ParameterSetSignatures(commandParamSets, foundSymbol); + return new ParameterSetSignatures(commandParamSets, symbol); } catch (RuntimeException e) { @@ -408,123 +281,40 @@ await CommandHelpers.GetCommandInfoAsync( } /// - /// Finds the definition of a symbol in the script file or any of the - /// files that it references. + /// Finds the possible definitions of the symbol in the file or workspace. + /// TODO: One day use IAsyncEnumerable. + /// TODO: Fix searching for definition of built-in commands. + /// TODO: Fix "definition" of dot-source (maybe?) /// - /// The initial script file to be searched for the symbol's definition. - /// The symbol for which a definition will be found. - /// The resulting GetDefinitionResult for the symbol's definition. - public async Task GetDefinitionOfSymbolAsync( - ScriptFile sourceFile, - SymbolReference foundSymbol) + public async Task> GetDefinitionOfSymbolAsync( + ScriptFile scriptFile, + SymbolReference symbol, + CancellationToken cancellationToken = default) { - Validate.IsNotNull(nameof(sourceFile), sourceFile); - Validate.IsNotNull(nameof(foundSymbol), foundSymbol); - - // If symbol is an alias, resolve it. - (Dictionary> _, Dictionary aliasToCmdlets) = - await CommandHelpers.GetAliasesAsync(_executionService).ConfigureAwait(false); - - if (aliasToCmdlets.ContainsKey(foundSymbol.SymbolName)) + List declarations = new(); + declarations.AddRange(scriptFile.References.TryGetReferences(symbol).Where(i => i.IsDeclaration)); + if (declarations.Count > 0) { - foundSymbol = new SymbolReference( - foundSymbol.SymbolType, - aliasToCmdlets[foundSymbol.SymbolName], - foundSymbol.ScriptRegion, - foundSymbol.FilePath, - foundSymbol.SourceLine); + _logger.LogDebug($"Found possible declaration in same file ${declarations}"); + return declarations; } - ScriptFile[] referencedFiles = - _workspaceService.ExpandScriptReferences( - sourceFile); - - HashSet filesSearched = new(StringComparer.OrdinalIgnoreCase); - - // look through the referenced files until definition is found - // or there are no more file to look through - SymbolReference foundDefinition = null; - foreach (ScriptFile scriptFile in referencedFiles) - { - foundDefinition = AstOperations.FindDefinitionOfSymbol(scriptFile.ScriptAst, foundSymbol); + IEnumerable references = + await ScanForReferencesOfSymbolAsync(symbol, cancellationToken).ConfigureAwait(false); + declarations.AddRange(references.Where(i => i.IsDeclaration)); - filesSearched.Add(scriptFile.FilePath); - if (foundDefinition is not null) - { - foundDefinition.FilePath = scriptFile.FilePath; - break; - } - - if (foundSymbol.SymbolType == SymbolType.Function) - { - // Dot-sourcing is parsed as a "Function" Symbol. - string dotSourcedPath = GetDotSourcedPath(foundSymbol, scriptFile); - if (scriptFile.FilePath == dotSourcedPath) - { - foundDefinition = new SymbolReference(SymbolType.Function, foundSymbol.SymbolName, scriptFile.ScriptAst.Extent, scriptFile.FilePath); - break; - } - } - } - - // if the definition the not found in referenced files - // look for it in all the files in the workspace - if (foundDefinition is null) - { - // Get a list of all powershell files in the workspace path - foreach (string file in _workspaceService.EnumeratePSFiles()) - { - if (filesSearched.Contains(file)) - { - continue; - } - - foundDefinition = - AstOperations.FindDefinitionOfSymbol( - Parser.ParseFile(file, out Token[] tokens, out ParseError[] parseErrors), - foundSymbol); - - filesSearched.Add(file); - if (foundDefinition is not null) - { - foundDefinition.FilePath = file; - break; - } - } - } - - // if the definition is not found in a file in the workspace - // look for it in the builtin commands but only if the symbol - // we are looking at is possibly a Function. - if (foundDefinition is null - && (foundSymbol.SymbolType == SymbolType.Function - || foundSymbol.SymbolType == SymbolType.Unknown)) - { - CommandInfo cmdInfo = - await CommandHelpers.GetCommandInfoAsync( - foundSymbol.SymbolName, - _runspaceContext.CurrentRunspace, - _executionService).ConfigureAwait(false); - - foundDefinition = - FindDeclarationForBuiltinCommand( - cmdInfo, - foundSymbol); - } - - return foundDefinition; + _logger.LogDebug($"Found possible declaration in workspace ${declarations}"); + return declarations; } - private Task _workspaceScanCompleted; - - private async Task ScanWorkspacePSFiles(CancellationToken cancellationToken = default) + internal async Task ScanWorkspacePSFiles(CancellationToken cancellationToken = default) { if (_configurationService.CurrentSettings.AnalyzeOpenDocumentsOnly) { return; } - Task scanTask = _workspaceScanCompleted; + Task? scanTask = _workspaceScanCompleted; // It's not impossible for two scans to start at once but it should be exceedingly // unlikely, and shouldn't break anything if it happens to. So we can save some // lock time by accepting that possibility. @@ -564,109 +354,22 @@ private async Task ScanWorkspacePSFiles(CancellationToken cancellationToken = de // TODO: There's a new API in net6 that lets you await a task with a cancellation token. // we should #if that in if feasible. TaskCompletionSource cancelled = new(); - cancellationToken.Register(() => cancelled.TrySetCanceled()); - await Task.WhenAny(scanTask, cancelled.Task).ConfigureAwait(false); - } - - /// - /// Gets a path from a dot-source symbol. - /// - /// The symbol representing the dot-source expression. - /// The script file containing the symbol - /// - private string GetDotSourcedPath(SymbolReference symbol, ScriptFile scriptFile) - { - string cleanedUpSymbol = PathUtils.NormalizePathSeparators(symbol.SymbolName.Trim('\'', '"')); - string psScriptRoot = Path.GetDirectoryName(scriptFile.FilePath); - return _workspaceService.ResolveRelativeScriptPath(psScriptRoot, - Regex.Replace(cleanedUpSymbol, @"\$PSScriptRoot|\${PSScriptRoot}", psScriptRoot, RegexOptions.IgnoreCase)); - } - - private SymbolReference FindDeclarationForBuiltinCommand( - CommandInfo commandInfo, - SymbolReference foundSymbol) - { - if (commandInfo == null) - { - return null; - } - - ScriptFile[] nestedModuleFiles = - GetBuiltinCommandScriptFiles( - commandInfo.Module); - - SymbolReference foundDefinition = null; - foreach (ScriptFile nestedModuleFile in nestedModuleFiles) - { - foundDefinition = AstOperations.FindDefinitionOfSymbol( - nestedModuleFile.ScriptAst, - foundSymbol); - - if (foundDefinition != null) - { - foundDefinition.FilePath = nestedModuleFile.FilePath; - break; - } - } - - return foundDefinition; - } - - private ScriptFile[] GetBuiltinCommandScriptFiles( - PSModuleInfo moduleInfo) - { - if (moduleInfo == null) - { - return Array.Empty(); - } - - string modPath = moduleInfo.Path; - List scriptFiles = new(); - 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", StringComparison.OrdinalIgnoreCase) || - modPath.EndsWith(".psm1", StringComparison.OrdinalIgnoreCase)) - { - newFile = _workspaceService.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", StringComparison.OrdinalIgnoreCase) || - nestedModPath.EndsWith(".psm1", StringComparison.OrdinalIgnoreCase)) - { - newFile = _workspaceService.GetFile(nestedModPath); - newFile.IsAnalysisEnabled = false; - scriptFiles.Add(newFile); - } - } - } - - return scriptFiles.ToArray(); + _ = cancellationToken.Register(() => cancelled.TrySetCanceled()); + _ = await Task.WhenAny(scanTask, cancelled.Task).ConfigureAwait(false); } /// /// Finds a function definition that follows or contains the given line number. /// - /// Open script file. - /// The 1 based line on which to look for function definition. - /// - /// If found, returns the function definition, otherwise, returns null. - public static FunctionDefinitionAst GetFunctionDefinitionForHelpComment( + public static FunctionDefinitionAst? GetFunctionDefinitionForHelpComment( ScriptFile scriptFile, - int lineNumber, - out string helpLocation) + int line, + out string? helpLocation) { + Validate.IsNotNull(nameof(scriptFile), scriptFile); // check if the next line contains a function definition - FunctionDefinitionAst funcDefnAst = GetFunctionDefinitionAtLine(scriptFile, lineNumber + 1); - if (funcDefnAst != null) + FunctionDefinitionAst? funcDefnAst = GetFunctionDefinitionAtLine(scriptFile, line + 1); + if (funcDefnAst is not null) { helpLocation = "before"; return funcDefnAst; @@ -681,8 +384,8 @@ public static FunctionDefinitionAst GetFunctionDefinitionForHelpComment( return false; } - return fdAst.Body.Extent.StartLineNumber < lineNumber && - fdAst.Body.Extent.EndLineNumber > lineNumber; + return fdAst.Body.Extent.StartLineNumber < line && + fdAst.Body.Extent.EndLineNumber > line; }, true); @@ -696,7 +399,7 @@ public static FunctionDefinitionAst GetFunctionDefinitionForHelpComment( // definition that contains `lineNumber` foreach (FunctionDefinitionAst foundAst in foundAsts.Cast()) { - if (funcDefnAst == null) + if (funcDefnAst is null) { funcDefnAst = foundAst; continue; @@ -709,14 +412,14 @@ public static FunctionDefinitionAst GetFunctionDefinitionForHelpComment( } } - // TODO use tokens to check for non empty character instead of just checking for line offset - if (funcDefnAst.Body.Extent.StartLineNumber == lineNumber - 1) + // TODO: use tokens to check for non empty character instead of just checking for line offset + if (funcDefnAst?.Body.Extent.StartLineNumber == line - 1) { helpLocation = "begin"; return funcDefnAst; } - if (funcDefnAst.Body.Extent.EndLineNumber == lineNumber + 1) + if (funcDefnAst?.Body.Extent.EndLineNumber == line + 1) { helpLocation = "end"; return funcDefnAst; @@ -729,11 +432,9 @@ public static FunctionDefinitionAst GetFunctionDefinitionForHelpComment( /// /// Gets the function defined on a given line. + /// TODO: Remove this. /// - /// Open script file. - /// The 1 based line on which to look for function definition. - /// If found, returns the function definition on the given line. Otherwise, returns null. - public static FunctionDefinitionAst GetFunctionDefinitionAtLine( + public static FunctionDefinitionAst? GetFunctionDefinitionAtLine( ScriptFile scriptFile, int lineNumber) { @@ -748,7 +449,7 @@ internal void OnConfigurationUpdated(object _, LanguageServerSettings e) { if (e.AnalyzeOpenDocumentsOnly) { - Task scanInProgress = _workspaceScanCompleted; + Task? scanInProgress = _workspaceScanCompleted; if (scanInProgress is not null) { // Wait until after the scan completes to close unopened files. diff --git a/src/PowerShellEditorServices/Services/Symbols/Vistors/AstOperations.cs b/src/PowerShellEditorServices/Services/Symbols/Visitors/AstOperations.cs similarity index 51% rename from src/PowerShellEditorServices/Services/Symbols/Vistors/AstOperations.cs rename to src/PowerShellEditorServices/Services/Symbols/Visitors/AstOperations.cs index e14b2051a..93b8fd924 100644 --- a/src/PowerShellEditorServices/Services/Symbols/Vistors/AstOperations.cs +++ b/src/PowerShellEditorServices/Services/Symbols/Visitors/AstOperations.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Linq.Expressions; using System.Management.Automation; using System.Management.Automation.Language; @@ -141,178 +140,6 @@ await executionService.ExecuteDelegateAsync( return commandCompletion; } - /// - /// Finds the symbol at a given file location - /// - /// The abstract syntax tree of the given script - /// The line number of the cursor for the given script - /// The column number of the cursor for the given script - /// Includes full function definition ranges in the search. - /// SymbolReference of found symbol - public static SymbolReference FindSymbolAtPosition( - Ast scriptAst, - int lineNumber, - int columnNumber, - bool includeFunctionDefinitions = false) - { - FindSymbolVisitor symbolVisitor = - new( - lineNumber, - columnNumber, - includeFunctionDefinitions); - - scriptAst.Visit(symbolVisitor); - - return symbolVisitor.FoundSymbolReference; - } - - /// - /// Finds the symbol (always Command type) at a given file location - /// - /// The abstract syntax tree of the given script - /// The line number of the cursor for the given script - /// The column number of the cursor for the given script - /// SymbolReference of found command - public static SymbolReference FindCommandAtPosition(Ast scriptAst, int lineNumber, int columnNumber) - { - FindCommandVisitor commandVisitor = new(lineNumber, columnNumber); - scriptAst.Visit(commandVisitor); - - return commandVisitor.FoundCommandReference; - } - - /// - /// 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 references of - /// Dictionary maping cmdlets to aliases for finding alias references - /// Dictionary maping aliases to cmdlets for finding alias references - /// - public static IEnumerable FindReferencesOfSymbol( - Ast scriptAst, - SymbolReference symbolReference, - IDictionary> cmdletToAliasDictionary = default, - IDictionary aliasToCmdletDictionary = default) - { - // find the symbol evaluators for the node types we are handling - FindReferencesVisitor referencesVisitor = new( - symbolReference, - cmdletToAliasDictionary, - aliasToCmdletDictionary); - - scriptAst.Visit(referencesVisitor); - - return referencesVisitor.FoundReferences; - } - - /// - /// Finds the definition of the symbol - /// - /// 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 - public static SymbolReference FindDefinitionOfSymbol( - Ast scriptAst, - SymbolReference symbolReference) - { - FindDeclarationVisitor declarationVisitor = new(symbolReference); - scriptAst.Visit(declarationVisitor); - return declarationVisitor.FoundDeclaration; - } - - /// - /// Finds all symbols in a script - /// - /// The abstract syntax tree of the given script - /// A collection of SymbolReference objects - public static IEnumerable FindSymbolsInDocument(Ast scriptAst) - { - // TODO: Restore this when we figure out how to support multiple - // PS versions in the new PSES-as-a-module world (issue #276) - // if (powerShellVersion >= new Version(5,0)) - // { - //#if PowerShell v5 - // FindSymbolsVisitor2 findSymbolsVisitor = new FindSymbolsVisitor2(); - // scriptAst.Visit(findSymbolsVisitor); - // symbolReferences = findSymbolsVisitor.SymbolReferences; - //#endif - // } - // else - - FindSymbolsVisitor findSymbolsVisitor = new(); - scriptAst.Visit(findSymbolsVisitor); - return findSymbolsVisitor.SymbolReferences; - } - - /// - /// Checks if a given ast represents the root node of a *.psd1 file. - /// - /// The abstract syntax tree of the given script - /// true if the AST represts a *.psd1 file, otherwise false - public static bool IsPowerShellDataFileAst(Ast ast) - { - // sometimes we don't have reliable access to the filename - // so we employ heuristics to check if the contents are - // part of a psd1 file. - return IsPowerShellDataFileAstNode( - new { Item = ast, Children = new List() }, - new Type[] { - typeof(ScriptBlockAst), - typeof(NamedBlockAst), - typeof(PipelineAst), - typeof(CommandExpressionAst), - typeof(HashtableAst) }, - 0); - } - - private static bool IsPowerShellDataFileAstNode(dynamic node, Type[] levelAstMap, int level) - { - dynamic levelAstTypeMatch = node.Item.GetType().Equals(levelAstMap[level]); - if (!levelAstTypeMatch) - { - return false; - } - - if (level == levelAstMap.Length - 1) - { - return levelAstTypeMatch; - } - - IEnumerable astsFound = (node.Item as Ast)?.FindAll(a => a is not null, false); - if (astsFound != null) - { - foreach (Ast astFound in astsFound) - { - if (!astFound.Equals(node.Item) - && node.Item.Equals(astFound.Parent) - && IsPowerShellDataFileAstNode( - new { Item = astFound, Children = new List() }, - levelAstMap, - level + 1)) - { - return true; - } - } - } - - return false; - } - - /// - /// Finds all files dot sourced in a script - /// - /// The abstract syntax tree of the given script - /// Pre-calculated value of $PSScriptRoot - /// - public static string[] FindDotSourcedIncludes(Ast scriptAst, string psScriptRoot) - { - FindDotSourcedVisitor dotSourcedVisitor = new(psScriptRoot); - scriptAst.Visit(dotSourcedVisitor); - - return dotSourcedVisitor.DotSourcedFiles.ToArray(); - } - internal static bool TryGetInferredValue(ExpandableStringExpressionAst expandableStringExpressionAst, out string value) { // Currently we only support inferring the value of `$PSScriptRoot`. We could potentially diff --git a/src/PowerShellEditorServices/Services/Symbols/Visitors/HashTableVisitor.cs b/src/PowerShellEditorServices/Services/Symbols/Visitors/HashTableVisitor.cs new file mode 100644 index 000000000..3d9100cf5 --- /dev/null +++ b/src/PowerShellEditorServices/Services/Symbols/Visitors/HashTableVisitor.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using System.Collections.Generic; +using System.Management.Automation.Language; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; + +namespace Microsoft.PowerShell.EditorServices.Services.Symbols +{ + /// + /// Visitor to find all the keys in Hashtable AST + /// + internal class FindHashtableSymbolsVisitor : AstVisitor + { + private readonly ScriptFile _file; + + /// + /// List of symbols (keys) found in the hashtable + /// + public List SymbolReferences { get; } + + /// + /// Initializes a new instance of FindHashtableSymbolsVisitor class + /// + public FindHashtableSymbolsVisitor(ScriptFile file) + { + SymbolReferences = new List(); + _file = file; + } + + /// + /// Adds keys in the input hashtable to the symbol reference + /// + /// A HashtableAst in the script's AST + /// A visit action that continues the search for references + public override AstVisitAction VisitHashtable(HashtableAst hashtableAst) + { + if (hashtableAst.KeyValuePairs == null) + { + return AstVisitAction.Continue; + } + + foreach (System.Tuple kvp in hashtableAst.KeyValuePairs) + { + if (kvp.Item1 is StringConstantExpressionAst keyStrConstExprAst) + { + IScriptExtent nameExtent = new ScriptExtent() + { + Text = keyStrConstExprAst.Value, + StartLineNumber = kvp.Item1.Extent.StartLineNumber, + EndLineNumber = kvp.Item2.Extent.EndLineNumber, + StartColumnNumber = kvp.Item1.Extent.StartColumnNumber, + EndColumnNumber = kvp.Item2.Extent.EndColumnNumber, + File = hashtableAst.Extent.File + }; + + SymbolReferences.Add( + // TODO: Should we fill this out better? + new SymbolReference( + SymbolType.HashtableKey, + nameExtent.Text, + nameExtent.Text, + nameExtent, + nameExtent, + _file, + isDeclaration: false)); + } + } + + return AstVisitAction.Continue; + } + } +} diff --git a/src/PowerShellEditorServices/Services/Symbols/Visitors/SymbolVisitor.cs b/src/PowerShellEditorServices/Services/Symbols/Visitors/SymbolVisitor.cs new file mode 100644 index 000000000..1035f2557 --- /dev/null +++ b/src/PowerShellEditorServices/Services/Symbols/Visitors/SymbolVisitor.cs @@ -0,0 +1,245 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Management.Automation.Language; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Utility; + +namespace Microsoft.PowerShell.EditorServices.Services.Symbols; + +/// +/// The goal of this is to be our one and only visitor, which parses a file when necessary +/// performing Action, which takes a SymbolReference (that this visitor creates) and returns an +/// AstVisitAction. In this way, all our symbols are created with the same initialization logic. +/// TODO: Visit hashtable keys, constants, arrays, namespaces, interfaces, operators, etc. +/// +internal sealed class SymbolVisitor : AstVisitor2 +{ + private readonly ScriptFile _file; + + private readonly Func _action; + + public SymbolVisitor(ScriptFile file, Func action) + { + _file = file; + _action = action; + } + + // TODO: Make all the display strings better (and performant). + public override AstVisitAction VisitCommand(CommandAst commandAst) + { + string? commandName = VisitorUtils.GetCommandName(commandAst); + if (commandName is null) + { + return AstVisitAction.Continue; + } + + return _action(new SymbolReference( + SymbolType.Function, + "fn " + CommandHelpers.StripModuleQualification(commandName, out _), + commandName, + commandAst.CommandElements[0].Extent, + commandAst.Extent, + _file, + isDeclaration: false)); + } + + public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst) + { + SymbolType symbolType = functionDefinitionAst.IsWorkflow + ? SymbolType.Workflow + : SymbolType.Function; + + // Extent for constructors and method trigger both this and VisitFunctionMember(). Covered in the latter. + // This will not exclude nested functions as they have ScriptBlockAst as parent + if (functionDefinitionAst.Parent is FunctionMemberAst) + { + return AstVisitAction.Continue; + } + + IScriptExtent nameExtent = VisitorUtils.GetNameExtent(functionDefinitionAst); + return _action(new SymbolReference( + symbolType, + "fn " + functionDefinitionAst.Name, + VisitorUtils.GetFunctionDisplayName(functionDefinitionAst), + nameExtent, + functionDefinitionAst.Extent, + _file, + isDeclaration: true)); + } + + public override AstVisitAction VisitParameter(ParameterAst parameterAst) + { + // TODO: Can we fix the display name's type by visiting this in VisitVariableExpression and + // getting the TypeConstraintAst somehow? + return _action(new SymbolReference( + SymbolType.Parameter, + "var " + parameterAst.Name.VariablePath.UserPath, + VisitorUtils.GetParamDisplayName(parameterAst), + parameterAst.Name.Extent, + parameterAst.Extent, + _file, + isDeclaration: true)); + } + + public override AstVisitAction VisitVariableExpression(VariableExpressionAst variableExpressionAst) + { + // Parameters are visited earlier, and we don't want to skip their children because we do + // want to visit their type constraints. + if (variableExpressionAst.Parent is ParameterAst) + { + return AstVisitAction.Continue; + } + + // TODO: Consider tracking unscoped variable references only when they're declared within + // the same function definition. + return _action(new SymbolReference( + SymbolType.Variable, + "var " + VisitorUtils.GetUnqualifiedVariableName(variableExpressionAst.VariablePath), + "$" + variableExpressionAst.VariablePath.UserPath, + variableExpressionAst.Extent, + variableExpressionAst.Extent, // TODO: Maybe parent? + _file, + isDeclaration: variableExpressionAst.Parent is AssignmentStatementAst or ParameterAst)); + } + + public override AstVisitAction VisitTypeDefinition(TypeDefinitionAst typeDefinitionAst) + { + SymbolType symbolType = typeDefinitionAst.IsEnum + ? SymbolType.Enum + : SymbolType.Class; + + IScriptExtent nameExtent = VisitorUtils.GetNameExtent(typeDefinitionAst); + return _action(new SymbolReference( + symbolType, + "type " + typeDefinitionAst.Name, + (symbolType is SymbolType.Enum ? "enum " : "class ") + typeDefinitionAst.Name + " { }", + nameExtent, + typeDefinitionAst.Extent, + _file, + isDeclaration: true)); + } + + public override AstVisitAction VisitTypeExpression(TypeExpressionAst typeExpressionAst) + { + return _action(new SymbolReference( + SymbolType.Type, + "type " + typeExpressionAst.TypeName.Name, + "(type) " + typeExpressionAst.TypeName.Name, + typeExpressionAst.Extent, + typeExpressionAst.Extent, + _file, + isDeclaration: false)); + } + + public override AstVisitAction VisitTypeConstraint(TypeConstraintAst typeConstraintAst) + { + return _action(new SymbolReference( + SymbolType.Type, + "type " + typeConstraintAst.TypeName.Name, + "(type) " + typeConstraintAst.TypeName.Name, + typeConstraintAst.Extent, + typeConstraintAst.Extent, + _file, + isDeclaration: false)); + } + + public override AstVisitAction VisitFunctionMember(FunctionMemberAst functionMemberAst) + { + SymbolType symbolType = functionMemberAst.IsConstructor + ? SymbolType.Constructor + : SymbolType.Method; + + IScriptExtent nameExtent = VisitorUtils.GetNameExtent(functionMemberAst); + + return _action(new SymbolReference( + symbolType, + "mtd " + functionMemberAst.Name, // We bucket all the overloads. + VisitorUtils.GetMemberOverloadName(functionMemberAst), + nameExtent, + functionMemberAst.Extent, + _file, + isDeclaration: true)); + } + + public override AstVisitAction VisitPropertyMember(PropertyMemberAst propertyMemberAst) + { + // Enum members and properties are the "same" according to PowerShell, so the symbol name's + // must be the same since we can't distinguish them in VisitMemberExpression. + SymbolType symbolType = + propertyMemberAst.Parent is TypeDefinitionAst typeAst && typeAst.IsEnum + ? SymbolType.EnumMember + : SymbolType.Property; + + return _action(new SymbolReference( + symbolType, + "prop " + propertyMemberAst.Name, + VisitorUtils.GetMemberOverloadName(propertyMemberAst), + VisitorUtils.GetNameExtent(propertyMemberAst), + propertyMemberAst.Extent, + _file, + isDeclaration: true)); + } + + public override AstVisitAction VisitMemberExpression(MemberExpressionAst memberExpressionAst) + { + string? memberName = memberExpressionAst.Member is StringConstantExpressionAst stringConstant ? stringConstant.Value : null; + if (string.IsNullOrEmpty(memberName)) + { + return AstVisitAction.Continue; + } + + // TODO: It's too bad we can't get the property's real symbol and reuse its display string. + return _action(new SymbolReference( + SymbolType.Property, + "prop " + memberName, + "(property) " + memberName, + memberExpressionAst.Member.Extent, + memberExpressionAst.Extent, + _file, + isDeclaration: false)); + } + + public override AstVisitAction VisitInvokeMemberExpression(InvokeMemberExpressionAst methodCallAst) + { + string? memberName = methodCallAst.Member is StringConstantExpressionAst stringConstant ? stringConstant.Value : null; + if (string.IsNullOrEmpty(memberName)) + { + return AstVisitAction.Continue; + } + + // TODO: It's too bad we can't get the member's real symbol and reuse its display string. + return _action(new SymbolReference( + SymbolType.Method, + "mtd " + memberName, + "(method) " + memberName, + methodCallAst.Member.Extent, + methodCallAst.Extent, + _file, + isDeclaration: false)); + } + + public override AstVisitAction VisitConfigurationDefinition(ConfigurationDefinitionAst configurationDefinitionAst) + { + string? name = configurationDefinitionAst.InstanceName is StringConstantExpressionAst stringConstant + ? stringConstant.Value : null; + if (string.IsNullOrEmpty(name)) + { + return AstVisitAction.Continue; + } + + IScriptExtent nameExtent = VisitorUtils.GetNameExtent(configurationDefinitionAst); + return _action(new SymbolReference( + SymbolType.Configuration, + "dsc " + name, + "configuration " + name + " { }", + nameExtent, + configurationDefinitionAst.Extent, + _file, + isDeclaration: true)); + } +} diff --git a/src/PowerShellEditorServices/Services/Symbols/Vistors/FindCommandVisitor.cs b/src/PowerShellEditorServices/Services/Symbols/Vistors/FindCommandVisitor.cs deleted file mode 100644 index 4a7b78faa..000000000 --- a/src/PowerShellEditorServices/Services/Symbols/Vistors/FindCommandVisitor.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Linq; -using System.Management.Automation.Language; - -namespace Microsoft.PowerShell.EditorServices.Services.Symbols -{ - /// - /// The vistior used to find the commandAst of a specific location in an AST - /// - internal class FindCommandVisitor : AstVisitor - { - private readonly int lineNumber; - private readonly int columnNumber; - - public SymbolReference FoundCommandReference { get; private set; } - - public FindCommandVisitor(int lineNumber, int columnNumber) - { - this.lineNumber = lineNumber; - this.columnNumber = columnNumber; - } - - public override AstVisitAction VisitPipeline(PipelineAst pipelineAst) - { - if (lineNumber == pipelineAst.Extent.StartLineNumber) - { - // Which command is the cursor in? - foreach (CommandAst commandAst in pipelineAst.PipelineElements.OfType()) - { - int trueEndColumnNumber = commandAst.Extent.EndColumnNumber; - string currentLine = commandAst.Extent.StartScriptPosition.Line; - - if (currentLine.Length >= trueEndColumnNumber) - { - // Get the text left in the line after the command's extent - string remainingLine = - currentLine.Substring( - commandAst.Extent.EndColumnNumber); - - // Calculate the "true" end column number by finding out how many - // whitespace characters are between this command and the next (or - // the end of the line). - // NOTE: +1 is added to trueEndColumnNumber to account for the position - // just after the last character in the command string or script line. - int preTrimLength = remainingLine.Length; - int postTrimLength = remainingLine.TrimStart().Length; - trueEndColumnNumber = - commandAst.Extent.EndColumnNumber + - (preTrimLength - postTrimLength) + 1; - } - - if (commandAst.Extent.StartColumnNumber <= columnNumber && - trueEndColumnNumber >= columnNumber) - { - FoundCommandReference = - new SymbolReference( - SymbolType.Function, - commandAst.CommandElements[0].Extent); - - return AstVisitAction.StopVisit; - } - } - } - - return base.VisitPipeline(pipelineAst); - } - } -} diff --git a/src/PowerShellEditorServices/Services/Symbols/Vistors/FindDeclarationVisitor.cs b/src/PowerShellEditorServices/Services/Symbols/Vistors/FindDeclarationVisitor.cs deleted file mode 100644 index 5c3071451..000000000 --- a/src/PowerShellEditorServices/Services/Symbols/Vistors/FindDeclarationVisitor.cs +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Management.Automation.Language; - -namespace Microsoft.PowerShell.EditorServices.Services.Symbols -{ - /// - /// The visitor used to find the definition of a symbol - /// - internal class FindDeclarationVisitor : AstVisitor - { - private readonly SymbolReference symbolRef; - private readonly string variableName; - - public SymbolReference FoundDeclaration { get; private set; } - - public FindDeclarationVisitor(SymbolReference symbolRef) - { - this.symbolRef = symbolRef; - if (this.symbolRef.SymbolType == SymbolType.Variable) - { - // converts `$varName` to `varName` or of the form ${varName} to varName - variableName = symbolRef.SymbolName.TrimStart('$').Trim('{', '}'); - } - } - - /// - /// Decides if the current function definition is the right definition - /// for the symbol being searched for. The definition of the symbol will be a of type - /// SymbolType.Function and have the same name as the symbol - /// - /// A FunctionDefinitionAst in the script's AST - /// A decision to stop searching if the right FunctionDefinitionAst was found, - /// 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, StringComparison.OrdinalIgnoreCase) + 1; - - IScriptExtent nameExtent = new ScriptExtent() - { - Text = functionDefinitionAst.Name, - StartLineNumber = functionDefinitionAst.Extent.StartLineNumber, - StartColumnNumber = startColumnNumber, - EndLineNumber = functionDefinitionAst.Extent.StartLineNumber, - EndColumnNumber = startColumnNumber + functionDefinitionAst.Name.Length, - File = functionDefinitionAst.Extent.File - }; - - // We compare to the SymbolName instead of its text because it may have been resolved - // from an alias. - if (symbolRef.SymbolType.Equals(SymbolType.Function) && - nameExtent.Text.Equals(symbolRef.SymbolName, StringComparison.CurrentCultureIgnoreCase)) - { - FoundDeclaration = - new SymbolReference( - SymbolType.Function, - nameExtent); - - return AstVisitAction.StopVisit; - } - - return base.VisitFunctionDefinition(functionDefinitionAst); - } - - /// - /// Check if the left hand side of an assignmentStatementAst is a VariableExpressionAst - /// with the same name as that of symbolRef. - /// - /// An AssignmentStatementAst - /// A decision to stop searching if the right VariableExpressionAst was found, - /// or a decision to continue if it wasn't found - public override AstVisitAction VisitAssignmentStatement(AssignmentStatementAst assignmentStatementAst) - { - if (variableName == null) - { - return AstVisitAction.Continue; - } - - // We want to check VariableExpressionAsts from within this AssignmentStatementAst so we visit it. - FindDeclarationVariableExpressionVisitor visitor = new(symbolRef); - assignmentStatementAst.Left.Visit(visitor); - - if (visitor.FoundDeclaration != null) - { - FoundDeclaration = visitor.FoundDeclaration; - return AstVisitAction.StopVisit; - } - return AstVisitAction.Continue; - } - - /// - /// The private visitor used to find the variable expression that matches a symbol - /// - private class FindDeclarationVariableExpressionVisitor : AstVisitor - { - private readonly SymbolReference symbolRef; - private readonly string variableName; - - public SymbolReference FoundDeclaration { get; private set; } - - public FindDeclarationVariableExpressionVisitor(SymbolReference symbolRef) - { - this.symbolRef = symbolRef; - if (this.symbolRef.SymbolType == SymbolType.Variable) - { - // converts `$varName` to `varName` or of the form ${varName} to varName - variableName = symbolRef.SymbolName.TrimStart('$').Trim('{', '}'); - } - } - - /// - /// Check if the VariableExpressionAst has the same name as that of symbolRef. - /// - /// A VariableExpressionAst - /// A decision to stop searching if the right VariableExpressionAst was found, - /// or a decision to continue if it wasn't found - public override AstVisitAction VisitVariableExpression(VariableExpressionAst variableExpressionAst) - { - if (variableExpressionAst.VariablePath.UserPath.Equals(variableName, StringComparison.OrdinalIgnoreCase)) - { - // TODO also find instances of set-variable - FoundDeclaration = new SymbolReference(SymbolType.Variable, variableExpressionAst.Extent); - return AstVisitAction.StopVisit; - } - return AstVisitAction.Continue; - } - - public override AstVisitAction VisitMemberExpression(MemberExpressionAst functionDefinitionAst) => - // We don't want to discover any variables in member expressisons (`$something.Foo`) - AstVisitAction.SkipChildren; - - public override AstVisitAction VisitIndexExpression(IndexExpressionAst functionDefinitionAst) => - // We don't want to discover any variables in index expressions (`$something[0]`) - AstVisitAction.SkipChildren; - } - } -} diff --git a/src/PowerShellEditorServices/Services/Symbols/Vistors/FindDotSourcedVisitor.cs b/src/PowerShellEditorServices/Services/Symbols/Vistors/FindDotSourcedVisitor.cs deleted file mode 100644 index 416a5d611..000000000 --- a/src/PowerShellEditorServices/Services/Symbols/Vistors/FindDotSourcedVisitor.cs +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Management.Automation.Language; -using Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices.Services.Symbols -{ - /// - /// The visitor used to find the dot-sourced files in an AST - /// - internal class FindDotSourcedVisitor : AstVisitor - { - private readonly string _psScriptRoot; - - /// - /// A hash set of the dot sourced files (because we don't want duplicates) - /// - public HashSet DotSourcedFiles { get; } - - /// - /// Creates a new instance of the FindDotSourcedVisitor class. - /// - /// Pre-calculated value of $PSScriptRoot - public FindDotSourcedVisitor(string psScriptRoot) - { - DotSourcedFiles = new HashSet(StringComparer.CurrentCultureIgnoreCase); - _psScriptRoot = psScriptRoot; - } - - /// - /// 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 decision 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) - { - CommandElementAst commandElementAst = commandAst.CommandElements[0]; - if (commandAst.InvocationOperator.Equals(TokenKind.Dot)) - { - string path = commandElementAst switch - { - StringConstantExpressionAst stringConstantExpressionAst => stringConstantExpressionAst.Value, - ExpandableStringExpressionAst expandableStringExpressionAst => GetPathFromExpandableStringExpression(expandableStringExpressionAst), - _ => null, - }; - if (!string.IsNullOrWhiteSpace(path)) - { - DotSourcedFiles.Add(PathUtils.NormalizePathSeparators(path)); - } - } - - return base.VisitCommand(commandAst); - } - - private string GetPathFromExpandableStringExpression(ExpandableStringExpressionAst expandableStringExpressionAst) - { - string path = expandableStringExpressionAst.Value; - foreach (ExpressionAst nestedExpression in expandableStringExpressionAst.NestedExpressions) - { - // If the string contains the variable $PSScriptRoot, we replace it with the corresponding value. - if (!(nestedExpression is VariableExpressionAst variableAst - && variableAst.VariablePath.UserPath.Equals("PSScriptRoot", StringComparison.OrdinalIgnoreCase))) - { - return null; // We return null instead of a partially evaluated ExpandableStringExpression. - } - - path = path.Replace(variableAst.ToString(), _psScriptRoot); - } - - return path; - } - } -} diff --git a/src/PowerShellEditorServices/Services/Symbols/Vistors/FindReferencesVisitor.cs b/src/PowerShellEditorServices/Services/Symbols/Vistors/FindReferencesVisitor.cs deleted file mode 100644 index 44b64c8f5..000000000 --- a/src/PowerShellEditorServices/Services/Symbols/Vistors/FindReferencesVisitor.cs +++ /dev/null @@ -1,172 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Management.Automation.Language; -using Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices.Services.Symbols -{ - /// - /// The visitor used to find the references of a symbol in a script's AST - /// - internal class FindReferencesVisitor : AstVisitor - { - private readonly SymbolReference _symbolRef; - private readonly IDictionary> _cmdletToAliasDictionary; - private readonly IDictionary _aliasToCmdletDictionary; - private readonly string _symbolRefCommandName; - private readonly bool _needsAliases; - - public List FoundReferences { get; set; } - - /// - /// 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, - IDictionary> cmdletToAliasDictionary = default, - IDictionary aliasToCmdletDictionary = default) - { - _symbolRef = symbolReference; - FoundReferences = new List(); - - if (cmdletToAliasDictionary is null || aliasToCmdletDictionary is null) - { - _needsAliases = false; - return; - } - - _needsAliases = true; - _cmdletToAliasDictionary = cmdletToAliasDictionary; - _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 an - // empty string. - aliasToCmdletDictionary.TryGetValue(symbolReference.ScriptRegion.Text, out _symbolRefCommandName); - - if (_symbolRefCommandName == null) - { - _symbolRefCommandName = string.Empty; - } - } - - /// - /// Decides if the current command is a reference of the symbol being searched for. - /// A reference of the symbol will be a of type SymbolType.Function - /// and have the same name as the symbol - /// - /// A CommandAst in the script's AST - /// A visit action that continues the search for references - public override AstVisitAction VisitCommand(CommandAst commandAst) - { - Ast commandNameAst = commandAst.CommandElements[0]; - string commandName = commandNameAst.Extent.Text; - - if (_symbolRef.SymbolType.Equals(SymbolType.Function)) - { - if (_needsAliases) - { - // Try to get the commandAst's name and aliases. - // - // If a command does not exist (if the symbol isn't an alias to a command) set - // command to an empty string value string command. - // - // If the aliases do not exist (if the symbol isn't a command that has aliases) - // set aliases to an empty List - _cmdletToAliasDictionary.TryGetValue(commandName, out List aliases); - _aliasToCmdletDictionary.TryGetValue(commandName, out string command); - if (aliases == null) { aliases = new List(); } - if (command == null) { command = string.Empty; } - - // 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.OrdinalIgnoreCase) - // Note that PowerShell command names and aliases are case insensitive. - || aliases.Exists((match) => string.Equals(match, _symbolRef.ScriptRegion.Text, StringComparison.OrdinalIgnoreCase)) - || command.Equals(_symbolRef.ScriptRegion.Text, StringComparison.OrdinalIgnoreCase) - || (!string.IsNullOrEmpty(command) - && command.Equals(_symbolRefCommandName, StringComparison.OrdinalIgnoreCase))) - { - FoundReferences.Add(new SymbolReference(SymbolType.Function, commandNameAst.Extent)); - } - } - else // search does not include aliases - { - if (commandName.Equals(_symbolRef.SymbolName, StringComparison.OrdinalIgnoreCase)) - { - FoundReferences.Add(new SymbolReference(SymbolType.Function, commandNameAst.Extent)); - } - } - } - - return base.VisitCommand(commandAst); - } - - /// - /// Decides if the current function definition is a reference of the symbol being searched for. - /// A reference of the symbol will be a of type SymbolType.Function and have the same name as the symbol - /// - /// A functionDefinitionAst in the script's AST - /// A visit action that continues the search for references - public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst) - { - (int startColumnNumber, int startLineNumber) = VisitorUtils.GetNameStartColumnAndLineNumbersFromAst(functionDefinitionAst); - - IScriptExtent nameExtent = new ScriptExtent() - { - Text = functionDefinitionAst.Name, - StartLineNumber = startLineNumber, - EndLineNumber = startLineNumber, - StartColumnNumber = startColumnNumber, - EndColumnNumber = startColumnNumber + functionDefinitionAst.Name.Length, - File = functionDefinitionAst.Extent.File - }; - - if (_symbolRef.SymbolType.Equals(SymbolType.Function) && - nameExtent.Text.Equals(_symbolRef.SymbolName, StringComparison.CurrentCultureIgnoreCase)) - { - FoundReferences.Add(new SymbolReference(SymbolType.Function, nameExtent)); - } - return base.VisitFunctionDefinition(functionDefinitionAst); - } - - /// - /// Decides if the current function definition is a reference of the symbol being searched for. - /// A reference of the symbol will be a of type SymbolType.Parameter and have the same name as the symbol - /// - /// A commandParameterAst in the script's AST - /// A visit action that continues the search for references - public override AstVisitAction VisitCommandParameter(CommandParameterAst commandParameterAst) - { - if (_symbolRef.SymbolType.Equals(SymbolType.Parameter) && - commandParameterAst.Extent.Text.Equals(_symbolRef.SymbolName, StringComparison.CurrentCultureIgnoreCase)) - { - FoundReferences.Add(new SymbolReference(SymbolType.Parameter, commandParameterAst.Extent)); - } - return AstVisitAction.Continue; - } - - /// - /// Decides if the current function definition is a reference of the symbol being searched for. - /// A reference of the symbol will be a of type SymbolType.Variable and have the same name as the symbol - /// - /// A variableExpressionAst in the script's AST - /// A visit action that continues the search for references - public override AstVisitAction VisitVariableExpression(VariableExpressionAst variableExpressionAst) - { - if (_symbolRef.SymbolType.Equals(SymbolType.Variable) - && variableExpressionAst.Extent.Text.Equals(_symbolRef.SymbolName, StringComparison.CurrentCultureIgnoreCase)) - { - FoundReferences.Add(new SymbolReference(SymbolType.Variable, variableExpressionAst.Extent)); - } - return AstVisitAction.Continue; - } - } -} diff --git a/src/PowerShellEditorServices/Services/Symbols/Vistors/FindSymbolVisitor.cs b/src/PowerShellEditorServices/Services/Symbols/Vistors/FindSymbolVisitor.cs deleted file mode 100644 index bf2520a3c..000000000 --- a/src/PowerShellEditorServices/Services/Symbols/Vistors/FindSymbolVisitor.cs +++ /dev/null @@ -1,151 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Management.Automation.Language; -using Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices.Services.Symbols -{ - /// - /// The visitor used to find the symbol at a specific location in the AST - /// - internal class FindSymbolVisitor : AstVisitor - { - private readonly int lineNumber; - private readonly int columnNumber; - private readonly bool includeFunctionDefinitions; - - public SymbolReference FoundSymbolReference { get; private set; } - - public FindSymbolVisitor( - int lineNumber, - int columnNumber, - bool includeFunctionDefinitions) - { - this.lineNumber = lineNumber; - this.columnNumber = columnNumber; - this.includeFunctionDefinitions = includeFunctionDefinitions; - } - - /// - /// Checks to see if this command ast is the symbol we are looking for. - /// - /// A CommandAst object in the script's AST - /// A decision to stop searching if the right symbol was found, - /// or a decision to continue if it wasn't found - public override AstVisitAction VisitCommand(CommandAst commandAst) - { - Ast commandNameAst = commandAst.CommandElements[0]; - - if (IsPositionInExtent(commandNameAst.Extent)) - { - FoundSymbolReference = - new SymbolReference( - SymbolType.Function, - commandNameAst.Extent); - - return AstVisitAction.StopVisit; - } - - return base.VisitCommand(commandAst); - } - - /// - /// Checks to see if this function definition is the symbol we are looking for. - /// - /// A functionDefinitionAst object in the script's AST - /// A decision to stop searching if the right symbol was found, - /// or a decision to continue if it wasn't found - public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst) - { - int startLineNumber = functionDefinitionAst.Extent.StartLineNumber; - int startColumnNumber = functionDefinitionAst.Extent.StartColumnNumber; - int endLineNumber = functionDefinitionAst.Extent.EndLineNumber; - int endColumnNumber = functionDefinitionAst.Extent.EndColumnNumber; - - if (!includeFunctionDefinitions) - { - // We only want the function name - (int startColumn, int startLine) = VisitorUtils.GetNameStartColumnAndLineNumbersFromAst(functionDefinitionAst); - startLineNumber = startLine; - startColumnNumber = startColumn; - endLineNumber = startLine; - endColumnNumber = startColumn + functionDefinitionAst.Name.Length; - } - - IScriptExtent nameExtent = new ScriptExtent() - { - Text = functionDefinitionAst.Name, - StartLineNumber = startLineNumber, - EndLineNumber = endLineNumber, - StartColumnNumber = startColumnNumber, - EndColumnNumber = endColumnNumber, - File = functionDefinitionAst.Extent.File - }; - - if (IsPositionInExtent(nameExtent)) - { - FoundSymbolReference = - new SymbolReference( - SymbolType.Function, - nameExtent); - - return AstVisitAction.StopVisit; - } - - return base.VisitFunctionDefinition(functionDefinitionAst); - } - - /// - /// Checks to see if this command parameter is the symbol we are looking for. - /// - /// A CommandParameterAst object in the script's AST - /// A decision to stop searching if the right symbol was found, - /// or a decision to continue if it wasn't found - public override AstVisitAction VisitCommandParameter(CommandParameterAst commandParameterAst) - { - if (IsPositionInExtent(commandParameterAst.Extent)) - { - FoundSymbolReference = - new SymbolReference( - SymbolType.Parameter, - commandParameterAst.Extent); - return AstVisitAction.StopVisit; - } - return AstVisitAction.Continue; - } - - /// - /// Checks to see if this variable expression is the symbol we are looking for. - /// - /// A VariableExpressionAst object in the script's AST - /// A decision to stop searching if the right symbol was found, - /// or a decision to continue if it wasn't found - public override AstVisitAction VisitVariableExpression(VariableExpressionAst variableExpressionAst) - { - if (IsPositionInExtent(variableExpressionAst.Extent)) - { - FoundSymbolReference = - new SymbolReference( - SymbolType.Variable, - variableExpressionAst.Extent); - - return AstVisitAction.StopVisit; - } - - return AstVisitAction.Continue; - } - - /// - /// Is the position of the given location is in the ast's extent - /// - /// The script extent of the element - /// True if the given position is in the range of the element's extent - private bool IsPositionInExtent(IScriptExtent extent) - { - return extent.StartLineNumber == lineNumber && - extent.StartColumnNumber <= columnNumber && - extent.EndColumnNumber >= columnNumber; - } - } -} diff --git a/src/PowerShellEditorServices/Services/Symbols/Vistors/FindSymbolsVisitor.cs b/src/PowerShellEditorServices/Services/Symbols/Vistors/FindSymbolsVisitor.cs deleted file mode 100644 index 970acb4e7..000000000 --- a/src/PowerShellEditorServices/Services/Symbols/Vistors/FindSymbolsVisitor.cs +++ /dev/null @@ -1,136 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Collections.Generic; -using System.Management.Automation.Language; - -namespace Microsoft.PowerShell.EditorServices.Services.Symbols -{ - /// - /// The visitor used to find all the symbols (function and class defs) in the AST. - /// - /// - /// Requires PowerShell v3 or higher - /// - internal class FindSymbolsVisitor : AstVisitor - { - public List SymbolReferences { get; } - - public FindSymbolsVisitor() => SymbolReferences = new List(); - - /// - /// Adds each function definition as a - /// - /// A functionDefinitionAst object in the script's AST - /// A decision to stop searching if the right symbol was found, - /// or a decision to continue if it wasn't found - public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst) - { - IScriptExtent nameExtent = new ScriptExtent() - { - Text = functionDefinitionAst.Name, - StartLineNumber = functionDefinitionAst.Extent.StartLineNumber, - EndLineNumber = functionDefinitionAst.Extent.EndLineNumber, - StartColumnNumber = functionDefinitionAst.Extent.StartColumnNumber, - EndColumnNumber = functionDefinitionAst.Extent.EndColumnNumber, - File = functionDefinitionAst.Extent.File - }; - - SymbolType symbolType = - functionDefinitionAst.IsWorkflow ? - SymbolType.Workflow : SymbolType.Function; - - SymbolReferences.Add( - new SymbolReference( - symbolType, - nameExtent)); - - return AstVisitAction.Continue; - } - - /// - /// Checks to see if this variable expression is the symbol we are looking for. - /// - /// A VariableExpressionAst object in the script's AST - /// A decision to stop searching if the right symbol was found, - /// or a decision to continue if it wasn't found - public override AstVisitAction VisitVariableExpression(VariableExpressionAst variableExpressionAst) - { - if (!IsAssignedAtScriptScope(variableExpressionAst)) - { - return AstVisitAction.Continue; - } - - SymbolReferences.Add( - new SymbolReference( - SymbolType.Variable, - variableExpressionAst.Extent)); - - return AstVisitAction.Continue; - } - - private static bool IsAssignedAtScriptScope(VariableExpressionAst variableExpressionAst) - { - Ast parent = variableExpressionAst.Parent; - if (parent is not AssignmentStatementAst) - { - return false; - } - - parent = parent.Parent; - return parent is null || parent.Parent is null || parent.Parent.Parent is null; - } - } - - /// - /// Visitor to find all the keys in Hashtable AST - /// - internal class FindHashtableSymbolsVisitor : AstVisitor - { - /// - /// List of symbols (keys) found in the hashtable - /// - public List SymbolReferences { get; } - - /// - /// Initializes a new instance of FindHashtableSymbolsVisitor class - /// - public FindHashtableSymbolsVisitor() => SymbolReferences = new List(); - - /// - /// Adds keys in the input hashtable to the symbol reference - /// - public override AstVisitAction VisitHashtable(HashtableAst hashtableAst) - { - if (hashtableAst.KeyValuePairs == null) - { - return AstVisitAction.Continue; - } - - foreach (System.Tuple kvp in hashtableAst.KeyValuePairs) - { - if (kvp.Item1 is StringConstantExpressionAst keyStrConstExprAst) - { - IScriptExtent nameExtent = new ScriptExtent() - { - Text = keyStrConstExprAst.Value, - StartLineNumber = kvp.Item1.Extent.StartLineNumber, - EndLineNumber = kvp.Item2.Extent.EndLineNumber, - StartColumnNumber = kvp.Item1.Extent.StartColumnNumber, - EndColumnNumber = kvp.Item2.Extent.EndColumnNumber, - File = hashtableAst.Extent.File - }; - - const SymbolType symbolType = SymbolType.HashtableKey; - - SymbolReferences.Add( - new SymbolReference( - symbolType, - nameExtent)); - } - } - - return AstVisitAction.Continue; - } - } -} diff --git a/src/PowerShellEditorServices/Services/Symbols/Vistors/FindSymbolsVisitor2.cs b/src/PowerShellEditorServices/Services/Symbols/Vistors/FindSymbolsVisitor2.cs deleted file mode 100644 index 15f5e49db..000000000 --- a/src/PowerShellEditorServices/Services/Symbols/Vistors/FindSymbolsVisitor2.cs +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.PowerShell.EditorServices.Services.Symbols -{ - // TODO: Restore this when we figure out how to support multiple - // PS versions in the new PSES-as-a-module world (issue #276) - - ///// - ///// The visitor used to find all the symbols (function and class defs) in the AST. - ///// - ///// - ///// Requires PowerShell v5 or higher - ///// - ///// - //internal class FindSymbolsVisitor2 : AstVisitor2 - //{ - // private FindSymbolsVisitor findSymbolsVisitor; - - // public List SymbolReferences - // { - // get - // { - // return this.findSymbolsVisitor.SymbolReferences; - // } - // } - - // public FindSymbolsVisitor2() - // { - // this.findSymbolsVisitor = new FindSymbolsVisitor(); - // } - - // /// - // /// Adds each function definition as a - // /// - // /// A functionDefinitionAst object in the script's AST - // /// A decision to stop searching if the right symbol was found, - // /// or a decision to continue if it wasn't found - // public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst) - // { - // return this.findSymbolsVisitor.VisitFunctionDefinition(functionDefinitionAst); - // } - - // /// - // /// Checks to see if this variable expression is the symbol we are looking for. - // /// - // /// A VariableExpressionAst object in the script's AST - // /// A decision to stop searching if the right symbol was found, - // /// or a decision to continue if it wasn't found - // public override AstVisitAction VisitVariableExpression(VariableExpressionAst variableExpressionAst) - // { - // return this.findSymbolsVisitor.VisitVariableExpression(variableExpressionAst); - // } - - // public override AstVisitAction VisitConfigurationDefinition(ConfigurationDefinitionAst configurationDefinitionAst) - // { - // IScriptExtent nameExtent = new ScriptExtent() - // { - // Text = configurationDefinitionAst.InstanceName.Extent.Text, - // StartLineNumber = configurationDefinitionAst.Extent.StartLineNumber, - // EndLineNumber = configurationDefinitionAst.Extent.EndLineNumber, - // StartColumnNumber = configurationDefinitionAst.Extent.StartColumnNumber, - // EndColumnNumber = configurationDefinitionAst.Extent.EndColumnNumber - // }; - - // this.findSymbolsVisitor.SymbolReferences.Add( - // new SymbolReference( - // SymbolType.Configuration, - // nameExtent)); - - // return AstVisitAction.Continue; - // } - //} -} diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/CodeActionHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/CodeActionHandler.cs index 2681baa19..895c804ec 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/CodeActionHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/CodeActionHandler.cs @@ -95,7 +95,7 @@ public override async Task Handle(CodeActionParams { Uri = request.TextDocument.Uri }, - Edits = new TextEditContainer(ScriptRegion.ToTextEdit(markerCorrection.Edit)) + Edits = new TextEditContainer(markerCorrection.Edit.ToTextEdit()) })) } }); diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/DefinitionHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/DefinitionHandler.cs index 8b9c2fc60..f39ca9917 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/DefinitionHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/DefinitionHandler.cs @@ -43,43 +43,38 @@ public override async Task Handle(DefinitionParams requ request.Position.Line + 1, request.Position.Character + 1); - List definitionLocations = new(); - if (foundSymbol != null) + if (foundSymbol is null) { - SymbolReference foundDefinition = await _symbolsService.GetDefinitionOfSymbolAsync( - scriptFile, - foundSymbol).ConfigureAwait(false); + return new LocationOrLocationLinks(); + } - if (foundDefinition != null) - { - definitionLocations.Add( + // Short-circuit if we're already on the definition. + if (foundSymbol.IsDeclaration) + { + return new LocationOrLocationLinks( + new LocationOrLocationLink[] { new LocationOrLocationLink( new Location { - Uri = DocumentUri.From(foundDefinition.FilePath), - Range = GetRangeFromScriptRegion(foundDefinition.ScriptRegion) - })); - } + Uri = DocumentUri.From(foundSymbol.FilePath), + Range = foundSymbol.NameRegion.ToRange() + })}); } - return new LocationOrLocationLinks(definitionLocations); - } - - private static Range GetRangeFromScriptRegion(ScriptRegion scriptRegion) - { - return new Range + List definitionLocations = new(); + foreach (SymbolReference foundDefinition in await _symbolsService.GetDefinitionOfSymbolAsync( + scriptFile, foundSymbol, cancellationToken).ConfigureAwait(false)) { - Start = new Position - { - Line = scriptRegion.StartLineNumber - 1, - Character = scriptRegion.StartColumnNumber - 1 - }, - End = new Position - { - Line = scriptRegion.EndLineNumber - 1, - Character = scriptRegion.EndColumnNumber - 1 - } - }; + definitionLocations.Add( + new LocationOrLocationLink( + new Location + { + Uri = DocumentUri.From(foundDefinition.FilePath), + Range = foundDefinition.NameRegion.ToRange() + })); + } + + return new LocationOrLocationLinks(definitionLocations); } } } diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/DocumentHighlightHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/DocumentHighlightHandler.cs index 8e318f3bc..5758e59a1 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/DocumentHighlightHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/DocumentHighlightHandler.cs @@ -40,25 +40,26 @@ public override Task Handle( { ScriptFile scriptFile = _workspaceService.GetFile(request.TextDocument.Uri); - IReadOnlyList symbolOccurrences = SymbolsService.FindOccurrencesInFile( + IEnumerable occurrences = SymbolsService.FindOccurrencesInFile( scriptFile, request.Position.Line + 1, request.Position.Character + 1); - if (symbolOccurrences is null) + if (occurrences is null) { return Task.FromResult(s_emptyHighlightContainer); } - DocumentHighlight[] highlights = new DocumentHighlight[symbolOccurrences.Count]; - for (int i = 0; i < symbolOccurrences.Count; i++) + List highlights = new(); + foreach (SymbolReference occurrence in occurrences) { - highlights[i] = new DocumentHighlight + highlights.Add(new DocumentHighlight { Kind = DocumentHighlightKind.Write, // TODO: Which symbol types are writable? - Range = symbolOccurrences[i].ScriptRegion.ToRange() - }; + Range = occurrence.NameRegion.ToRange() // Just the symbol name + }); } + _logger.LogDebug("Highlights: " + highlights); return Task.FromResult(new DocumentHighlightContainer(highlights)); diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/DocumentSymbolHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/DocumentSymbolHandler.cs index f0973a7f0..823d01f26 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/DocumentSymbolHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/DocumentSymbolHandler.cs @@ -44,44 +44,66 @@ public PsesDocumentSymbolHandler(ILoggerFactory factory, WorkspaceService worksp DocumentSelector = LspUtils.PowerShellDocumentSelector }; - public override Task Handle(DocumentSymbolParams request, CancellationToken cancellationToken) + // AKA the outline feature + public override async Task Handle(DocumentSymbolParams request, CancellationToken cancellationToken) { ScriptFile scriptFile = _workspaceService.GetFile(request.TextDocument.Uri); - IEnumerable foundSymbols = - ProvideDocumentSymbols(scriptFile); - - SymbolInformationOrDocumentSymbol[] symbols = null; + IEnumerable foundSymbols = ProvideDocumentSymbols(scriptFile); + if (foundSymbols is null) + { + return null; + } string containerName = Path.GetFileNameWithoutExtension(scriptFile.FilePath); - symbols = foundSymbols != null - ? foundSymbols - .Select(r => - { - return new SymbolInformationOrDocumentSymbol(new SymbolInformation - { - ContainerName = containerName, - Kind = GetSymbolKind(r.SymbolType), - Location = new Location - { - Uri = DocumentUri.From(r.FilePath), - Range = GetRangeFromScriptRegion(r.ScriptRegion) - }, - Name = GetDecoratedSymbolName(r) - }); - }) - .ToArray() - : Array.Empty(); - - return Task.FromResult(new SymbolInformationOrDocumentSymbolContainer(symbols)); + List symbols = new(); + foreach (SymbolReference r in foundSymbols) + { + // This async method is pretty dense with synchronous code + // so it's helpful to add some yields. + await Task.Yield(); + cancellationToken.ThrowIfCancellationRequested(); + + // Outline view should only include declarations. + // + // TODO: We should also include function invocations that are part of DSLs (like + // Invoke-Build etc.). + if (!r.IsDeclaration || r.Type is SymbolType.Parameter) + { + continue; + } + + // TODO: This should be a DocumentSymbol now as SymbolInformation is deprecated. But + // this requires figuring out how to populate `children`. Once we do that, the range + // can be NameRegion. + // + // symbols.Add(new SymbolInformationOrDocumentSymbol(new DocumentSymbol + // { + // Name = SymbolTypeUtils.GetDecoratedSymbolName(r), + // Kind = SymbolTypeUtils.GetSymbolKind(r.SymbolType), + // Range = r.ScriptRegion.ToRange(), + // SelectionRange = r.NameRegion.ToRange() + // })); + symbols.Add(new SymbolInformationOrDocumentSymbol(new SymbolInformation + { + ContainerName = containerName, + Kind = SymbolTypeUtils.GetSymbolKind(r.Type), + Location = new Location + { + Uri = DocumentUri.From(r.FilePath), + Range = new Range(r.NameRegion.ToRange().Start, r.ScriptRegion.ToRange().End) // Jump to name start, but keep whole range to support symbol tree in outline + }, + Name = r.Name + })); + } + + return new SymbolInformationOrDocumentSymbolContainer(symbols); } - private IEnumerable ProvideDocumentSymbols( - ScriptFile scriptFile) + private IEnumerable ProvideDocumentSymbols(ScriptFile scriptFile) { - return - InvokeProviders(p => p.ProvideDocumentSymbols(scriptFile)) + return InvokeProviders(p => p.ProvideDocumentSymbols(scriptFile)) .SelectMany(r => r); } @@ -123,45 +145,5 @@ protected IEnumerable InvokeProviders( return providerResults; } - - private static SymbolKind GetSymbolKind(SymbolType symbolType) - { - return symbolType switch - { - SymbolType.Configuration or SymbolType.Function or SymbolType.Workflow => SymbolKind.Function, - _ => SymbolKind.Variable, - }; - } - - private static string GetDecoratedSymbolName(ISymbolReference symbolReference) - { - string name = symbolReference.SymbolName; - - if (symbolReference.SymbolType is SymbolType.Configuration or - SymbolType.Function or - SymbolType.Workflow) - { - name += " { }"; - } - - return name; - } - - private static Range GetRangeFromScriptRegion(ScriptRegion scriptRegion) - { - return new Range - { - Start = new Position - { - Line = scriptRegion.StartLineNumber - 1, - Character = scriptRegion.StartColumnNumber - 1 - }, - End = new Position - { - Line = scriptRegion.EndLineNumber - 1, - Character = scriptRegion.EndColumnNumber - 1 - } - }; - } } } diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/HoverHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/HoverHandler.cs index cf90c6565..567da0159 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/HoverHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/HoverHandler.cs @@ -52,14 +52,14 @@ await _symbolsService.FindSymbolDetailsAtLocationAsync( request.Position.Line + 1, request.Position.Character + 1).ConfigureAwait(false); - if (symbolDetails == null) + if (symbolDetails is null) { return null; } List symbolInfo = new() { - new MarkedString("PowerShell", symbolDetails.DisplayString) + new MarkedString("PowerShell", symbolDetails.SymbolReference.Name) }; if (!string.IsNullOrEmpty(symbolDetails.Documentation)) @@ -67,29 +67,10 @@ await _symbolsService.FindSymbolDetailsAtLocationAsync( symbolInfo.Add(new MarkedString("markdown", symbolDetails.Documentation)); } - Range symbolRange = GetRangeFromScriptRegion(symbolDetails.SymbolReference.ScriptRegion); - return new Hover { Contents = new MarkedStringsOrMarkupContent(symbolInfo), - Range = symbolRange - }; - } - - private static Range GetRangeFromScriptRegion(ScriptRegion scriptRegion) - { - return new Range - { - Start = new Position - { - Line = scriptRegion.StartLineNumber - 1, - Character = scriptRegion.StartColumnNumber - 1 - }, - End = new Position - { - Line = scriptRegion.EndLineNumber - 1, - Character = scriptRegion.EndColumnNumber - 1 - } + Range = symbolDetails.SymbolReference.NameRegion.ToRange() }; } } diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/ReferencesHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/ReferencesHandler.cs index d76aa7c66..1d41f3571 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/ReferencesHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/ReferencesHandler.cs @@ -41,45 +41,24 @@ public override async Task Handle(ReferenceParams request, Ca request.Position.Line + 1, request.Position.Character + 1); - List referencesResult = - await _symbolsService.FindReferencesOfSymbol( - foundSymbol, - _workspaceService.ExpandScriptReferences(scriptFile), - _workspaceService, - cancellationToken).ConfigureAwait(false); - List locations = new(); - - if (referencesResult != null) + foreach (SymbolReference foundReference in await _symbolsService.ScanForReferencesOfSymbolAsync( + foundSymbol, cancellationToken).ConfigureAwait(false)) { - foreach (SymbolReference foundReference in referencesResult) + // Respect the request's setting to include declarations. + if (!request.Context.IncludeDeclaration && foundReference.IsDeclaration) { - locations.Add(new Location - { - Uri = DocumentUri.From(foundReference.FilePath), - Range = GetRangeFromScriptRegion(foundReference.ScriptRegion) - }); + continue; } + + locations.Add(new Location + { + Uri = DocumentUri.From(foundReference.FilePath), + Range = foundReference.NameRegion.ToRange() + }); } return new LocationContainer(locations); } - - private static Range GetRangeFromScriptRegion(ScriptRegion scriptRegion) - { - return new Range - { - Start = new Position - { - Line = scriptRegion.StartLineNumber - 1, - Character = scriptRegion.StartColumnNumber - 1 - }, - End = new Position - { - Line = scriptRegion.EndLineNumber - 1, - Character = scriptRegion.EndColumnNumber - 1 - } - }; - } } } diff --git a/src/PowerShellEditorServices/Services/TextDocument/Handlers/SignatureHelpHandler.cs b/src/PowerShellEditorServices/Services/TextDocument/Handlers/SignatureHelpHandler.cs index 3e294a1e2..240cef13d 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/Handlers/SignatureHelpHandler.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/Handlers/SignatureHelpHandler.cs @@ -34,7 +34,7 @@ public PsesSignatureHelpHandler( protected override SignatureHelpRegistrationOptions CreateRegistrationOptions(SignatureHelpCapability capability, ClientCapabilities clientCapabilities) => new() { DocumentSelector = LspUtils.PowerShellDocumentSelector, - // A sane default of " ". We may be able to include others like "-". + // TODO: We should evaluate what triggers (and re-triggers) signature help (like dash?) TriggerCharacters = new Container(" ") }; @@ -54,7 +54,7 @@ await _symbolsService.FindParameterSetsInFileAsync( request.Position.Line + 1, request.Position.Character + 1).ConfigureAwait(false); - if (parameterSets == null) + if (parameterSets is null) { return new SignatureHelp(); } @@ -89,7 +89,7 @@ private static ParameterInformation CreateParameterInfo(ParameterInfo parameterI return new ParameterInformation { Label = parameterInfo.Name, - Documentation = string.Empty + Documentation = parameterInfo.HelpMessage }; } } diff --git a/src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs b/src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs index c3e5ac303..3e52e4938 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs @@ -7,7 +7,6 @@ using System.Linq; using System.Management.Automation; using System.Management.Automation.Language; -using Microsoft.PowerShell.EditorServices.Services.Symbols; using Microsoft.PowerShell.EditorServices.Utility; using OmniSharp.Extensions.LanguageServer.Protocol; @@ -53,6 +52,7 @@ internal sealed class ScriptFile /// Gets or sets a boolean that determines whether /// semantic analysis should be enabled for this file. /// For internal use only. + /// TODO: Actually use and respect this property to avoid built-in files from being analyzed. /// internal bool IsAnalysisEnabled { get; set; } @@ -110,15 +110,6 @@ public Token[] ScriptTokens private set; } - /// - /// Gets the array of filepaths dot sourced in this ScriptFile - /// - public string[] ReferencedFiles - { - get; - private set; - } - internal ReferenceTable References { get; } internal bool IsOpen { get; set; } @@ -599,20 +590,6 @@ private void ParseFileContents() parseErrors .Select(ScriptFileMarker.FromParseError) .ToList(); - - // Untitled files have no directory - // Discussed in https://github.com/PowerShell/PowerShellEditorServices/pull/815. - // Rather than working hard to enable things for untitled files like a phantom directory, - // users should save the file. - if (IsInMemory) - { - // Need to initialize the ReferencedFiles property to an empty array. - ReferencedFiles = Array.Empty(); - return; - } - - // Get all dot sourced referenced files and store them - ReferencedFiles = AstOperations.FindDotSourcedIncludes(ScriptAst, Path.GetDirectoryName(FilePath)); } #endregion diff --git a/src/PowerShellEditorServices/Services/TextDocument/ScriptFileMarker.cs b/src/PowerShellEditorServices/Services/TextDocument/ScriptFileMarker.cs index 43c465701..e3273e3e6 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/ScriptFileMarker.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/ScriptFileMarker.cs @@ -34,19 +34,19 @@ public enum ScriptFileMarkerLevel ///          /// Information: This warning is trivial, but may be useful. They are recommended by PowerShell best practice.         ///  -        Information = 0, + Information = 0,         ///          /// WARNING: This warning may cause a problem or does not follow PowerShell's recommended guidelines.         ///  -        Warning = 1, + Warning = 1,         ///          /// ERROR: This warning is likely to cause a problem or does not follow PowerShell's required guidelines.         ///  -        Error = 2, + Error = 2,         ///          /// ERROR: This diagnostic is caused by an actual parsing error, and is generated only by the engine.         ///  -        ParseError = 3 + ParseError = 3 }; /// @@ -102,7 +102,7 @@ internal static ScriptFileMarker FromParseError( { Message = parseError.Message, Level = ScriptFileMarkerLevel.Error, - ScriptRegion = ScriptRegion.Create(parseError.Extent), + ScriptRegion = new(parseError.Extent), Source = "PowerShell" }; } @@ -157,7 +157,7 @@ internal static ScriptFileMarker FromDiagnosticRecord(PSObject psObject) Message = diagnosticRecord.Message as string ?? string.Empty, RuleName = diagnosticRecord.RuleName as string ?? string.Empty, Level = level, - ScriptRegion = ScriptRegion.Create(diagnosticRecord.Extent as IScriptExtent), + ScriptRegion = new(diagnosticRecord.Extent as IScriptExtent), Corrections = markerCorrections, Source = "PSScriptAnalyzer" }; diff --git a/src/PowerShellEditorServices/Services/TextDocument/ScriptRegion.cs b/src/PowerShellEditorServices/Services/TextDocument/ScriptRegion.cs index c4d407105..e818b6d63 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/ScriptRegion.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/ScriptRegion.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +#nullable enable + using System; using System.Management.Automation.Language; using OmniSharp.Extensions.LanguageServer.Protocol.Models; @@ -12,64 +14,42 @@ namespace Microsoft.PowerShell.EditorServices.Services.TextDocument /// public sealed class ScriptRegion : IScriptExtent { - #region Static Methods + internal TextEdit ToTextEdit() => new() { NewText = Text, Range = ToRange() }; - /// - /// Creates a new instance of the ScriptRegion class from an - /// instance of an IScriptExtent implementation. - /// - /// - /// The IScriptExtent to copy into the ScriptRegion. - /// - /// - /// A new ScriptRegion instance with the same details as the IScriptExtent. - /// - public static ScriptRegion Create(IScriptExtent scriptExtent) + internal Range ToRange() { - // IScriptExtent throws an ArgumentOutOfRange exception if Text is null - string scriptExtentText; - try + return new Range { - scriptExtentText = scriptExtent.Text; - } - catch (ArgumentOutOfRangeException) - { - scriptExtentText = string.Empty; - } + Start = new Position + { + Line = StartLineNumber - 1, + Character = StartColumnNumber - 1 + }, + End = new Position + { + Line = EndLineNumber - 1, + Character = EndColumnNumber - 1 + } + }; + } - return new ScriptRegion( - scriptExtent.File, - scriptExtentText, - scriptExtent.StartLineNumber, - scriptExtent.StartColumnNumber, - scriptExtent.StartOffset, - scriptExtent.EndLineNumber, - scriptExtent.EndColumnNumber, - scriptExtent.EndOffset); + // Same as PowerShell's EmptyScriptExtent + internal bool IsEmpty() + { + return StartLineNumber == 0 && StartColumnNumber == 0 + && EndLineNumber == 0 && EndColumnNumber == 0 + && string.IsNullOrEmpty(File) + && string.IsNullOrEmpty(Text); } - internal static TextEdit ToTextEdit(ScriptRegion scriptRegion) + // Do not use PowerShell's ContainsLineAndColumn, it's nonsense. + internal bool ContainsPosition(int line, int column) { - return new TextEdit - { - NewText = scriptRegion.Text, - Range = new Range - { - Start = new Position - { - Line = scriptRegion.StartLineNumber - 1, - Character = scriptRegion.StartColumnNumber - 1, - }, - End = new Position - { - Line = scriptRegion.EndLineNumber - 1, - Character = scriptRegion.EndColumnNumber - 1, - } - } - }; + return StartLineNumber <= line && line <= EndLineNumber + && StartColumnNumber <= column && column <= EndColumnNumber; } - #endregion + public override string ToString() => $"Start {StartLineNumber}:{StartColumnNumber}, End {EndLineNumber}:{EndColumnNumber}"; #region Constructors @@ -93,6 +73,33 @@ public ScriptRegion( EndOffset = endOffset; } + public ScriptRegion(IScriptExtent scriptExtent) + { + File = scriptExtent.File; + + // IScriptExtent throws an ArgumentOutOfRange exception if Text is null + try + { + Text = scriptExtent.Text; + } + catch (ArgumentOutOfRangeException) + { + Text = string.Empty; + } + + StartLineNumber = scriptExtent.StartLineNumber; + StartColumnNumber = scriptExtent.StartColumnNumber; + StartOffset = scriptExtent.StartOffset; + EndLineNumber = scriptExtent.EndLineNumber; + EndColumnNumber = scriptExtent.EndColumnNumber; + EndOffset = scriptExtent.EndOffset; + } + + /// + /// NOTE: While unused, we kept this as it was previously exposed on a public class. + /// + public static ScriptRegion Create(IScriptExtent scriptExtent) => new(scriptExtent); + #endregion #region Properties diff --git a/src/PowerShellEditorServices/Services/TextDocument/TokenOperations.cs b/src/PowerShellEditorServices/Services/TextDocument/TokenOperations.cs index 67853388a..d1a3ca5e5 100644 --- a/src/PowerShellEditorServices/Services/TextDocument/TokenOperations.cs +++ b/src/PowerShellEditorServices/Services/TextDocument/TokenOperations.cs @@ -25,8 +25,9 @@ internal static class TokenOperations /// /// Extracts all of the unique foldable regions in a script given the list tokens /// - internal static FoldingReferenceList FoldableReferences( - Token[] tokens) +#pragma warning disable CA1502 // Cyclomatic complexity we don't care about + internal static FoldingReferenceList FoldableReferences(Token[] tokens) +#pragma warning restore CA1502 { FoldingReferenceList refList = new(); diff --git a/src/PowerShellEditorServices/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs b/src/PowerShellEditorServices/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs index 8a9aaa815..7f9a43049 100644 --- a/src/PowerShellEditorServices/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs +++ b/src/PowerShellEditorServices/Services/Workspace/Handlers/WorkspaceSymbolsHandler.cs @@ -34,30 +34,40 @@ public PsesWorkspaceSymbolsHandler(ILoggerFactory loggerFactory, SymbolsService public override async Task> Handle(WorkspaceSymbolParams request, CancellationToken cancellationToken) { + await _symbolsService.ScanWorkspacePSFiles(cancellationToken).ConfigureAwait(false); List symbols = new(); foreach (ScriptFile scriptFile in _workspaceService.GetOpenedFiles()) { - List foundSymbols = - _symbolsService.FindSymbolsInFile( - scriptFile); + IEnumerable foundSymbols = _symbolsService.FindSymbolsInFile(scriptFile); // TODO: Need to compute a relative path that is based on common path for all workspace files string containerName = Path.GetFileNameWithoutExtension(scriptFile.FilePath); - foreach (SymbolReference foundOccurrence in foundSymbols) + foreach (SymbolReference symbol in foundSymbols) { // This async method is pretty dense with synchronous code // so it's helpful to add some yields. await Task.Yield(); cancellationToken.ThrowIfCancellationRequested(); - if (!IsQueryMatch(request.Query, foundOccurrence.SymbolName)) + + if (!symbol.IsDeclaration) + { + continue; + } + + if (symbol.Type is SymbolType.Parameter) + { + continue; + } + + if (!IsQueryMatch(request.Query, symbol.Name)) { continue; } // Exclude Pester setup/teardown symbols as they're unnamed - if (foundOccurrence is PesterSymbolReference pesterSymbol && + if (symbol is PesterSymbolReference pesterSymbol && !PesterSymbolReference.IsPesterTestCommand(pesterSymbol.Command)) { continue; @@ -65,16 +75,17 @@ public override async Task> Handle(WorkspaceSymbolP Location location = new() { - Uri = DocumentUri.From(foundOccurrence.FilePath), - Range = GetRangeFromScriptRegion(foundOccurrence.ScriptRegion) + Uri = DocumentUri.From(symbol.FilePath), + Range = symbol.NameRegion.ToRange() }; + // TODO: This should be a WorkplaceSymbol now as SymbolInformation is deprecated. symbols.Add(new SymbolInformation { ContainerName = containerName, - Kind = foundOccurrence.SymbolType == SymbolType.Variable ? SymbolKind.Variable : SymbolKind.Function, + Kind = SymbolTypeUtils.GetSymbolKind(symbol.Type), Location = location, - Name = GetDecoratedSymbolName(foundOccurrence) + Name = symbol.Name }); } } @@ -86,37 +97,6 @@ public override async Task> Handle(WorkspaceSymbolP private static bool IsQueryMatch(string query, string symbolName) => symbolName.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0; - private static Range GetRangeFromScriptRegion(ScriptRegion scriptRegion) - { - return new Range - { - Start = new Position - { - Line = scriptRegion.StartLineNumber - 1, - Character = scriptRegion.StartColumnNumber - 1 - }, - End = new Position - { - Line = scriptRegion.EndLineNumber - 1, - Character = scriptRegion.EndColumnNumber - 1 - } - }; - } - - private static string GetDecoratedSymbolName(SymbolReference symbolReference) - { - string name = symbolReference.SymbolName; - - if (symbolReference.SymbolType is SymbolType.Configuration or - SymbolType.Function or - SymbolType.Workflow) - { - name += " { }"; - } - - return name; - } - #endregion } } diff --git a/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs b/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs index 0d9a614e4..002a757ad 100644 --- a/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs +++ b/src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs @@ -5,7 +5,6 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Security; using System.Text; using Microsoft.Extensions.FileSystemGlobbing; @@ -275,10 +274,9 @@ public ScriptFile GetFileBuffer(DocumentUri documentUri, string initialBuffer) } /// - /// Gets an array of all opened ScriptFiles in the workspace. + /// Gets an IEnumerable of all opened ScriptFiles in the workspace. /// - /// An array of all opened ScriptFiles in the workspace. - public ScriptFile[] GetOpenedFiles() => workspaceFiles.Values.ToArray(); + public IEnumerable GetOpenedFiles() => workspaceFiles.Values; /// /// Closes a currently open script file with the given file path. @@ -292,35 +290,6 @@ public void CloseFile(ScriptFile scriptFile) workspaceFiles.TryRemove(keyName, out ScriptFile _); } - /// - /// 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(); - List expandedReferences = new(); - - // add original file so it's not searched for, then find all file references - referencedScriptFiles.Add(scriptFile.Id, scriptFile); - RecursivelyFindReferences(scriptFile, referencedScriptFiles); - - // remove original file from referenced 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.Id); - expandedReferences.Add(scriptFile); - - if (referencedScriptFiles.Count > 0) - { - expandedReferences.AddRange(referencedScriptFiles.Values); - } - - return expandedReferences.ToArray(); - } - /// /// Gets the workspace-relative path of the given file path. /// @@ -372,7 +341,7 @@ public IEnumerable EnumeratePSFiles( bool ignoreReparsePoints ) { - if (WorkspacePath == null || !Directory.Exists(WorkspacePath)) + if (WorkspacePath is null || !Directory.Exists(WorkspacePath)) { yield break; } @@ -401,54 +370,6 @@ bool ignoreReparsePoints #endregion #region Private Methods - /// - /// Recursively searches through referencedFiles in scriptFiles - /// and builds a Dictionary of the file references - /// - /// Details an contents of "root" script file - /// A Dictionary of referenced script files - private void RecursivelyFindReferences( - ScriptFile scriptFile, - Dictionary referencedScriptFiles) - { - // Get the base path of the current script for use in resolving relative paths - string baseFilePath = scriptFile.IsInMemory - ? WorkspacePath - : Path.GetDirectoryName(scriptFile.FilePath); - - foreach (string referencedFileName in scriptFile.ReferencedFiles) - { - string resolvedScriptPath = - ResolveRelativeScriptPath( - baseFilePath, - referencedFileName); - - // If there was an error resolving the string, skip this reference - if (resolvedScriptPath == null) - { - continue; - } - - logger.LogDebug( - string.Format( - "Resolved relative path '{0}' to '{1}'", - referencedFileName, - resolvedScriptPath)); - - // Get the referenced file if it's not already in referencedScriptFiles - if (TryGetFile(resolvedScriptPath, out ScriptFile referencedFile)) - { - // Normalize the resolved script path and add it to the - // referenced files list if it isn't there already - resolvedScriptPath = resolvedScriptPath.ToLower(); - if (!referencedScriptFiles.ContainsKey(resolvedScriptPath)) - { - referencedScriptFiles.Add(resolvedScriptPath, referencedFile); - RecursivelyFindReferences(referencedFile, referencedScriptFiles); - } - } - } - } internal static StreamReader OpenStreamReader(DocumentUri uri) { diff --git a/src/PowerShellEditorServices/Utility/Extensions.cs b/src/PowerShellEditorServices/Utility/Extensions.cs index c280f1b14..88a9fa4f3 100644 --- a/src/PowerShellEditorServices/Utility/Extensions.cs +++ b/src/PowerShellEditorServices/Utility/Extensions.cs @@ -2,9 +2,6 @@ // Licensed under the MIT License. using System; -using System.Linq; -using System.Collections.Generic; -using System.Management.Automation.Language; using System.Text; namespace Microsoft.PowerShell.EditorServices.Utility @@ -33,119 +30,6 @@ public static string SafeToString(this object obj) return str; } - /// - /// Get the maximum of the elements from the given enumerable. - /// - /// Type of object for which the enumerable is defined. - /// An enumerable object of type T - /// A comparer for ordering elements of type T. The comparer should handle null values. - /// An object of type T. If the enumerable is empty or has all null elements, then the method returns null. - public static T MaxElement(this IEnumerable elements, Func comparer) where T : class - { - if (elements == null) - { - throw new ArgumentNullException(nameof(elements)); - } - - if (comparer == null) - { - throw new ArgumentNullException(nameof(comparer)); - } - - if (!elements.Any()) - { - return null; - } - - T maxElement = elements.First(); - foreach (T element in elements.Skip(1)) - { - if (element != null && comparer(element, maxElement) > 0) - { - maxElement = element; - } - } - - return maxElement; - } - - /// - /// Get the minimum of the elements from the given enumerable. - /// - /// Type of object for which the enumerable is defined. - /// An enumerable object of type T - /// A comparer for ordering elements of type T. The comparer should handle null values. - /// An object of type T. If the enumerable is empty or has all null elements, then the method returns null. - public static T MinElement(this IEnumerable elements, Func comparer) where T : class => MaxElement(elements, (elementX, elementY) => -1 * comparer(elementX, elementY)); - - /// - /// Compare extents with respect to their widths. - /// - /// Width of an extent is defined as the difference between its EndOffset and StartOffest properties. - /// - /// Extent of type IScriptExtent. - /// Extent of type IScriptExtent. - /// 0 if extentX and extentY are equal in width. 1 if width of extent X is greater than that of extent Y. Otherwise, -1. - public static int ExtentWidthComparer(this IScriptExtent extentX, IScriptExtent extentY) - { - if (extentX == null && extentY == null) - { - return 0; - } - - if (extentX != null && extentY == null) - { - return 1; - } - - if (extentX == null) - { - return -1; - } - - int extentWidthX = extentX.EndOffset - extentX.StartOffset; - int extentWidthY = extentY.EndOffset - extentY.StartOffset; - if (extentWidthX > extentWidthY) - { - return 1; - } - else if (extentWidthX < extentWidthY) - { - return -1; - } - else - { - return 0; - } - } - - /// - /// Check if the given coordinates are wholly contained in the instance's extent. - /// - /// Extent of type IScriptExtent. - /// 1-based line number. - /// 1-based column number - /// True if the coordinates are wholly contained in the instance's extent, otherwise, false. - public static bool Contains(this IScriptExtent scriptExtent, int line, int column) - { - if (scriptExtent.StartLineNumber > line || scriptExtent.EndLineNumber < line) - { - return false; - } - - if (scriptExtent.StartLineNumber == line) - { - return scriptExtent.StartColumnNumber <= column; - } - - if (scriptExtent.EndLineNumber == line) - { - return scriptExtent.EndColumnNumber >= column; - } - - return true; - } - /// /// Same as but never CRLF. Use this when building /// formatting for clients that may not render CRLF correctly. diff --git a/src/PowerShellEditorServices/Utility/VisitorUtils.cs b/src/PowerShellEditorServices/Utility/VisitorUtils.cs index 861d04acc..fbfdd1413 100644 --- a/src/PowerShellEditorServices/Utility/VisitorUtils.cs +++ b/src/PowerShellEditorServices/Utility/VisitorUtils.cs @@ -1,7 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +#nullable enable + +using System; +using System.Collections.Generic; +using System.Management.Automation; using System.Management.Automation.Language; +using System.Text; +using PSESSymbols = Microsoft.PowerShell.EditorServices.Services.Symbols; namespace Microsoft.PowerShell.EditorServices.Utility { @@ -10,42 +17,347 @@ namespace Microsoft.PowerShell.EditorServices.Utility /// internal static class VisitorUtils { + internal static string? GetCommandName(CommandAst commandAst) + { + string commandName = commandAst.GetCommandName(); + if (!string.IsNullOrEmpty(commandName)) + { + return commandName; + } + + if (commandAst.CommandElements[0] is not ExpandableStringExpressionAst expandableStringExpressionAst) + { + return null; + } + + return PSESSymbols.AstOperations.TryGetInferredValue(expandableStringExpressionAst, out string value) ? value : null; + } + + // Strip the qualification, if there is any, so $var is a reference of $script:var etc. + internal static string GetUnqualifiedVariableName(VariablePath variablePath) + { + return variablePath.IsUnqualified + ? variablePath.UserPath + : variablePath.UserPath.Substring(variablePath.UserPath.IndexOf(':') + 1); + } + /// - /// Calculates the start line and column of the actual function name in a function definition AST. + /// Calculates the start line and column of the actual symbol name in a AST. /// - /// A FunctionDefinitionAst object in the script's AST - /// A tuple with start column and line for the function name - internal static (int startColumn, int startLine) GetNameStartColumnAndLineNumbersFromAst(FunctionDefinitionAst ast) + /// An Ast object in the script's AST + /// An int specifying start index of name in the AST's extent text + /// A tuple with start column and line of the symbol name + private static (int startColumn, int startLine) GetNameStartColumnAndLineFromAst(Ast ast, int nameStartIndex) { int startColumnNumber = ast.Extent.StartColumnNumber; int startLineNumber = ast.Extent.StartLineNumber; - int astOffset = ast.IsFilter ? "filter".Length : ast.IsWorkflow ? "workflow".Length : "function".Length; string astText = ast.Extent.Text; - // The line offset represents the offset on the line that we're on where as // astOffset is the offset on the entire text of the AST. - int lineOffset = astOffset; - for (; astOffset < astText.Length; astOffset++, lineOffset++) + for (int astOffset = 0; astOffset <= ast.Extent.Text.Length; astOffset++, startColumnNumber++) { if (astText[astOffset] == '\n') { // reset numbers since we are operating on a different line and increment the line number. startColumnNumber = 0; startLineNumber++; - lineOffset = 0; } else if (astText[astOffset] == '\r') { // Do nothing with carriage returns... we only look for line feeds since those // are used on every platform. } - else if (!char.IsWhiteSpace(astText[astOffset])) + else if (astOffset >= nameStartIndex && !char.IsWhiteSpace(astText[astOffset])) { // This is the start of the function name so we've found our start column and line number. break; } } - return (startColumnNumber + lineOffset, startLineNumber); + return (startColumnNumber, startLineNumber); + } + + /// + /// Calculates the start line and column of the actual function name in a function definition AST. + /// + /// A FunctionDefinitionAst object in the script's AST + /// A tuple with start column and line for the function name + internal static (int startColumn, int startLine) GetNameStartColumnAndLineFromAst(FunctionDefinitionAst functionDefinitionAst) + { + int startOffset = functionDefinitionAst.IsFilter ? "filter".Length : functionDefinitionAst.IsWorkflow ? "workflow".Length : "function".Length; + return GetNameStartColumnAndLineFromAst(functionDefinitionAst, startOffset); + } + + /// + /// Calculates the start line and column of the actual class/enum name in a type definition AST. + /// + /// A TypeDefinitionAst object in the script's AST + /// A tuple with start column and line for the type name + internal static (int startColumn, int startLine) GetNameStartColumnAndLineFromAst(TypeDefinitionAst typeDefinitionAst) + { + int startOffset = typeDefinitionAst.IsEnum ? "enum".Length : "class".Length; + return GetNameStartColumnAndLineFromAst(typeDefinitionAst, startOffset); + } + + /// + /// Calculates the start line and column of the actual method/constructor name in a function member AST. + /// + /// A FunctionMemberAst object in the script's AST + /// A tuple with start column and line for the method/constructor name + internal static (int startColumn, int startLine) GetNameStartColumnAndLineFromAst(FunctionMemberAst functionMemberAst) + { + // find name index to get offset even with attributes, static, hidden ++ + int nameStartIndex = functionMemberAst.Extent.Text.IndexOf( + functionMemberAst.Name + '(', StringComparison.OrdinalIgnoreCase); + return GetNameStartColumnAndLineFromAst(functionMemberAst, nameStartIndex); + } + + /// + /// Calculates the start line and column of the actual property name in a property member AST. + /// + /// A PropertyMemberAst object in the script's AST + /// A bool indicating this is a enum member + /// A tuple with start column and line for the property name + internal static (int startColumn, int startLine) GetNameStartColumnAndLineFromAst(PropertyMemberAst propertyMemberAst, bool isEnumMember) + { + // find name index to get offset even with attributes, static, hidden ++ + string searchString = isEnumMember + ? propertyMemberAst.Name : '$' + propertyMemberAst.Name; + int nameStartIndex = propertyMemberAst.Extent.Text.IndexOf( + searchString, StringComparison.OrdinalIgnoreCase); + return GetNameStartColumnAndLineFromAst(propertyMemberAst, nameStartIndex); + } + + /// + /// Calculates the start line and column of the actual configuration name in a configuration definition AST. + /// + /// A ConfigurationDefinitionAst object in the script's AST + /// A tuple with start column and line for the configuration name + internal static (int startColumn, int startLine) GetNameStartColumnAndLineFromAst(ConfigurationDefinitionAst configurationDefinitionAst) + { + const int startOffset = 13; // "configuration".Length + return GetNameStartColumnAndLineFromAst(configurationDefinitionAst, startOffset); + } + + /// + /// Gets a new ScriptExtent for a given Ast for the symbol name only (variable) + /// + /// A FunctionDefinitionAst in the script's AST + /// A ScriptExtent with for the symbol name only + internal static PSESSymbols.ScriptExtent GetNameExtent(FunctionDefinitionAst functionDefinitionAst) + { + (int startColumn, int startLine) = GetNameStartColumnAndLineFromAst(functionDefinitionAst); + + return new PSESSymbols.ScriptExtent() + { + Text = functionDefinitionAst.Name, + StartLineNumber = startLine, + EndLineNumber = startLine, + StartColumnNumber = startColumn, + EndColumnNumber = startColumn + functionDefinitionAst.Name.Length, + File = functionDefinitionAst.Extent.File + }; + } + + /// + /// Gets a new ScriptExtent for a given Ast for the symbol name only (variable) + /// + /// A TypeDefinitionAst in the script's AST + /// A ScriptExtent with for the symbol name only + internal static PSESSymbols.ScriptExtent GetNameExtent(TypeDefinitionAst typeDefinitionAst) + { + (int startColumn, int startLine) = GetNameStartColumnAndLineFromAst(typeDefinitionAst); + + return new PSESSymbols.ScriptExtent() + { + Text = typeDefinitionAst.Name, + StartLineNumber = startLine, + EndLineNumber = startLine, + StartColumnNumber = startColumn, + EndColumnNumber = startColumn + typeDefinitionAst.Name.Length, + File = typeDefinitionAst.Extent.File + }; + } + + /// + /// Gets a new ScriptExtent for a given Ast for the symbol name only (variable) + /// + /// A FunctionMemberAst in the script's AST + /// A ScriptExtent with for the symbol name only + internal static PSESSymbols.ScriptExtent GetNameExtent(FunctionMemberAst functionMemberAst) + { + (int startColumn, int startLine) = GetNameStartColumnAndLineFromAst(functionMemberAst); + + return new PSESSymbols.ScriptExtent() + { + Text = GetMemberOverloadName(functionMemberAst), + StartLineNumber = startLine, + EndLineNumber = startLine, + StartColumnNumber = startColumn, + EndColumnNumber = startColumn + functionMemberAst.Name.Length, + File = functionMemberAst.Extent.File + }; + } + + /// + /// Gets a new ScriptExtent for a given Ast for the property name only + /// + /// A PropertyMemberAst in the script's AST + /// A ScriptExtent with for the symbol name only + internal static PSESSymbols.ScriptExtent GetNameExtent(PropertyMemberAst propertyMemberAst) + { + bool isEnumMember = propertyMemberAst.Parent is TypeDefinitionAst typeDef && typeDef.IsEnum; + (int startColumn, int startLine) = GetNameStartColumnAndLineFromAst(propertyMemberAst, isEnumMember); + + // +1 when class property to as start includes $ + int endColumnNumber = isEnumMember ? + startColumn + propertyMemberAst.Name.Length : + startColumn + propertyMemberAst.Name.Length + 1; + + return new PSESSymbols.ScriptExtent() + { + Text = GetMemberOverloadName(propertyMemberAst), + StartLineNumber = startLine, + EndLineNumber = startLine, + StartColumnNumber = startColumn, + EndColumnNumber = endColumnNumber, + File = propertyMemberAst.Extent.File + }; + } + + /// + /// Gets a new ScriptExtent for a given Ast for the configuration instance name only + /// + /// A ConfigurationDefinitionAst in the script's AST + /// A ScriptExtent with for the symbol name only + internal static PSESSymbols.ScriptExtent GetNameExtent(ConfigurationDefinitionAst configurationDefinitionAst) + { + string configurationName = configurationDefinitionAst.InstanceName.Extent.Text; + (int startColumn, int startLine) = GetNameStartColumnAndLineFromAst(configurationDefinitionAst); + + return new PSESSymbols.ScriptExtent() + { + Text = configurationName, + StartLineNumber = startLine, + EndLineNumber = startLine, + StartColumnNumber = startColumn, + EndColumnNumber = startColumn + configurationName.Length, + File = configurationDefinitionAst.Extent.File + }; + } + + /// + /// Gets the function name with parameters and return type. + /// + internal static string GetFunctionDisplayName(FunctionDefinitionAst functionDefinitionAst) + { + StringBuilder sb = new(); + if (functionDefinitionAst.IsWorkflow) + { + sb.Append("workflow"); + } + else if (functionDefinitionAst.IsFilter) + { + sb.Append("filter"); + } + else + { + sb.Append("function"); + } + sb.Append(' ').Append(functionDefinitionAst.Name).Append(" ("); + // Add parameters + // TODO: Fix the parameters, this doesn't work for those specified in the body. + if (functionDefinitionAst.Parameters?.Count > 0) + { + List parameters = new(functionDefinitionAst.Parameters.Count); + foreach (ParameterAst param in functionDefinitionAst.Parameters) + { + parameters.Add(param.Extent.Text); + } + + sb.Append(string.Join(", ", parameters)); + } + sb.Append(')'); + + return sb.ToString(); + } + + /// + /// Gets the display name of a parameter with its default value. + /// + internal static string GetParamDisplayName(ParameterAst parameterAst) + { + StringBuilder sb = new(); + + sb.Append("(parameter) "); + if (parameterAst.StaticType is not null) + { + sb.Append('[').Append(parameterAst.StaticType).Append(']'); + } + sb.Append('$').Append(parameterAst.Name.VariablePath.UserPath); + string? constantValue = parameterAst.DefaultValue is ConstantExpressionAst constant + ? constant.Value.ToString() : null; + + if (!string.IsNullOrEmpty(constantValue)) + { + sb.Append(" = ").Append(constantValue); + } + + return sb.ToString(); + } + + /// + /// Gets the method or constructor name with parameters for current overload. + /// + /// A FunctionMemberAst object in the script's AST + /// Function member name with return type (optional) and parameters + internal static string GetMemberOverloadName(FunctionMemberAst functionMemberAst) + { + StringBuilder sb = new(); + + // Prepend return type and class. Used for symbol details (hover) + if (!functionMemberAst.IsConstructor) + { + sb.Append(functionMemberAst.ReturnType?.TypeName.Name ?? "void").Append(' '); + } + + sb.Append(functionMemberAst.Name); + + // Add parameters + sb.Append('('); + if (functionMemberAst.Parameters.Count > 0) + { + List parameters = new(functionMemberAst.Parameters.Count); + foreach (ParameterAst param in functionMemberAst.Parameters) + { + parameters.Add(param.Extent.Text); + } + + sb.Append(string.Join(", ", parameters)); + } + sb.Append(')'); + + return sb.ToString(); + } + + /// + /// Gets the property name with type and class/enum. + /// + /// A PropertyMemberAst object in the script's AST + /// Property name with type (optional) and class/enum + internal static string GetMemberOverloadName(PropertyMemberAst propertyMemberAst) + { + StringBuilder sb = new(); + + // Prepend return type and class. Used for symbol details (hover) + if (propertyMemberAst.Parent is TypeDefinitionAst typeAst && !typeAst.IsEnum) + { + sb.Append('[') + .Append(propertyMemberAst.PropertyType?.TypeName.Name ?? "object") + .Append("] $"); + } + + sb.Append(propertyMemberAst.Name); + return sb.ToString(); } } } diff --git a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs index 0ca4bb2e3..d990ebe09 100644 --- a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs +++ b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System; @@ -442,7 +442,7 @@ await PsesLanguageClient Range range = symInfoOrDocSym.SymbolInformation.Location.Range; Assert.Equal(1, range.Start.Line); - Assert.Equal(0, range.Start.Character); + Assert.Equal(9, range.Start.Character); Assert.Equal(3, range.End.Line); Assert.Equal(1, range.End.Character); }); @@ -841,7 +841,7 @@ public async Task NoMessageIfPesterCodeLensDisabled() } [Fact] - public async Task CanSendReferencesCodeLensRequestAsync() + public async Task CanSendFunctionReferencesCodeLensRequestAsync() { string filePath = NewTestFile(@" function CanSendReferencesCodeLensRequest { @@ -867,7 +867,7 @@ function CanSendReferencesCodeLensRequest { Range range = codeLens.Range; Assert.Equal(1, range.Start.Line); - Assert.Equal(0, range.Start.Character); + Assert.Equal(9, range.Start.Character); Assert.Equal(3, range.End.Line); Assert.Equal(1, range.End.Character); @@ -878,6 +878,110 @@ function CanSendReferencesCodeLensRequest { Assert.Equal("1 reference", codeLensResolveResult.Command.Title); } + [Fact] + public async Task CanSendClassReferencesCodeLensRequestAsync() + { + string filePath = NewTestFile(@" +param( + [MyBaseClass]$enumValue +) + +class MyBaseClass { + +} + +class ChildClass : MyBaseClass, System.IDisposable { + +} + +$o = [MyBaseClass]::new() +$o -is [MyBaseClass] +"); + + CodeLensContainer codeLenses = await PsesLanguageClient + .SendRequest( + "textDocument/codeLens", + new CodeLensParams + { + TextDocument = new TextDocumentIdentifier + { + Uri = new Uri(filePath) + } + }) + .Returning(CancellationToken.None).ConfigureAwait(true); + + Assert.Collection(codeLenses, + codeLens => + { + Range range = codeLens.Range; + Assert.Equal(5, range.Start.Line); + Assert.Equal(6, range.Start.Character); + Assert.Equal(7, range.End.Line); + Assert.Equal(1, range.End.Character); + }, + codeLens => + { + Range range = codeLens.Range; + Assert.Equal(9, range.Start.Line); + Assert.Equal(6, range.Start.Character); + Assert.Equal(11, range.End.Line); + Assert.Equal(1, range.End.Character); + } + ); + + CodeLens baseClassCodeLens = codeLenses.First(); + CodeLens codeLensResolveResult = await PsesLanguageClient + .SendRequest("codeLens/resolve", baseClassCodeLens) + .Returning(CancellationToken.None).ConfigureAwait(true); + + Assert.Equal("4 references", codeLensResolveResult.Command.Title); + } + + [Fact] + public async Task CanSendEnumReferencesCodeLensRequestAsync() + { + string filePath = NewTestFile(@" +param( + [MyEnum]$enumValue +) + +enum MyEnum { + First = 1 + Second + Third +} + +[MyEnum]::First +'First' -is [MyEnum] +"); + + CodeLensContainer codeLenses = await PsesLanguageClient + .SendRequest( + "textDocument/codeLens", + new CodeLensParams + { + TextDocument = new TextDocumentIdentifier + { + Uri = new Uri(filePath) + } + }) + .Returning(CancellationToken.None).ConfigureAwait(true); + + CodeLens codeLens = Assert.Single(codeLenses); + + Range range = codeLens.Range; + Assert.Equal(5, range.Start.Line); + Assert.Equal(5, range.Start.Character); + Assert.Equal(9, range.End.Line); + Assert.Equal(1, range.End.Character); + + CodeLens codeLensResolveResult = await PsesLanguageClient + .SendRequest("codeLens/resolve", codeLens) + .Returning(CancellationToken.None).ConfigureAwait(true); + + Assert.Equal("3 references", codeLensResolveResult.Command.Title); + } + [SkippableFact] public async Task CanSendCodeActionRequestAsync() { diff --git a/test/PowerShellEditorServices.Test.Shared/Definition/FindsFunctionDefinitionInDotSourceReference.cs b/test/PowerShellEditorServices.Test.Shared/Definition/FindsFunctionDefinitionInDotSourceReference.cs index b7456faeb..842dec9b8 100644 --- a/test/PowerShellEditorServices.Test.Shared/Definition/FindsFunctionDefinitionInDotSourceReference.cs +++ b/test/PowerShellEditorServices.Test.Shared/Definition/FindsFunctionDefinitionInDotSourceReference.cs @@ -5,7 +5,7 @@ namespace Microsoft.PowerShell.EditorServices.Test.Shared.Definition { - public static class FindsFunctionDefinitionInDotSourceReferenceData + public static class FindsFunctionDefinitionInWorkspaceData { public static readonly ScriptRegion SourceDetails = new( file: TestUtilities.NormalizePath("References/FileWithReferences.ps1"), diff --git a/test/PowerShellEditorServices.Test.Shared/Definition/FindsFunctionDefinitionInWorkspace.cs b/test/PowerShellEditorServices.Test.Shared/Definition/FindsFunctionDefinitionInWorkspace.cs deleted file mode 100644 index a88c143bf..000000000 --- a/test/PowerShellEditorServices.Test.Shared/Definition/FindsFunctionDefinitionInWorkspace.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.PowerShell.EditorServices.Services.TextDocument; - -namespace Microsoft.PowerShell.EditorServices.Test.Shared.Definition -{ - public static class FindsFunctionDefinitionInWorkspaceData - { - public static readonly ScriptRegion SourceDetails = new( - file: TestUtilities.NormalizePath("References/ReferenceFileD.ps1"), - text: string.Empty, - startLineNumber: 1, - startColumnNumber: 2, - startOffset: 0, - endLineNumber: 0, - endColumnNumber: 0, - endOffset: 0); - } -} diff --git a/test/PowerShellEditorServices.Test.Shared/Definition/FindsTypeSymbolsDefinition.cs b/test/PowerShellEditorServices.Test.Shared/Definition/FindsTypeSymbolsDefinition.cs new file mode 100644 index 000000000..a0b5ae61c --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Definition/FindsTypeSymbolsDefinition.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.PowerShell.EditorServices.Services.TextDocument; + +namespace Microsoft.PowerShell.EditorServices.Test.Shared.Definition +{ + public static class FindsTypeSymbolsDefinitionData + { + public static readonly ScriptRegion ClassSourceDetails = new( + file: TestUtilities.NormalizePath("References/TypeAndClassesFile.ps1"), + text: string.Empty, + startLineNumber: 8, + startColumnNumber: 14, + startOffset: 0, + endLineNumber: 0, + endColumnNumber: 0, + endOffset: 0); + + public static readonly ScriptRegion EnumSourceDetails = new( + file: TestUtilities.NormalizePath("References/TypeAndClassesFile.ps1"), + text: string.Empty, + startLineNumber: 39, + startColumnNumber: 10, + startOffset: 0, + endLineNumber: 0, + endColumnNumber: 0, + endOffset: 0); + + public static readonly ScriptRegion TypeExpressionSourceDetails = new( + file: TestUtilities.NormalizePath("References/TypeAndClassesFile.ps1"), + text: string.Empty, + startLineNumber: 45, + startColumnNumber: 5, + startOffset: 0, + endLineNumber: 0, + endColumnNumber: 0, + endOffset: 0); + + public static readonly ScriptRegion TypeConstraintSourceDetails = new( + file: TestUtilities.NormalizePath("References/TypeAndClassesFile.ps1"), + text: string.Empty, + startLineNumber: 25, + startColumnNumber: 24, + startOffset: 0, + endLineNumber: 0, + endColumnNumber: 0, + endOffset: 0); + + public static readonly ScriptRegion ConstructorSourceDetails = new( + file: TestUtilities.NormalizePath("References/TypeAndClassesFile.ps1"), + text: string.Empty, + startLineNumber: 9, + startColumnNumber: 14, + startOffset: 0, + endLineNumber: 0, + endColumnNumber: 0, + endOffset: 0); + + public static readonly ScriptRegion MethodSourceDetails = new( + file: TestUtilities.NormalizePath("References/TypeAndClassesFile.ps1"), + text: string.Empty, + startLineNumber: 19, + startColumnNumber: 25, + startOffset: 0, + endLineNumber: 0, + endColumnNumber: 0, + endOffset: 0); + + public static readonly ScriptRegion PropertySourceDetails = new( + file: TestUtilities.NormalizePath("References/TypeAndClassesFile.ps1"), + text: string.Empty, + startLineNumber: 15, + startColumnNumber: 32, + startOffset: 0, + endLineNumber: 0, + endColumnNumber: 0, + endOffset: 0); + + public static readonly ScriptRegion EnumMemberSourceDetails = new( + file: TestUtilities.NormalizePath("References/TypeAndClassesFile.ps1"), + text: string.Empty, + startLineNumber: 41, + startColumnNumber: 11, + startOffset: 0, + endLineNumber: 0, + endColumnNumber: 0, + endOffset: 0); + } +} diff --git a/test/PowerShellEditorServices.Test.Shared/Occurrences/FindsOccurrencesOnTypeSymbols.cs b/test/PowerShellEditorServices.Test.Shared/Occurrences/FindsOccurrencesOnTypeSymbols.cs new file mode 100644 index 000000000..675dbe455 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Occurrences/FindsOccurrencesOnTypeSymbols.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.PowerShell.EditorServices.Services.TextDocument; + +namespace Microsoft.PowerShell.EditorServices.Test.Shared.Occurrences +{ + public static class FindsOccurrencesOnTypeSymbolsData + { + public static readonly ScriptRegion ClassSourceDetails = new( + file: TestUtilities.NormalizePath("References/TypeAndClassesFile.ps1"), + text: string.Empty, + startLineNumber: 8, + startColumnNumber: 16, + startOffset: 0, + endLineNumber: 0, + endColumnNumber: 0, + endOffset: 0); + + public static readonly ScriptRegion EnumSourceDetails = new( + file: TestUtilities.NormalizePath("References/TypeAndClassesFile.ps1"), + text: string.Empty, + startLineNumber: 39, + startColumnNumber: 7, + startOffset: 0, + endLineNumber: 0, + endColumnNumber: 0, + endOffset: 0); + + public static readonly ScriptRegion TypeExpressionSourceDetails = new( + file: TestUtilities.NormalizePath("References/TypeAndClassesFile.ps1"), + text: string.Empty, + startLineNumber: 34, + startColumnNumber: 16, + startOffset: 0, + endLineNumber: 0, + endColumnNumber: 0, + endOffset: 0); + + public static readonly ScriptRegion TypeConstraintSourceDetails = new( + file: TestUtilities.NormalizePath("References/TypeAndClassesFile.ps1"), + text: string.Empty, + startLineNumber: 8, + startColumnNumber: 24, + startOffset: 0, + endLineNumber: 0, + endColumnNumber: 0, + endOffset: 0); + + public static readonly ScriptRegion ConstructorSourceDetails = new( + file: TestUtilities.NormalizePath("References/TypeAndClassesFile.ps1"), + text: string.Empty, + startLineNumber: 13, + startColumnNumber: 14, + startOffset: 0, + endLineNumber: 0, + endColumnNumber: 0, + endOffset: 0); + + public static readonly ScriptRegion MethodSourceDetails = new( + file: TestUtilities.NormalizePath("References/TypeAndClassesFile.ps1"), + text: string.Empty, + startLineNumber: 28, + startColumnNumber: 22, + startOffset: 0, + endLineNumber: 0, + endColumnNumber: 0, + endOffset: 0); + + public static readonly ScriptRegion PropertySourceDetails = new( + file: TestUtilities.NormalizePath("References/TypeAndClassesFile.ps1"), + text: string.Empty, + startLineNumber: 15, + startColumnNumber: 18, + startOffset: 0, + endLineNumber: 0, + endColumnNumber: 0, + endOffset: 0); + + public static readonly ScriptRegion EnumMemberSourceDetails = new( + file: TestUtilities.NormalizePath("References/TypeAndClassesFile.ps1"), + text: string.Empty, + startLineNumber: 45, + startColumnNumber: 16, + startOffset: 0, + endLineNumber: 0, + endColumnNumber: 0, + endOffset: 0); + } +} diff --git a/test/PowerShellEditorServices.Test.Shared/Definition/FindsDotSourcedFile.cs b/test/PowerShellEditorServices.Test.Shared/Occurrences/FindsOccurrencesOnVariable.cs similarity index 57% rename from test/PowerShellEditorServices.Test.Shared/Definition/FindsDotSourcedFile.cs rename to test/PowerShellEditorServices.Test.Shared/Occurrences/FindsOccurrencesOnVariable.cs index e69976d2c..c01db0591 100644 --- a/test/PowerShellEditorServices.Test.Shared/Definition/FindsDotSourcedFile.cs +++ b/test/PowerShellEditorServices.Test.Shared/Occurrences/FindsOccurrencesOnVariable.cs @@ -1,16 +1,16 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using Microsoft.PowerShell.EditorServices.Services.TextDocument; -namespace Microsoft.PowerShell.EditorServices.Test.Shared.Definition +namespace Microsoft.PowerShell.EditorServices.Test.Shared.Occurrences { - public static class FindsDotSourcedFileData + public static class FindsOccurrencesOnVariableData { public static readonly ScriptRegion SourceDetails = new( - file: TestUtilities.NormalizePath("References/DotSources.ps1"), + file: TestUtilities.NormalizePath("References/SimpleFile.ps1"), text: string.Empty, - startLineNumber: 1, + startLineNumber: 8, startColumnNumber: 3, startOffset: 0, endLineNumber: 0, diff --git a/test/PowerShellEditorServices.Test.Shared/References/DotSources.ps1 b/test/PowerShellEditorServices.Test.Shared/References/DotSources.ps1 deleted file mode 100644 index 685183ea6..000000000 --- a/test/PowerShellEditorServices.Test.Shared/References/DotSources.ps1 +++ /dev/null @@ -1,15 +0,0 @@ -. ./ReferenceFileE.ps1 -. "$PSScriptRoot/ReferenceFileE.ps1" -. "${PSScriptRoot}/ReferenceFileE.ps1" -. './ReferenceFileE.ps1' -. "./ReferenceFileE.ps1" -. .\ReferenceFileE.ps1 -. '.\ReferenceFileE.ps1' -. ".\ReferenceFileE.ps1" -. ReferenceFileE.ps1 -. 'ReferenceFileE.ps1' -. "ReferenceFileE.ps1" -. ./dir/../ReferenceFileE.ps1 -. ./invalidfile.ps1 -. "" -. $someVar diff --git a/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesOnFunctionMultiFileDotSource.cs b/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesOnFunctionMultiFileDotSource.cs deleted file mode 100644 index 6a231ae60..000000000 --- a/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesOnFunctionMultiFileDotSource.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.PowerShell.EditorServices.Services.TextDocument; - -namespace Microsoft.PowerShell.EditorServices.Test.Shared.References -{ - public static class FindsReferencesOnFunctionMultiFileDotSourceFileB - { - public static readonly ScriptRegion SourceDetails = new( - file: TestUtilities.NormalizePath("References/ReferenceFileB.ps1"), - text: string.Empty, - startLineNumber: 5, - startColumnNumber: 8, - startOffset: 0, - endLineNumber: 0, - endColumnNumber: 0, - endOffset: 0); - } - - public static class FindsReferencesOnFunctionMultiFileDotSourceFileC - { - public static readonly ScriptRegion SourceDetails = new( - file: TestUtilities.NormalizePath("References/ReferenceFileC.ps1"), - text: string.Empty, - startLineNumber: 4, - startColumnNumber: 10, - startOffset: 0, - endLineNumber: 0, - endColumnNumber: 0, - endOffset: 0); - } -} diff --git a/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesOnTypeSymbols.cs b/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesOnTypeSymbols.cs new file mode 100644 index 000000000..ecd892610 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesOnTypeSymbols.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.PowerShell.EditorServices.Services.TextDocument; + +namespace Microsoft.PowerShell.EditorServices.Test.Shared.References +{ + public static class FindsReferencesOnTypeSymbolsData + { + public static readonly ScriptRegion ClassSourceDetails = new( + file: TestUtilities.NormalizePath("References/TypeAndClassesFile.ps1"), + text: string.Empty, + startLineNumber: 8, + startColumnNumber: 12, + startOffset: 0, + endLineNumber: 0, + endColumnNumber: 0, + endOffset: 0); + + public static readonly ScriptRegion EnumSourceDetails = new( + file: TestUtilities.NormalizePath("References/TypeAndClassesFile.ps1"), + text: string.Empty, + startLineNumber: 39, + startColumnNumber: 8, + startOffset: 0, + endLineNumber: 0, + endColumnNumber: 0, + endOffset: 0); + + public static readonly ScriptRegion ConstructorSourceDetails = new( + file: TestUtilities.NormalizePath("References/TypeAndClassesFile.ps1"), + text: string.Empty, + startLineNumber: 9, + startColumnNumber: 8, + startOffset: 0, + endLineNumber: 0, + endColumnNumber: 0, + endOffset: 0); + + public static readonly ScriptRegion MethodSourceDetails = new( + file: TestUtilities.NormalizePath("References/TypeAndClassesFile.ps1"), + text: string.Empty, + startLineNumber: 36, + startColumnNumber: 16, + startOffset: 0, + endLineNumber: 0, + endColumnNumber: 0, + endOffset: 0); + + public static readonly ScriptRegion PropertySourceDetails = new( + file: TestUtilities.NormalizePath("References/TypeAndClassesFile.ps1"), + text: string.Empty, + startLineNumber: 35, + startColumnNumber: 12, + startOffset: 0, + endLineNumber: 0, + endColumnNumber: 0, + endOffset: 0); + + public static readonly ScriptRegion EnumMemberSourceDetails = new( + file: TestUtilities.NormalizePath("References/TypeAndClassesFile.ps1"), + text: string.Empty, + startLineNumber: 45, + startColumnNumber: 16, + startOffset: 0, + endLineNumber: 0, + endColumnNumber: 0, + endOffset: 0); + + public static readonly ScriptRegion TypeExpressionSourceDetails = new( + file: TestUtilities.NormalizePath("References/TypeAndClassesFile.ps1"), + text: string.Empty, + startLineNumber: 34, + startColumnNumber: 12, + startOffset: 0, + endLineNumber: 0, + endColumnNumber: 0, + endOffset: 0); + + public static readonly ScriptRegion TypeConstraintSourceDetails = new( + file: TestUtilities.NormalizePath("References/TypeAndClassesFile.ps1"), + text: string.Empty, + startLineNumber: 25, + startColumnNumber: 22, + startOffset: 0, + endLineNumber: 0, + endColumnNumber: 0, + endOffset: 0); + } +} diff --git a/test/PowerShellEditorServices.Test.Shared/References/FunctionReference.ps1 b/test/PowerShellEditorServices.Test.Shared/References/FunctionReference.ps1 new file mode 100644 index 000000000..279f262b7 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/References/FunctionReference.ps1 @@ -0,0 +1,24 @@ +function BasicFunction {} +BasicFunction + +function FunctionWithExtraSpace +{ + +} FunctionWithExtraSpace + +function + + + FunctionNameOnDifferentLine + + + + + + + {} + + + FunctionNameOnDifferentLine + + function IndentedFunction { } IndentedFunction diff --git a/test/PowerShellEditorServices.Test.Shared/References/ReferenceFileA.ps1 b/test/PowerShellEditorServices.Test.Shared/References/ReferenceFileA.ps1 deleted file mode 100644 index 31ef35600..000000000 --- a/test/PowerShellEditorServices.Test.Shared/References/ReferenceFileA.ps1 +++ /dev/null @@ -1,9 +0,0 @@ -. .\ReferenceFileA.ps1 -. ./ReferenceFileB.ps1 -. .\ReferenceFileC.ps1 - -function My-Function ($myInput) -{ - My-Function $myInput -} -Get-ChildItem diff --git a/test/PowerShellEditorServices.Test.Shared/References/ReferenceFileB.ps1 b/test/PowerShellEditorServices.Test.Shared/References/ReferenceFileB.ps1 deleted file mode 100644 index 980ed33da..000000000 --- a/test/PowerShellEditorServices.Test.Shared/References/ReferenceFileB.ps1 +++ /dev/null @@ -1,5 +0,0 @@ -. "$PSScriptRoot\ReferenceFileC.ps1" - -Get-ChildItem - -My-Function "testb" diff --git a/test/PowerShellEditorServices.Test.Shared/References/ReferenceFileC.ps1 b/test/PowerShellEditorServices.Test.Shared/References/ReferenceFileC.ps1 deleted file mode 100644 index 6e1ee3131..000000000 --- a/test/PowerShellEditorServices.Test.Shared/References/ReferenceFileC.ps1 +++ /dev/null @@ -1,4 +0,0 @@ -. ./ReferenceFileA.ps1 -Get-ChildItem - -My-Function "testc" diff --git a/test/PowerShellEditorServices.Test.Shared/References/ReferenceFileD.ps1 b/test/PowerShellEditorServices.Test.Shared/References/ReferenceFileD.ps1 deleted file mode 100644 index 39aad7d37..000000000 --- a/test/PowerShellEditorServices.Test.Shared/References/ReferenceFileD.ps1 +++ /dev/null @@ -1 +0,0 @@ -My-FunctionInFileE "this is my function" \ No newline at end of file diff --git a/test/PowerShellEditorServices.Test.Shared/References/ReferenceFileE.ps1 b/test/PowerShellEditorServices.Test.Shared/References/ReferenceFileE.ps1 deleted file mode 100644 index 2a3e9b2a9..000000000 --- a/test/PowerShellEditorServices.Test.Shared/References/ReferenceFileE.ps1 +++ /dev/null @@ -1,4 +0,0 @@ -function My-FunctionInFileE -{ - -} \ 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 e7a271447..b60389c63 100644 --- a/test/PowerShellEditorServices.Test.Shared/References/SimpleFile.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/References/SimpleFile.ps1 @@ -1,6 +1,6 @@ function My-Function ($myInput) { - My-Function $myInput + My-Function $myInput } $things = 4 diff --git a/test/PowerShellEditorServices.Test.Shared/References/TypeAndClassesFile.ps1 b/test/PowerShellEditorServices.Test.Shared/References/TypeAndClassesFile.ps1 new file mode 100644 index 000000000..ac2888f9f --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/References/TypeAndClassesFile.ps1 @@ -0,0 +1,46 @@ +Get-ChildItem ./file1.ps1 +$myScriptVar = 123 + +class BaseClass { + +} + +class SuperClass : BaseClass { + SuperClass([string]$name) { + + } + + SuperClass() { } + + [string]$SomePropWithDefault = 'this is a default value' + + [int]$SomeProp + + [string]MyClassMethod([string]$param1, $param2, [int]$param3) { + $this.SomePropWithDefault = 'something happend' + return 'finished' + } + + [string] + MyClassMethod([MyEnum]$param1) { + return 'hello world' + } + [string]MyClassMethod() { + return 'hello world' + } +} + +New-Object SuperClass +$o = [SuperClass]::new() +$o.SomeProp +$o.MyClassMethod() + + +enum MyEnum { + First + Second + Third +} + +[MyEnum]::First +'First' -is [MyEnum] diff --git a/test/PowerShellEditorServices.Test.Shared/Symbols/DSCFile.ps1 b/test/PowerShellEditorServices.Test.Shared/Symbols/DSCFile.ps1 new file mode 100644 index 000000000..defec6863 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Symbols/DSCFile.ps1 @@ -0,0 +1,4 @@ +# This file represents a script with a DSC configuration +configuration AConfiguration { + Node "TEST-PC" {} +} diff --git a/test/PowerShellEditorServices.Test.Shared/Symbols/FindSymbolsInDSCFile.cs b/test/PowerShellEditorServices.Test.Shared/Symbols/FindSymbolsInDSCFile.cs new file mode 100644 index 000000000..6e3d45ff2 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Symbols/FindSymbolsInDSCFile.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.PowerShell.EditorServices.Services.TextDocument; + +namespace Microsoft.PowerShell.EditorServices.Test.Shared.Symbols +{ + public static class FindSymbolsInDSCFile + { + public static readonly ScriptRegion SourceDetails = + new( + file: TestUtilities.NormalizePath("Symbols/DSCFile.ps1"), + text: string.Empty, + startLineNumber: 0, + startColumnNumber: 0, + startOffset: 0, + endLineNumber: 0, + endColumnNumber: 0, + endOffset: 0); + } +} diff --git a/test/PowerShellEditorServices.Test.Shared/Symbols/FindSymbolsInNewLineSymbolFile.cs b/test/PowerShellEditorServices.Test.Shared/Symbols/FindSymbolsInNewLineSymbolFile.cs new file mode 100644 index 000000000..0be43f8d1 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Symbols/FindSymbolsInNewLineSymbolFile.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.PowerShell.EditorServices.Services.TextDocument; + +namespace Microsoft.PowerShell.EditorServices.Test.Shared.Symbols +{ + public static class FindSymbolsInNewLineSymbolFile + { + public static readonly ScriptRegion SourceDetails = + new( + file: TestUtilities.NormalizePath("Symbols/NewLineSymbols.ps1"), + text: string.Empty, + startLineNumber: 0, + startColumnNumber: 0, + startOffset: 0, + endLineNumber: 0, + endColumnNumber: 0, + endOffset: 0); + } +} diff --git a/test/PowerShellEditorServices.Test.Shared/Symbols/MultipleSymbols.ps1 b/test/PowerShellEditorServices.Test.Shared/Symbols/MultipleSymbols.ps1 index f234fed03..db53a6c1a 100644 --- a/test/PowerShellEditorServices.Test.Shared/Symbols/MultipleSymbols.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Symbols/MultipleSymbols.ps1 @@ -22,10 +22,22 @@ function AnAdvancedFunction { workflow AWorkflow {} -Configuration AConfiguration { - Node "TEST-PC" {} +class AClass { + [string]$AProperty + + AClass([string]$AParameter) { + + } + + [void]AMethod([string]$param1, [int]$param2, $param3) { + + } +} + +enum AEnum { + AValue = 0 } AFunction 1..3 | AFilter -AnAdvancedFunction \ No newline at end of file +AnAdvancedFunction diff --git a/test/PowerShellEditorServices.Test.Shared/Symbols/NewLineSymbols.ps1 b/test/PowerShellEditorServices.Test.Shared/Symbols/NewLineSymbols.ps1 new file mode 100644 index 000000000..5ca44f02a --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/Symbols/NewLineSymbols.ps1 @@ -0,0 +1,28 @@ +function +returnTrue { + $true +} + +class +NewLineClass { + NewLineClass() { + + } + + static + hidden + [string] + $SomePropWithDefault = 'some value' + + static + hidden + [string] + MyClassMethod([MyNewLineEnum]$param1) { + return 'hello world $param1' + } +} + +enum +MyNewLineEnum { + First +} diff --git a/test/PowerShellEditorServices.Test/Language/SymbolsServiceTests.cs b/test/PowerShellEditorServices.Test/Language/SymbolsServiceTests.cs index e7938c287..53993a484 100644 --- a/test/PowerShellEditorServices.Test/Language/SymbolsServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Language/SymbolsServiceTests.cs @@ -3,9 +3,9 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Management.Automation; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging.Abstractions; @@ -22,6 +22,7 @@ using Microsoft.PowerShell.EditorServices.Test.Shared.References; using Microsoft.PowerShell.EditorServices.Test.Shared.SymbolDetails; using Microsoft.PowerShell.EditorServices.Test.Shared.Symbols; +using Microsoft.PowerShell.EditorServices.Utility; using Xunit; namespace PowerShellEditorServices.Test.Language @@ -32,11 +33,15 @@ public class SymbolsServiceTests : IDisposable private readonly PsesInternalHost psesHost; private readonly WorkspaceService workspace; private readonly SymbolsService symbolsService; + private static readonly bool s_isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); public SymbolsServiceTests() { psesHost = PsesHostFactory.Create(NullLoggerFactory.Instance); - workspace = new WorkspaceService(NullLoggerFactory.Instance); + workspace = new WorkspaceService(NullLoggerFactory.Instance) + { + WorkspacePath = TestUtilities.GetSharedPath("References") + }; symbolsService = new SymbolsService( NullLoggerFactory.Instance, psesHost, @@ -55,6 +60,19 @@ public void Dispose() GC.SuppressFinalize(this); } + private static void AssertIsRegion( + ScriptRegion region, + int startLineNumber, + int startColumnNumber, + int endLineNumber, + int endColumnNumber) + { + Assert.Equal(startLineNumber, region.StartLineNumber); + Assert.Equal(startColumnNumber, region.StartColumnNumber); + Assert.Equal(endLineNumber, region.EndLineNumber); + Assert.Equal(endColumnNumber, region.EndColumnNumber); + } + private ScriptFile GetScriptFile(ScriptRegion scriptRegion) => workspace.GetFile(TestUtilities.GetSharedPath(scriptRegion.File)); private Task GetParamSetSignatures(ScriptRegion scriptRegion) @@ -65,72 +83,94 @@ private Task GetParamSetSignatures(ScriptRegion scriptRe scriptRegion.StartColumnNumber); } - private Task GetDefinition(ScriptRegion scriptRegion) + private async Task> GetDefinitions(ScriptRegion scriptRegion) { ScriptFile scriptFile = GetScriptFile(scriptRegion); - SymbolReference symbolReference = SymbolsService.FindSymbolAtLocation( + // TODO: We should just use the name to find it. + SymbolReference symbol = SymbolsService.FindSymbolAtLocation( scriptFile, scriptRegion.StartLineNumber, scriptRegion.StartColumnNumber); - Assert.NotNull(symbolReference); + Assert.NotNull(symbol); - return symbolsService.GetDefinitionOfSymbolAsync(scriptFile, symbolReference); + IEnumerable symbols = + await symbolsService.GetDefinitionOfSymbolAsync(scriptFile, symbol).ConfigureAwait(true); + + return symbols.OrderBy((i) => i.ScriptRegion.ToRange().Start); } - private Task> GetReferences(ScriptRegion scriptRegion) + private async Task GetDefinition(ScriptRegion scriptRegion) + { + IEnumerable definitions = await GetDefinitions(scriptRegion).ConfigureAwait(true); + return definitions.FirstOrDefault(); + } + + private async Task> GetReferences(ScriptRegion scriptRegion) { ScriptFile scriptFile = GetScriptFile(scriptRegion); - SymbolReference symbolReference = SymbolsService.FindSymbolAtLocation( + SymbolReference symbol = SymbolsService.FindSymbolAtLocation( scriptFile, scriptRegion.StartLineNumber, scriptRegion.StartColumnNumber); - Assert.NotNull(symbolReference); + Assert.NotNull(symbol); + + IEnumerable symbols = + await symbolsService.ScanForReferencesOfSymbolAsync(symbol).ConfigureAwait(true); - return symbolsService.FindReferencesOfSymbol( - symbolReference, - workspace.ExpandScriptReferences(scriptFile), - workspace); + return symbols.OrderBy((i) => i.ScriptRegion.ToRange().Start); } - private IReadOnlyList GetOccurrences(ScriptRegion scriptRegion) + private IEnumerable GetOccurrences(ScriptRegion scriptRegion) { - return SymbolsService.FindOccurrencesInFile( - GetScriptFile(scriptRegion), - scriptRegion.StartLineNumber, - scriptRegion.StartColumnNumber); + return SymbolsService + .FindOccurrencesInFile( + GetScriptFile(scriptRegion), + scriptRegion.StartLineNumber, + scriptRegion.StartColumnNumber) + .OrderBy(symbol => symbol.ScriptRegion.ToRange().Start) + .ToArray(); } - private List FindSymbolsInFile(ScriptRegion scriptRegion) => symbolsService.FindSymbolsInFile(GetScriptFile(scriptRegion)); + private IEnumerable FindSymbolsInFile(ScriptRegion scriptRegion) + { + return symbolsService + .FindSymbolsInFile(GetScriptFile(scriptRegion)) + .OrderBy(symbol => symbol.ScriptRegion.ToRange().Start); + } [Fact] public async Task FindsParameterHintsOnCommand() { - ParameterSetSignatures paramSignatures = await GetParamSetSignatures(FindsParameterSetsOnCommandData.SourceDetails).ConfigureAwait(true); - Assert.NotNull(paramSignatures); - Assert.Equal("Get-Process", paramSignatures.CommandName); - Assert.Equal(6, paramSignatures.Signatures.Length); + // TODO: Fix signatures to use parameters, not sets. + ParameterSetSignatures signatures = await GetParamSetSignatures(FindsParameterSetsOnCommandData.SourceDetails).ConfigureAwait(true); + Assert.NotNull(signatures); + Assert.Equal("Get-Process", signatures.CommandName); + Assert.Equal(6, signatures.Signatures.Length); } [Fact] public async Task FindsCommandForParamHintsWithSpaces() { - ParameterSetSignatures paramSignatures = await GetParamSetSignatures(FindsParameterSetsOnCommandWithSpacesData.SourceDetails).ConfigureAwait(true); - Assert.NotNull(paramSignatures); - Assert.Equal("Write-Host", paramSignatures.CommandName); - Assert.Single(paramSignatures.Signatures); + ParameterSetSignatures signatures = await GetParamSetSignatures(FindsParameterSetsOnCommandWithSpacesData.SourceDetails).ConfigureAwait(true); + Assert.NotNull(signatures); + Assert.Equal("Write-Host", signatures.CommandName); + Assert.Single(signatures.Signatures); } [Fact] public async Task FindsFunctionDefinition() { - SymbolReference definitionResult = await GetDefinition(FindsFunctionDefinitionData.SourceDetails).ConfigureAwait(true); - Assert.Equal(1, definitionResult.ScriptRegion.StartLineNumber); - Assert.Equal(10, definitionResult.ScriptRegion.StartColumnNumber); - Assert.Equal("My-Function", definitionResult.SymbolName); + SymbolReference symbol = await GetDefinition(FindsFunctionDefinitionData.SourceDetails).ConfigureAwait(true); + Assert.Equal("fn My-Function", symbol.Id); + Assert.Equal("function My-Function ($myInput)", symbol.Name); + Assert.Equal(SymbolType.Function, symbol.Type); + AssertIsRegion(symbol.NameRegion, 1, 10, 1, 21); + AssertIsRegion(symbol.ScriptRegion, 1, 1, 4, 2); + Assert.True(symbol.IsDeclaration); } [Fact] @@ -142,19 +182,48 @@ await psesHost.ExecutePSCommandAsync( new PSCommand().AddScript("Set-Alias -Name My-Alias -Value My-Function"), CancellationToken.None).ConfigureAwait(true); - SymbolReference definitionResult = await GetDefinition(FindsFunctionDefinitionOfAliasData.SourceDetails).ConfigureAwait(true); - Assert.Equal(1, definitionResult.ScriptRegion.StartLineNumber); - Assert.Equal(10, definitionResult.ScriptRegion.StartColumnNumber); - Assert.Equal("My-Function", definitionResult.SymbolName); + SymbolReference symbol = await GetDefinition(FindsFunctionDefinitionOfAliasData.SourceDetails).ConfigureAwait(true); + Assert.Equal("function My-Function ($myInput)", symbol.Name); + Assert.Equal(SymbolType.Function, symbol.Type); + AssertIsRegion(symbol.NameRegion, 1, 10, 1, 21); + AssertIsRegion(symbol.ScriptRegion, 1, 1, 4, 2); + Assert.True(symbol.IsDeclaration); } [Fact] public async Task FindsReferencesOnFunction() { - List referencesResult = await GetReferences(FindsReferencesOnFunctionData.SourceDetails).ConfigureAwait(true); - Assert.Equal(2, referencesResult.Count); - Assert.Equal(3, referencesResult[0].ScriptRegion.StartLineNumber); - Assert.Equal(2, referencesResult[0].ScriptRegion.StartColumnNumber); + IEnumerable symbols = await GetReferences(FindsReferencesOnFunctionData.SourceDetails).ConfigureAwait(true); + Assert.Collection(symbols, + (i) => + { + Assert.Equal("fn My-Function", i.Id); + Assert.Equal("function My-Function ($myInput)", i.Name); + Assert.Equal(SymbolType.Function, i.Type); + Assert.True(i.IsDeclaration); + }, + (i) => + { + Assert.Equal("fn My-Function", i.Id); + Assert.Equal("My-Function", i.Name); + Assert.Equal(SymbolType.Function, i.Type); + Assert.EndsWith(FindsFunctionDefinitionInWorkspaceData.SourceDetails.File, i.FilePath); + Assert.False(i.IsDeclaration); + }, + (i) => + { + Assert.Equal("fn My-Function", i.Id); + Assert.Equal("My-Function", i.Name); + Assert.Equal(SymbolType.Function, i.Type); + Assert.False(i.IsDeclaration); + }, + (i) => + { + Assert.Equal("fn My-Function", i.Id); + Assert.Equal("My-Function", i.Name); + Assert.Equal(SymbolType.Function, i.Type); + Assert.False(i.IsDeclaration); + }); } [Fact] @@ -165,218 +234,676 @@ await psesHost.ExecutePSCommandAsync( new PSCommand().AddScript("Set-Alias -Name My-Alias -Value My-Function"), CancellationToken.None).ConfigureAwait(true); - List referencesResult = await GetReferences(FindsReferencesOnFunctionData.SourceDetails).ConfigureAwait(true); - Assert.Equal(3, referencesResult.Count); - Assert.Equal(3, referencesResult[0].ScriptRegion.StartLineNumber); - Assert.Equal(2, referencesResult[0].ScriptRegion.StartColumnNumber); + IEnumerable symbols = await GetReferences(FindsReferencesOnFunctionData.SourceDetails).ConfigureAwait(true); + + Assert.Collection(symbols, + (i) => AssertIsRegion(i.NameRegion, 1, 10, 1, 21), + (i) => AssertIsRegion(i.NameRegion, 3, 1, 3, 12), + (i) => AssertIsRegion(i.NameRegion, 3, 5, 3, 16), + (i) => AssertIsRegion(i.NameRegion, 10, 1, 10, 12), + // The alias. + (i) => + { + AssertIsRegion(i.NameRegion, 20, 1, 20, 9); + Assert.Equal("fn My-Alias", i.Id); + }); } [Fact] - public async Task FindsFunctionDefinitionInDotSourceReference() + public async Task FindsFunctionDefinitionInWorkspace() { - SymbolReference definitionResult = await GetDefinition(FindsFunctionDefinitionInDotSourceReferenceData.SourceDetails).ConfigureAwait(true); - Assert.True( - definitionResult.FilePath.EndsWith(FindsFunctionDefinitionData.SourceDetails.File), - "Unexpected reference file: " + definitionResult.FilePath); - Assert.Equal(1, definitionResult.ScriptRegion.StartLineNumber); - Assert.Equal(10, definitionResult.ScriptRegion.StartColumnNumber); - Assert.Equal("My-Function", definitionResult.SymbolName); + IEnumerable symbols = await GetDefinitions(FindsFunctionDefinitionInWorkspaceData.SourceDetails).ConfigureAwait(true); + SymbolReference symbol = Assert.Single(symbols); + Assert.Equal("fn My-Function", symbol.Id); + Assert.Equal("function My-Function ($myInput)", symbol.Name); + Assert.True(symbol.IsDeclaration); + Assert.EndsWith(FindsFunctionDefinitionData.SourceDetails.File, symbol.FilePath); } [Fact] - public async Task FindsDotSourcedFile() + public async Task FindsVariableDefinition() { - SymbolReference definitionResult = await GetDefinition(FindsDotSourcedFileData.SourceDetails).ConfigureAwait(true); - Assert.NotNull(definitionResult); - Assert.True( - definitionResult.FilePath.EndsWith(Path.Combine("References", "ReferenceFileE.ps1")), - "Unexpected reference file: " + definitionResult.FilePath); - Assert.Equal(1, definitionResult.ScriptRegion.StartLineNumber); - Assert.Equal(1, definitionResult.ScriptRegion.StartColumnNumber); - Assert.Equal("./ReferenceFileE.ps1", definitionResult.SymbolName); + SymbolReference symbol = await GetDefinition(FindsVariableDefinitionData.SourceDetails).ConfigureAwait(true); + Assert.Equal("var things", symbol.Id); + Assert.Equal("$things", symbol.Name); + Assert.Equal(SymbolType.Variable, symbol.Type); + Assert.True(symbol.IsDeclaration); + AssertIsRegion(symbol.NameRegion, 6, 1, 6, 8); } [Fact] - public async Task FindsFunctionDefinitionInWorkspace() + public async Task FindsReferencesOnVariable() { - workspace.WorkspacePath = TestUtilities.GetSharedPath("References"); - SymbolReference definitionResult = await GetDefinition(FindsFunctionDefinitionInWorkspaceData.SourceDetails).ConfigureAwait(true); - Assert.EndsWith("ReferenceFileE.ps1", definitionResult.FilePath); - Assert.Equal("My-FunctionInFileE", definitionResult.SymbolName); + IEnumerable symbols = await GetReferences(FindsReferencesOnVariableData.SourceDetails).ConfigureAwait(true); + Assert.Collection(symbols, + (i) => + { + Assert.Equal("var things", i.Id); + Assert.Equal("$things", i.Name); + Assert.Equal(SymbolType.Variable, i.Type); + Assert.True(i.IsDeclaration); + }, + (i) => + { + Assert.Equal("var things", i.Id); + Assert.Equal("$things", i.Name); + Assert.Equal(SymbolType.Variable, i.Type); + Assert.False(i.IsDeclaration); + }, + (i) => + { + Assert.Equal("var things", i.Id); + Assert.Equal("$things", i.Name); + Assert.Equal(SymbolType.Variable, i.Type); + Assert.False(i.IsDeclaration); + }); + + Assert.Equal(symbols, GetOccurrences(FindsOccurrencesOnVariableData.SourceDetails)); } [Fact] - public async Task FindsVariableDefinition() + public void FindsOccurrencesOnFunction() { - SymbolReference definitionResult = await GetDefinition(FindsVariableDefinitionData.SourceDetails).ConfigureAwait(true); - Assert.Equal(6, definitionResult.ScriptRegion.StartLineNumber); - Assert.Equal(1, definitionResult.ScriptRegion.StartColumnNumber); - Assert.Equal("$things", definitionResult.SymbolName); + IEnumerable symbols = GetOccurrences(FindsOccurrencesOnFunctionData.SourceDetails); + Assert.Collection(symbols, + (i) => + { + Assert.Equal("fn My-Function", i.Id); + Assert.Equal(SymbolType.Function, i.Type); + Assert.True(i.IsDeclaration); + }, + (i) => + { + Assert.Equal("fn My-Function", i.Id); + Assert.Equal(SymbolType.Function, i.Type); + Assert.False(i.IsDeclaration); + }, + (i) => + { + Assert.Equal("fn My-Function", i.Id); + Assert.Equal(SymbolType.Function, i.Type); + Assert.False(i.IsDeclaration); + }); } [Fact] - public async Task FindsReferencesOnVariable() + public void FindsOccurrencesOnParameter() { - List referencesResult = await GetReferences(FindsReferencesOnVariableData.SourceDetails).ConfigureAwait(true); - Assert.Equal(3, referencesResult.Count); - Assert.Equal(10, referencesResult[referencesResult.Count - 1].ScriptRegion.StartLineNumber); - Assert.Equal(13, referencesResult[referencesResult.Count - 1].ScriptRegion.StartColumnNumber); + IEnumerable symbols = GetOccurrences(FindOccurrencesOnParameterData.SourceDetails); + Assert.Collection(symbols, + (i) => + { + Assert.Equal("var myInput", i.Id); + // TODO: Parameter names need work. + Assert.Equal("(parameter) [System.Object]$myInput", i.Name); + Assert.Equal(SymbolType.Parameter, i.Type); + AssertIsRegion(i.NameRegion, 1, 23, 1, 31); + Assert.True(i.IsDeclaration); + }, + (i) => + { + Assert.Equal("var myInput", i.Id); + Assert.Equal("$myInput", i.Name); + Assert.Equal(SymbolType.Variable, i.Type); + AssertIsRegion(i.NameRegion, 3, 17, 3, 25); + Assert.False(i.IsDeclaration); + }); } [Fact] - public void FindsOccurrencesOnFunction() + public async Task FindsReferencesOnCommandWithAlias() { - IReadOnlyList occurrencesResult = GetOccurrences(FindsOccurrencesOnFunctionData.SourceDetails); - Assert.Equal(3, occurrencesResult.Count); - Assert.Equal(10, occurrencesResult[occurrencesResult.Count - 1].ScriptRegion.StartLineNumber); - Assert.Equal(1, occurrencesResult[occurrencesResult.Count - 1].ScriptRegion.StartColumnNumber); + // NOTE: This doesn't use GetOccurrences as it's testing for aliases. + IEnumerable symbols = await GetReferences(FindsReferencesOnBuiltInCommandWithAliasData.SourceDetails).ConfigureAwait(true); + Assert.Collection(symbols.Where( + (i) => i.FilePath + .EndsWith(FindsReferencesOnBuiltInCommandWithAliasData.SourceDetails.File)), + (i) => Assert.Equal("fn Get-ChildItem", i.Id), + (i) => Assert.Equal("fn gci", i.Id), + (i) => Assert.Equal("fn dir", i.Id), + (i) => Assert.Equal("fn Get-ChildItem", i.Id)); } [Fact] - public void FindsOccurrencesOnParameter() + public async Task FindsClassDefinition() { - IReadOnlyList occurrencesResult = GetOccurrences(FindOccurrencesOnParameterData.SourceDetails); - Assert.Equal(2, occurrencesResult.Count); - Assert.Equal("$myInput", occurrencesResult[occurrencesResult.Count - 1].SymbolName); - Assert.Equal(3, occurrencesResult[occurrencesResult.Count - 1].ScriptRegion.StartLineNumber); + SymbolReference symbol = await GetDefinition(FindsTypeSymbolsDefinitionData.ClassSourceDetails).ConfigureAwait(true); + Assert.Equal("type SuperClass", symbol.Id); + Assert.Equal("class SuperClass { }", symbol.Name); + Assert.Equal(SymbolType.Class, symbol.Type); + Assert.True(symbol.IsDeclaration); + AssertIsRegion(symbol.NameRegion, 8, 7, 8, 17); } [Fact] - public async Task FindsReferencesOnCommandWithAlias() + public async Task FindsReferencesOnClass() { - List referencesResult = await GetReferences(FindsReferencesOnBuiltInCommandWithAliasData.SourceDetails).ConfigureAwait(true); - Assert.Equal(4, referencesResult.Count); - Assert.Equal("Get-ChildItem", referencesResult[1].SymbolName); - Assert.Equal("Get-ChildItem", referencesResult[2].SymbolName); - Assert.Equal("Get-ChildItem", referencesResult[referencesResult.Count - 1].SymbolName); + IEnumerable symbols = await GetReferences(FindsReferencesOnTypeSymbolsData.ClassSourceDetails).ConfigureAwait(true); + Assert.Collection(symbols, + (i) => + { + Assert.Equal("type SuperClass", i.Id); + Assert.Equal("class SuperClass { }", i.Name); + Assert.Equal(SymbolType.Class, i.Type); + Assert.True(i.IsDeclaration); + }, + (i) => + { + Assert.Equal("type SuperClass", i.Id); + Assert.Equal("(type) SuperClass", i.Name); + Assert.Equal(SymbolType.Type, i.Type); + Assert.False(i.IsDeclaration); + }); + + Assert.Equal(symbols, GetOccurrences(FindsOccurrencesOnTypeSymbolsData.ClassSourceDetails)); } [Fact] - public async Task FindsReferencesOnFileWithReferencesFileB() + public async Task FindsEnumDefinition() { - List referencesResult = await GetReferences(FindsReferencesOnFunctionMultiFileDotSourceFileB.SourceDetails).ConfigureAwait(true); - Assert.Equal(3, referencesResult.Count); + SymbolReference symbol = await GetDefinition(FindsTypeSymbolsDefinitionData.EnumSourceDetails).ConfigureAwait(true); + Assert.Equal("type MyEnum", symbol.Id); + Assert.Equal("enum MyEnum { }", symbol.Name); + Assert.Equal(SymbolType.Enum, symbol.Type); + Assert.True(symbol.IsDeclaration); + AssertIsRegion(symbol.NameRegion, 39, 6, 39, 12); } [Fact] - public async Task FindsReferencesOnFileWithReferencesFileC() + public async Task FindsReferencesOnEnum() { - List referencesResult = await GetReferences(FindsReferencesOnFunctionMultiFileDotSourceFileC.SourceDetails).ConfigureAwait(true); - Assert.Equal(3, referencesResult.Count); + IEnumerable symbols = await GetReferences(FindsReferencesOnTypeSymbolsData.EnumSourceDetails).ConfigureAwait(true); + Assert.Collection(symbols, + (i) => + { + Assert.Equal("type MyEnum", i.Id); + Assert.Equal("(type) MyEnum", i.Name); + Assert.Equal(SymbolType.Type, i.Type); + Assert.False(i.IsDeclaration); + }, + (i) => + { + Assert.Equal("type MyEnum", i.Id); + Assert.Equal("enum MyEnum { }", i.Name); + Assert.Equal(SymbolType.Enum, i.Type); + Assert.True(i.IsDeclaration); + }, + (i) => + { + Assert.Equal("type MyEnum", i.Id); + Assert.Equal("(type) MyEnum", i.Name); + Assert.Equal(SymbolType.Type, i.Type); + Assert.False(i.IsDeclaration); + }, + (i) => + { + Assert.Equal("type MyEnum", i.Id); + Assert.Equal("(type) MyEnum", i.Name); + Assert.Equal(SymbolType.Type, i.Type); + Assert.False(i.IsDeclaration); + }); + + Assert.Equal(symbols, GetOccurrences(FindsOccurrencesOnTypeSymbolsData.EnumSourceDetails)); } [Fact] - public async Task FindsDetailsForBuiltInCommand() + public async Task FindsTypeExpressionDefinition() { - SymbolDetails symbolDetails = await symbolsService.FindSymbolDetailsAtLocationAsync( - GetScriptFile(FindsDetailsForBuiltInCommandData.SourceDetails), - FindsDetailsForBuiltInCommandData.SourceDetails.StartLineNumber, - FindsDetailsForBuiltInCommandData.SourceDetails.StartColumnNumber).ConfigureAwait(true); + SymbolReference symbol = await GetDefinition(FindsTypeSymbolsDefinitionData.TypeExpressionSourceDetails).ConfigureAwait(true); + AssertIsRegion(symbol.NameRegion, 39, 6, 39, 12); + Assert.Equal("type MyEnum", symbol.Id); + Assert.Equal("enum MyEnum { }", symbol.Name); + Assert.True(symbol.IsDeclaration); + } - Assert.NotNull(symbolDetails.Documentation); - Assert.NotEqual("", symbolDetails.Documentation); + [Fact] + public async Task FindsReferencesOnTypeExpression() + { + IEnumerable symbols = await GetReferences(FindsReferencesOnTypeSymbolsData.TypeExpressionSourceDetails).ConfigureAwait(true); + Assert.Collection(symbols, + (i) => + { + Assert.Equal("type SuperClass", i.Id); + Assert.Equal("class SuperClass { }", i.Name); + Assert.Equal(SymbolType.Class, i.Type); + Assert.True(i.IsDeclaration); + }, + (i) => + { + Assert.Equal("type SuperClass", i.Id); + Assert.Equal("(type) SuperClass", i.Name); + Assert.Equal(SymbolType.Type, i.Type); + Assert.False(i.IsDeclaration); + }); + + Assert.Equal(symbols, GetOccurrences(FindsOccurrencesOnTypeSymbolsData.TypeExpressionSourceDetails)); } [Fact] - public void FindsSymbolsInFile() + public async Task FindsTypeConstraintDefinition() { - List symbolsResult = - FindSymbolsInFile( - FindSymbolsInMultiSymbolFile.SourceDetails); + SymbolReference symbol = await GetDefinition(FindsTypeSymbolsDefinitionData.TypeConstraintSourceDetails).ConfigureAwait(true); + AssertIsRegion(symbol.NameRegion, 39, 6, 39, 12); + Assert.Equal("type MyEnum", symbol.Id); + Assert.Equal("enum MyEnum { }", symbol.Name); + Assert.True(symbol.IsDeclaration); + } - Assert.Equal(4, symbolsResult.Count(symbolReference => symbolReference.SymbolType == SymbolType.Function)); - Assert.Equal(3, symbolsResult.Count(symbolReference => symbolReference.SymbolType == SymbolType.Variable)); - Assert.Single(symbolsResult.Where(symbolReference => symbolReference.SymbolType == SymbolType.Workflow)); + [Fact] + public async Task FindsReferencesOnTypeConstraint() + { + IEnumerable symbols = await GetReferences(FindsReferencesOnTypeSymbolsData.TypeConstraintSourceDetails).ConfigureAwait(true); + Assert.Collection(symbols, + (i) => + { + Assert.Equal("type MyEnum", i.Id); + Assert.Equal("(type) MyEnum", i.Name); + Assert.Equal(SymbolType.Type, i.Type); + Assert.False(i.IsDeclaration); + }, + (i) => + { + Assert.Equal("type MyEnum", i.Id); + Assert.Equal("enum MyEnum { }", i.Name); + Assert.Equal(SymbolType.Enum, i.Type); + Assert.True(i.IsDeclaration); + }, + (i) => + { + Assert.Equal("type MyEnum", i.Id); + Assert.Equal("(type) MyEnum", i.Name); + Assert.Equal(SymbolType.Type, i.Type); + Assert.False(i.IsDeclaration); + }, + (i) => + { + Assert.Equal("type MyEnum", i.Id); + Assert.Equal("(type) MyEnum", i.Name); + Assert.Equal(SymbolType.Type, i.Type); + Assert.False(i.IsDeclaration); + }); + } - SymbolReference firstFunctionSymbol = symbolsResult.First(r => r.SymbolType == SymbolType.Function); - Assert.Equal("AFunction", firstFunctionSymbol.SymbolName); - Assert.Equal(7, firstFunctionSymbol.ScriptRegion.StartLineNumber); - Assert.Equal(1, firstFunctionSymbol.ScriptRegion.StartColumnNumber); + [Fact] + public void FindsOccurrencesOnTypeConstraint() + { + IEnumerable symbols = GetOccurrences(FindsOccurrencesOnTypeSymbolsData.TypeConstraintSourceDetails); + Assert.Collection(symbols, + (i) => + { + Assert.Equal("type BaseClass", i.Id); + Assert.Equal("class BaseClass { }", i.Name); + Assert.Equal(SymbolType.Class, i.Type); + Assert.True(i.IsDeclaration); + }, + (i) => + { + Assert.Equal("type BaseClass", i.Id); + Assert.Equal("(type) BaseClass", i.Name); + Assert.Equal(SymbolType.Type, i.Type); + Assert.False(i.IsDeclaration); + }); + } - SymbolReference lastVariableSymbol = symbolsResult.Last(r => r.SymbolType == SymbolType.Variable); - Assert.Equal("$Script:ScriptVar2", lastVariableSymbol.SymbolName); - Assert.Equal(3, lastVariableSymbol.ScriptRegion.StartLineNumber); - Assert.Equal(1, lastVariableSymbol.ScriptRegion.StartColumnNumber); + [Fact] + public async Task FindsConstructorDefinition() + { + IEnumerable symbols = await GetDefinitions(FindsTypeSymbolsDefinitionData.ConstructorSourceDetails).ConfigureAwait(true); + Assert.Collection(symbols, + (i) => + { + Assert.Equal("mtd SuperClass", i.Id); + Assert.Equal("SuperClass([string]$name)", i.Name); + Assert.Equal(SymbolType.Constructor, i.Type); + Assert.True(i.IsDeclaration); + }, + (i) => + { + Assert.Equal("mtd SuperClass", i.Id); + Assert.Equal("SuperClass()", i.Name); + Assert.Equal(SymbolType.Constructor, i.Type); + Assert.True(i.IsDeclaration); + }); + + Assert.Equal(symbols, await GetReferences(FindsReferencesOnTypeSymbolsData.ConstructorSourceDetails).ConfigureAwait(true)); + Assert.Equal(symbols, GetOccurrences(FindsOccurrencesOnTypeSymbolsData.ConstructorSourceDetails)); + } - SymbolReference firstWorkflowSymbol = symbolsResult.First(r => r.SymbolType == SymbolType.Workflow); - Assert.Equal("AWorkflow", firstWorkflowSymbol.SymbolName); - Assert.Equal(23, firstWorkflowSymbol.ScriptRegion.StartLineNumber); - Assert.Equal(1, firstWorkflowSymbol.ScriptRegion.StartColumnNumber); + [Fact] + public async Task FindsMethodDefinition() + { + IEnumerable symbols = await GetDefinitions(FindsTypeSymbolsDefinitionData.MethodSourceDetails).ConfigureAwait(true); + Assert.Collection(symbols, + (i) => + { + Assert.Equal("mtd MyClassMethod", i.Id); + Assert.Equal("string MyClassMethod([string]$param1, $param2, [int]$param3)", i.Name); + Assert.Equal(SymbolType.Method, i.Type); + Assert.True(i.IsDeclaration); + }, + (i) => + { + Assert.Equal("mtd MyClassMethod", i.Id); + Assert.Equal("string MyClassMethod([MyEnum]$param1)", i.Name); + Assert.Equal(SymbolType.Method, i.Type); + Assert.True(i.IsDeclaration); + }, + (i) => + { + Assert.Equal("mtd MyClassMethod", i.Id); + Assert.Equal("string MyClassMethod()", i.Name); + Assert.Equal(SymbolType.Method, i.Type); + Assert.True(i.IsDeclaration); + }); + } - // TODO: Bring this back when we can use AstVisitor2 again (#276) - //Assert.Equal(1, symbolsResult.FoundOccurrences.Where(r => r.SymbolType == SymbolType.Configuration).Count()); - //SymbolReference firstConfigurationSymbol = symbolsResult.FoundOccurrences.Where(r => r.SymbolType == SymbolType.Configuration).First(); - //Assert.Equal("AConfiguration", firstConfigurationSymbol.SymbolName); - //Assert.Equal(25, firstConfigurationSymbol.ScriptRegion.StartLineNumber); - //Assert.Equal(1, firstConfigurationSymbol.ScriptRegion.StartColumnNumber); + [Fact] + public async Task FindsReferencesOnMethod() + { + IEnumerable symbols = await GetReferences(FindsReferencesOnTypeSymbolsData.MethodSourceDetails).ConfigureAwait(true); + Assert.Collection(symbols, + (i) => Assert.Equal("string MyClassMethod([string]$param1, $param2, [int]$param3)", i.Name), + (i) => Assert.Equal("string MyClassMethod([MyEnum]$param1)", i.Name), + (i) => Assert.Equal("string MyClassMethod()", i.Name), + (i) => // The invocation! + { + Assert.Equal("mtd MyClassMethod", i.Id); + Assert.Equal("(method) MyClassMethod", i.Name); + Assert.Equal("$o.MyClassMethod()", i.SourceLine); + Assert.Equal(SymbolType.Method, i.Type); + Assert.False(i.IsDeclaration); + }); + + Assert.Equal(symbols, GetOccurrences(FindsOccurrencesOnTypeSymbolsData.MethodSourceDetails)); } [Fact] - public void FindsSymbolsInPesterFile() + public async Task FindsPropertyDefinition() { - List symbolsResult = FindSymbolsInFile(FindSymbolsInPesterFile.SourceDetails).OfType().ToList(); - Assert.Equal(12, symbolsResult.Count(r => r.SymbolType == SymbolType.Function)); + SymbolReference symbol = await GetDefinition(FindsTypeSymbolsDefinitionData.PropertySourceDetails).ConfigureAwait(true); + Assert.Equal("prop SomePropWithDefault", symbol.Id); + Assert.Equal("[string] $SomePropWithDefault", symbol.Name); + Assert.Equal(SymbolType.Property, symbol.Type); + Assert.True(symbol.IsDeclaration); + } - Assert.Equal(1, symbolsResult.Count(r => r.Command == PesterCommandType.Describe)); - SymbolReference firstDescribeSymbol = symbolsResult.First(r => r.Command == PesterCommandType.Describe); - Assert.Equal("Describe \"Testing Pester symbols\"", firstDescribeSymbol.SymbolName); - Assert.Equal(9, firstDescribeSymbol.ScriptRegion.StartLineNumber); - Assert.Equal(1, firstDescribeSymbol.ScriptRegion.StartColumnNumber); + [Fact] + public async Task FindsReferencesOnProperty() + { + IEnumerable symbols = await GetReferences(FindsReferencesOnTypeSymbolsData.PropertySourceDetails).ConfigureAwait(true); + Assert.Collection(symbols, + (i) => + { + Assert.Equal("prop SomeProp", i.Id); + Assert.Equal("[int] $SomeProp", i.Name); + Assert.Equal(SymbolType.Property, i.Type); + Assert.True(i.IsDeclaration); + }, + (i) => + { + Assert.Equal("prop SomeProp", i.Id); + Assert.Equal("(property) SomeProp", i.Name); + Assert.Equal(SymbolType.Property, i.Type); + Assert.False(i.IsDeclaration); + }); + } + + [Fact] + public void FindsOccurrencesOnProperty() + { + IEnumerable symbols = GetOccurrences(FindsOccurrencesOnTypeSymbolsData.PropertySourceDetails); + Assert.Collection(symbols, + (i) => + { + Assert.Equal("prop SomePropWithDefault", i.Id); + Assert.Equal("[string] $SomePropWithDefault", i.Name); + Assert.Equal(SymbolType.Property, i.Type); + Assert.True(i.IsDeclaration); + }, + (i) => + { + Assert.Equal("prop SomePropWithDefault", i.Id); + Assert.Equal("(property) SomePropWithDefault", i.Name); + Assert.Equal(SymbolType.Property, i.Type); + Assert.False(i.IsDeclaration); + }); + } + + [Fact] + public async Task FindsEnumMemberDefinition() + { + SymbolReference symbol = await GetDefinition(FindsTypeSymbolsDefinitionData.EnumMemberSourceDetails).ConfigureAwait(true); + Assert.Equal("prop Second", symbol.Id); + // Doesn't include [MyEnum]:: because that'd be redundant in the outline. + Assert.Equal("Second", symbol.Name); + Assert.Equal(SymbolType.EnumMember, symbol.Type); + Assert.True(symbol.IsDeclaration); + AssertIsRegion(symbol.NameRegion, 41, 5, 41, 11); + + symbol = await GetDefinition(FindsReferencesOnTypeSymbolsData.EnumMemberSourceDetails).ConfigureAwait(true); + Assert.Equal("prop First", symbol.Id); + Assert.Equal("First", symbol.Name); + Assert.Equal(SymbolType.EnumMember, symbol.Type); + Assert.True(symbol.IsDeclaration); + AssertIsRegion(symbol.NameRegion, 40, 5, 40, 10); + } + + [Fact] + public async Task FindsReferencesOnEnumMember() + { + IEnumerable symbols = await GetReferences(FindsReferencesOnTypeSymbolsData.EnumMemberSourceDetails).ConfigureAwait(true); + Assert.Collection(symbols, + (i) => + { + Assert.Equal("prop First", i.Id); + Assert.Equal("First", i.Name); + Assert.Equal(SymbolType.EnumMember, i.Type); + Assert.True(i.IsDeclaration); + }, + (i) => + { + Assert.Equal("prop First", i.Id); + // The reference is just a member invocation, and so indistinguishable from a property. + Assert.Equal("(property) First", i.Name); + Assert.Equal(SymbolType.Property, i.Type); + Assert.False(i.IsDeclaration); + }); + + Assert.Equal(symbols, GetOccurrences(FindsOccurrencesOnTypeSymbolsData.EnumMemberSourceDetails)); + } - Assert.Equal(1, symbolsResult.Count(r => r.Command == PesterCommandType.Context)); - SymbolReference firstContextSymbol = symbolsResult.First(r => r.Command == PesterCommandType.Context); - Assert.Equal("Context \"When a Pester file is given\"", firstContextSymbol.SymbolName); - Assert.Equal(10, firstContextSymbol.ScriptRegion.StartLineNumber); - Assert.Equal(5, firstContextSymbol.ScriptRegion.StartColumnNumber); + [SkippableFact] + public async Task FindsDetailsForBuiltInCommand() + { + Skip.IfNot(VersionUtils.IsMacOS, "macOS gets the right synopsis but others don't."); + SymbolDetails symbolDetails = await symbolsService.FindSymbolDetailsAtLocationAsync( + GetScriptFile(FindsDetailsForBuiltInCommandData.SourceDetails), + FindsDetailsForBuiltInCommandData.SourceDetails.StartLineNumber, + FindsDetailsForBuiltInCommandData.SourceDetails.StartColumnNumber).ConfigureAwait(true); - Assert.Equal(4, symbolsResult.Count(r => r.Command == PesterCommandType.It)); - SymbolReference lastItSymbol = symbolsResult.Last(r => r.Command == PesterCommandType.It); - Assert.Equal("It \"Should return setup and teardown symbols\"", lastItSymbol.SymbolName); - Assert.Equal(31, lastItSymbol.ScriptRegion.StartLineNumber); - Assert.Equal(9, lastItSymbol.ScriptRegion.StartColumnNumber); + Assert.Equal("Gets the processes that are running on the local computer.", symbolDetails.Documentation); + } - Assert.Equal(1, symbolsResult.Count(r => r.Command == PesterCommandType.BeforeDiscovery)); - SymbolReference firstBeforeDisocverySymbol = symbolsResult.First(r => r.Command == PesterCommandType.BeforeDiscovery); - Assert.Equal("BeforeDiscovery", firstBeforeDisocverySymbol.SymbolName); - Assert.Equal(1, firstBeforeDisocverySymbol.ScriptRegion.StartLineNumber); - Assert.Equal(1, firstBeforeDisocverySymbol.ScriptRegion.StartColumnNumber); + [Fact] + public void FindsSymbolsInFile() + { + IEnumerable symbols = FindSymbolsInFile(FindSymbolsInMultiSymbolFile.SourceDetails); + + Assert.Equal(7, symbols.Count(i => i.Type == SymbolType.Function)); + Assert.Equal(8, symbols.Count(i => i.Type == SymbolType.Variable)); + Assert.Equal(4, symbols.Count(i => i.Type == SymbolType.Parameter)); + Assert.Equal(12, symbols.Count(i => i.Id.StartsWith("var "))); + Assert.Equal(2, symbols.Count(i => i.Id.StartsWith("prop "))); + + SymbolReference symbol = symbols.First(i => i.Type == SymbolType.Function); + Assert.Equal("fn AFunction", symbol.Id); + Assert.Equal("function AFunction ()", symbol.Name); + Assert.True(symbol.IsDeclaration); + + symbol = symbols.First(i => i.Id == "fn AFilter"); + Assert.Equal("filter AFilter ()", symbol.Name); + Assert.True(symbol.IsDeclaration); + + symbol = symbols.Last(i => i.Type == SymbolType.Variable); + Assert.Equal("var nestedVar", symbol.Id); + Assert.Equal("$nestedVar", symbol.Name); + Assert.False(symbol.IsDeclaration); + AssertIsRegion(symbol.NameRegion, 16, 29, 16, 39); + + symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.Workflow)); + Assert.Equal("fn AWorkflow", symbol.Id); + Assert.Equal("workflow AWorkflow ()", symbol.Name); + Assert.True(symbol.IsDeclaration); + + symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.Class)); + Assert.Equal("type AClass", symbol.Id); + Assert.Equal("class AClass { }", symbol.Name); + Assert.True(symbol.IsDeclaration); + + symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.Property)); + Assert.Equal("prop AProperty", symbol.Id); + Assert.Equal("[string] $AProperty", symbol.Name); + Assert.True(symbol.IsDeclaration); + + symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.Constructor)); + Assert.Equal("mtd AClass", symbol.Id); + Assert.Equal("AClass([string]$AParameter)", symbol.Name); + Assert.True(symbol.IsDeclaration); + + symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.Method)); + Assert.Equal("mtd AMethod", symbol.Id); + Assert.Equal("void AMethod([string]$param1, [int]$param2, $param3)", symbol.Name); + Assert.True(symbol.IsDeclaration); + + symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.Enum)); + Assert.Equal("type AEnum", symbol.Id); + Assert.Equal("enum AEnum { }", symbol.Name); + Assert.True(symbol.IsDeclaration); + + symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.EnumMember)); + Assert.Equal("prop AValue", symbol.Id); + Assert.Equal("AValue", symbol.Name); + Assert.True(symbol.IsDeclaration); + } - Assert.Equal(2, symbolsResult.Count(r => r.Command == PesterCommandType.BeforeAll)); - SymbolReference lastBeforeAllSymbol = symbolsResult.Last(r => r.Command == PesterCommandType.BeforeAll); - Assert.Equal("BeforeAll", lastBeforeAllSymbol.SymbolName); - Assert.Equal(11, lastBeforeAllSymbol.ScriptRegion.StartLineNumber); - Assert.Equal(9, lastBeforeAllSymbol.ScriptRegion.StartColumnNumber); + [Fact] + public void FindsSymbolsWithNewLineInFile() + { + IEnumerable symbols = FindSymbolsInFile(FindSymbolsInNewLineSymbolFile.SourceDetails); + + SymbolReference symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.Function)); + Assert.Equal("fn returnTrue", symbol.Id); + AssertIsRegion(symbol.NameRegion, 2, 1, 2, 11); + AssertIsRegion(symbol.ScriptRegion, 1, 1, 4, 2); + + symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.Class)); + Assert.Equal("type NewLineClass", symbol.Id); + AssertIsRegion(symbol.NameRegion, 7, 1, 7, 13); + AssertIsRegion(symbol.ScriptRegion, 6, 1, 23, 2); + + symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.Constructor)); + Assert.Equal("mtd NewLineClass", symbol.Id); + AssertIsRegion(symbol.NameRegion, 8, 5, 8, 17); + AssertIsRegion(symbol.ScriptRegion, 8, 5, 10, 6); + + symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.Property)); + Assert.Equal("prop SomePropWithDefault", symbol.Id); + AssertIsRegion(symbol.NameRegion, 15, 5, 15, 25); + AssertIsRegion(symbol.ScriptRegion, 12, 5, 15, 40); + + symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.Method)); + Assert.Equal("mtd MyClassMethod", symbol.Id); + Assert.Equal("string MyClassMethod([MyNewLineEnum]$param1)", symbol.Name); + AssertIsRegion(symbol.NameRegion, 20, 5, 20, 18); + AssertIsRegion(symbol.ScriptRegion, 17, 5, 22, 6); + + symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.Enum)); + Assert.Equal("type MyNewLineEnum", symbol.Id); + AssertIsRegion(symbol.NameRegion, 26, 1, 26, 14); + AssertIsRegion(symbol.ScriptRegion, 25, 1, 28, 2); + + symbol = Assert.Single(symbols.Where(i => i.Type == SymbolType.EnumMember)); + Assert.Equal("prop First", symbol.Id); + AssertIsRegion(symbol.NameRegion, 27, 5, 27, 10); + AssertIsRegion(symbol.ScriptRegion, 27, 5, 27, 10); + } - Assert.Equal(1, symbolsResult.Count(r => r.Command == PesterCommandType.BeforeEach)); - SymbolReference firstBeforeEachSymbol = symbolsResult.First(r => r.Command == PesterCommandType.BeforeEach); - Assert.Equal("BeforeEach", firstBeforeEachSymbol.SymbolName); - Assert.Equal(15, firstBeforeEachSymbol.ScriptRegion.StartLineNumber); - Assert.Equal(9, firstBeforeEachSymbol.ScriptRegion.StartColumnNumber); + [Fact(Skip="DSC symbols don't work yet.")] + public void FindsSymbolsInDSCFile() + { + Skip.If(!s_isWindows, "DSC only works properly on Windows."); - Assert.Equal(1, symbolsResult.Count(r => r.Command == PesterCommandType.AfterEach)); - SymbolReference firstAfterEachSymbol = symbolsResult.First(r => r.Command == PesterCommandType.AfterEach); - Assert.Equal("AfterEach", firstAfterEachSymbol.SymbolName); - Assert.Equal(35, firstAfterEachSymbol.ScriptRegion.StartLineNumber); - Assert.Equal(9, firstAfterEachSymbol.ScriptRegion.StartColumnNumber); + IEnumerable symbols = FindSymbolsInFile(FindSymbolsInDSCFile.SourceDetails); + SymbolReference symbol = Assert.Single(symbols, i => i.Type == SymbolType.Configuration); + Assert.Equal("AConfiguration", symbol.Id); + Assert.Equal(2, symbol.ScriptRegion.StartLineNumber); + Assert.Equal(15, symbol.ScriptRegion.StartColumnNumber); + } - Assert.Equal(1, symbolsResult.Count(r => r.Command == PesterCommandType.AfterAll)); - SymbolReference firstAfterAllSymbol = symbolsResult.First(r => r.Command == PesterCommandType.AfterAll); - Assert.Equal("AfterAll", firstAfterAllSymbol.SymbolName); - Assert.Equal(40, firstAfterAllSymbol.ScriptRegion.StartLineNumber); - Assert.Equal(5, firstAfterAllSymbol.ScriptRegion.StartColumnNumber); + [Fact] + public void FindsSymbolsInPesterFile() + { + IEnumerable symbols = FindSymbolsInFile(FindSymbolsInPesterFile.SourceDetails).OfType(); + Assert.Equal(12, symbols.Count(i => i.Type == SymbolType.Function)); + + SymbolReference symbol = Assert.Single(symbols, i => i.Command == PesterCommandType.Describe); + Assert.Equal("Describe \"Testing Pester symbols\"", symbol.Id); + Assert.Equal(9, symbol.ScriptRegion.StartLineNumber); + Assert.Equal(1, symbol.ScriptRegion.StartColumnNumber); + + symbol = Assert.Single(symbols, i => i.Command == PesterCommandType.Context); + Assert.Equal("Context \"When a Pester file is given\"", symbol.Id); + Assert.Equal(10, symbol.ScriptRegion.StartLineNumber); + Assert.Equal(5, symbol.ScriptRegion.StartColumnNumber); + + Assert.Equal(4, symbols.Count(i => i.Command == PesterCommandType.It)); + symbol = symbols.Last(i => i.Command == PesterCommandType.It); + Assert.Equal("It \"Should return setup and teardown symbols\"", symbol.Id); + Assert.Equal(31, symbol.ScriptRegion.StartLineNumber); + Assert.Equal(9, symbol.ScriptRegion.StartColumnNumber); + + symbol = Assert.Single(symbols, i => i.Command == PesterCommandType.BeforeDiscovery); + Assert.Equal("BeforeDiscovery", symbol.Id); + Assert.Equal(1, symbol.ScriptRegion.StartLineNumber); + Assert.Equal(1, symbol.ScriptRegion.StartColumnNumber); + + Assert.Equal(2, symbols.Count(i => i.Command == PesterCommandType.BeforeAll)); + symbol = symbols.Last(i => i.Command == PesterCommandType.BeforeAll); + Assert.Equal("BeforeAll", symbol.Id); + Assert.Equal(11, symbol.ScriptRegion.StartLineNumber); + Assert.Equal(9, symbol.ScriptRegion.StartColumnNumber); + + symbol = Assert.Single(symbols, i => i.Command == PesterCommandType.BeforeEach); + Assert.Equal("BeforeEach", symbol.Id); + Assert.Equal(15, symbol.ScriptRegion.StartLineNumber); + Assert.Equal(9, symbol.ScriptRegion.StartColumnNumber); + + symbol = Assert.Single(symbols, i => i.Command == PesterCommandType.AfterEach); + Assert.Equal("AfterEach", symbol.Id); + Assert.Equal(35, symbol.ScriptRegion.StartLineNumber); + Assert.Equal(9, symbol.ScriptRegion.StartColumnNumber); + + symbol = Assert.Single(symbols, i => i.Command == PesterCommandType.AfterAll); + Assert.Equal("AfterAll", symbol.Id); + Assert.Equal(40, symbol.ScriptRegion.StartLineNumber); + Assert.Equal(5, symbol.ScriptRegion.StartColumnNumber); } [Fact] - public void LangServerFindsSymbolsInPSDFile() + public void FindsSymbolsInPSDFile() { - List symbolsResult = FindSymbolsInFile(FindSymbolsInPSDFile.SourceDetails); - Assert.Equal(3, symbolsResult.Count); + IEnumerable symbols = FindSymbolsInFile(FindSymbolsInPSDFile.SourceDetails); + Assert.All(symbols, i => Assert.Equal(SymbolType.HashtableKey, i.Type)); + Assert.Collection(symbols, + i => Assert.Equal("property1", i.Id), + i => Assert.Equal("property2", i.Id), + i => Assert.Equal("property3", i.Id)); } [Fact] public void FindsSymbolsInNoSymbolsFile() { - List symbolsResult = FindSymbolsInFile(FindSymbolsInNoSymbolsFile.SourceDetails); + IEnumerable symbolsResult = FindSymbolsInFile(FindSymbolsInNoSymbolsFile.SourceDetails); Assert.Empty(symbolsResult); } } diff --git a/test/PowerShellEditorServices.Test/Services/Symbols/AstOperationsTests.cs b/test/PowerShellEditorServices.Test/Services/Symbols/AstOperationsTests.cs index 6789c4323..649ef32fd 100644 --- a/test/PowerShellEditorServices.Test/Services/Symbols/AstOperationsTests.cs +++ b/test/PowerShellEditorServices.Test/Services/Symbols/AstOperationsTests.cs @@ -2,9 +2,12 @@ // Licensed under the MIT License. using System.Collections.Generic; -using System.Management.Automation; -using System.Management.Automation.Language; +using System.Linq; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.PowerShell.EditorServices.Services; using Microsoft.PowerShell.EditorServices.Services.Symbols; +using Microsoft.PowerShell.EditorServices.Services.TextDocument; +using Microsoft.PowerShell.EditorServices.Test.Shared; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using Xunit; @@ -13,64 +16,46 @@ namespace PowerShellEditorServices.Test.Services.Symbols [Trait("Category", "AstOperations")] public class AstOperationsTests { - private const string s_scriptString = @"function BasicFunction {} -BasicFunction + private readonly ScriptFile scriptFile; -function FunctionWithExtraSpace -{ - -} FunctionWithExtraSpace - -function - - - FunctionNameOnDifferentLine - - - - - - - {} - - - FunctionNameOnDifferentLine - - function IndentedFunction { } IndentedFunction -"; - private static readonly ScriptBlockAst s_ast = (ScriptBlockAst)ScriptBlock.Create(s_scriptString).Ast; + public AstOperationsTests() + { + WorkspaceService workspace = new(NullLoggerFactory.Instance); + scriptFile = workspace.GetFile(TestUtilities.GetSharedPath("References/FunctionReference.ps1")); + } [Theory] - [InlineData(1, 15, "BasicFunction")] - [InlineData(2, 3, "BasicFunction")] - [InlineData(4, 31, "FunctionWithExtraSpace")] - [InlineData(7, 18, "FunctionWithExtraSpace")] - [InlineData(12, 22, "FunctionNameOnDifferentLine")] - [InlineData(22, 13, "FunctionNameOnDifferentLine")] - [InlineData(24, 30, "IndentedFunction")] - [InlineData(24, 52, "IndentedFunction")] - public void CanFindSymbolAtPosition(int lineNumber, int columnNumber, string expectedName) + [InlineData(1, 15, "fn BasicFunction")] + [InlineData(2, 3, "fn BasicFunction")] + [InlineData(4, 31, "fn FunctionWithExtraSpace")] + [InlineData(7, 18, "fn FunctionWithExtraSpace")] + [InlineData(12, 22, "fn FunctionNameOnDifferentLine")] + [InlineData(22, 13, "fn FunctionNameOnDifferentLine")] + [InlineData(24, 30, "fn IndentedFunction")] + [InlineData(24, 52, "fn IndentedFunction")] + public void CanFindSymbolAtPosition(int line, int column, string expectedName) { - SymbolReference reference = AstOperations.FindSymbolAtPosition(s_ast, lineNumber, columnNumber); - Assert.NotNull(reference); - Assert.Equal(expectedName, reference.SymbolName); + SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition(line, column); + Assert.NotNull(symbol); + Assert.Equal(expectedName, symbol.Id); } [Theory] [MemberData(nameof(FindReferencesOfSymbolAtPositionData))] - public void CanFindReferencesOfSymbolAtPosition(int lineNumber, int columnNumber, Range[] symbolRange) + public void CanFindReferencesOfSymbolAtPosition(int line, int column, Range[] symbolRange) { - SymbolReference symbol = AstOperations.FindSymbolAtPosition(s_ast, lineNumber, columnNumber); + SymbolReference symbol = scriptFile.References.TryGetSymbolAtPosition(line, column); - IEnumerable references = AstOperations.FindReferencesOfSymbol(s_ast, symbol); + IEnumerable references = scriptFile.References.TryGetReferences(symbol); + Assert.NotEmpty(references); int positionsIndex = 0; - foreach (SymbolReference reference in references) + foreach (SymbolReference reference in references.OrderBy((i) => i.ScriptRegion.ToRange().Start)) { - Assert.Equal(symbolRange[positionsIndex].Start.Line, reference.ScriptRegion.StartLineNumber); - Assert.Equal(symbolRange[positionsIndex].Start.Character, reference.ScriptRegion.StartColumnNumber); - Assert.Equal(symbolRange[positionsIndex].End.Line, reference.ScriptRegion.EndLineNumber); - Assert.Equal(symbolRange[positionsIndex].End.Character, reference.ScriptRegion.EndColumnNumber); + Assert.Equal(symbolRange[positionsIndex].Start.Line, reference.NameRegion.StartLineNumber); + Assert.Equal(symbolRange[positionsIndex].Start.Character, reference.NameRegion.StartColumnNumber); + Assert.Equal(symbolRange[positionsIndex].End.Line, reference.NameRegion.EndLineNumber); + Assert.Equal(symbolRange[positionsIndex].End.Character, reference.NameRegion.EndColumnNumber); positionsIndex++; } diff --git a/test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs b/test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs index 40efc5c26..4839eacfa 100644 --- a/test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs +++ b/test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs @@ -174,30 +174,6 @@ public void CanAppendToEndOfFile() ); } - [Trait("Category", "ScriptFile")] - [Fact] - public void FindsDotSourcedFiles() - { - string exampleScriptContents = TestUtilities.PlatformNormalize( - ". ./athing.ps1\n" + - ". ./somefile.ps1\n" + - ". ./somefile.ps1\n" + - "Do-Stuff $uri\n" + - ". simpleps.ps1"); - - using StringReader stringReader = new(exampleScriptContents); - ScriptFile scriptFile = - new( - // Use any absolute path. Even if it doesn't exist. - DocumentUri.FromFileSystemPath(Path.Combine(Path.GetTempPath(), "TestFile.ps1")), - stringReader, - PowerShellVersion); - - Assert.Equal(3, scriptFile.ReferencedFiles.Length); - Console.Write("a" + scriptFile.ReferencedFiles[0]); - Assert.Equal(TestUtilities.NormalizePath("./athing.ps1"), scriptFile.ReferencedFiles[0]); - } - [Trait("Category", "ScriptFile")] [Fact] public void ThrowsExceptionWithEditOutsideOfRange() @@ -610,7 +586,6 @@ public void PropertiesInitializedCorrectlyForFile() Assert.Equal(path, scriptFile.FilePath, ignoreCase: !VersionUtils.IsLinux); Assert.True(scriptFile.IsAnalysisEnabled); Assert.False(scriptFile.IsInMemory); - Assert.Empty(scriptFile.ReferencedFiles); Assert.Empty(scriptFile.DiagnosticMarkers); Assert.Single(scriptFile.ScriptTokens); Assert.Single(scriptFile.FileLines); @@ -635,7 +610,6 @@ public void PropertiesInitializedCorrectlyForUntitled() Assert.Equal(path, scriptFile.DocumentUri); Assert.True(scriptFile.IsAnalysisEnabled); Assert.True(scriptFile.IsInMemory); - Assert.Empty(scriptFile.ReferencedFiles); Assert.Empty(scriptFile.DiagnosticMarkers); Assert.Equal(10, scriptFile.ScriptTokens.Length); Assert.Equal(3, scriptFile.FileLines.Count);