Skip to content

Commit 87da4ab

Browse files
committed
First pass at implementing FindSymbolsVisitor and a few unit tests to test the functionality.
1 parent fc4721f commit 87da4ab

File tree

11 files changed

+269
-4
lines changed

11 files changed

+269
-4
lines changed

src/PowerShellEditorServices/Language/AstOperations.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,8 @@ static public IEnumerable<SymbolReference> FindReferencesOfSymbol(
131131
scriptAst.Visit(referencesVisitor);
132132

133133
return referencesVisitor.FoundReferences;
134-
135134
}
135+
136136
/// <summary>
137137
/// Finds all references (not including aliases) in a script for the given symbol
138138
/// </summary>
@@ -172,6 +172,19 @@ static public SymbolReference FindDefinitionOfSymbol(
172172
return declarationVisitor.FoundDeclartion;
173173
}
174174

175+
/// <summary>
176+
/// Finds all symbols in a script
177+
/// </summary>
178+
/// <param name="scriptAst">The abstract syntax tree of the given script</param>
179+
/// <returns>A collection of SymbolReference objects</returns>
180+
static public IEnumerable<SymbolReference> FindSymbolsInDocument(Ast scriptAst)
181+
{
182+
FindSymbolsVisitor findSymbolsVisitor = new FindSymbolsVisitor();
183+
scriptAst.Visit(findSymbolsVisitor);
184+
185+
return findSymbolsVisitor.SymbolReferences;
186+
}
187+
175188
/// <summary>
176189
/// Finds all files dot sourced in a script
177190
/// </summary>
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
//
2+
// Copyright (c) Microsoft. All rights reserved.
3+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
4+
//
5+
6+
using System.Collections.Generic;
7+
using System.Management.Automation.Language;
8+
9+
namespace Microsoft.PowerShell.EditorServices
10+
{
11+
/// <summary>
12+
/// The visitor used to find all the symbols (function and class defs) in the AST.
13+
/// </summary>
14+
internal class FindSymbolsVisitor : AstVisitor2
15+
{
16+
public List<SymbolReference> SymbolReferences { get; private set; }
17+
18+
public FindSymbolsVisitor()
19+
{
20+
this.SymbolReferences = new List<SymbolReference>();
21+
}
22+
23+
/// <summary>
24+
/// Adds each function defintion as a
25+
/// </summary>
26+
/// <param name="functionDefinitionAst">A functionDefinitionAst object in the script's AST</param>
27+
/// <returns>A decision to stop searching if the right symbol was found,
28+
/// or a decision to continue if it wasn't found</returns>
29+
public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst)
30+
{
31+
IScriptExtent nameExtent = new ScriptExtent() {
32+
Text = functionDefinitionAst.Name,
33+
StartLineNumber = functionDefinitionAst.Extent.StartLineNumber,
34+
EndLineNumber = functionDefinitionAst.Extent.EndLineNumber,
35+
StartColumnNumber = functionDefinitionAst.Extent.StartColumnNumber,
36+
EndColumnNumber = functionDefinitionAst.Extent.EndColumnNumber
37+
};
38+
39+
SymbolType symbolType =
40+
functionDefinitionAst.IsWorkflow ?
41+
SymbolType.Workflow : SymbolType.Function;
42+
43+
this.SymbolReferences.Add(
44+
new SymbolReference(
45+
symbolType,
46+
nameExtent));
47+
48+
return AstVisitAction.Continue;
49+
}
50+
51+
/// <summary>
52+
/// Checks to see if this variable expression is the symbol we are looking for.
53+
/// </summary>
54+
/// <param name="variableExpressionAst">A VariableExpressionAst object in the script's AST</param>
55+
/// <returns>A descion to stop searching if the right symbol was found,
56+
/// or a decision to continue if it wasn't found</returns>
57+
public override AstVisitAction VisitVariableExpression(VariableExpressionAst variableExpressionAst)
58+
{
59+
if (!IsAssignedAtScriptScope(variableExpressionAst))
60+
{
61+
return AstVisitAction.Continue;
62+
}
63+
64+
this.SymbolReferences.Add(
65+
new SymbolReference(
66+
SymbolType.Variable,
67+
variableExpressionAst.Extent));
68+
69+
return AstVisitAction.Continue;
70+
}
71+
72+
public override AstVisitAction VisitConfigurationDefinition(ConfigurationDefinitionAst configurationDefinitionAst)
73+
{
74+
IScriptExtent nameExtent = new ScriptExtent() {
75+
Text = configurationDefinitionAst.InstanceName.Extent.Text,
76+
StartLineNumber = configurationDefinitionAst.Extent.StartLineNumber,
77+
EndLineNumber = configurationDefinitionAst.Extent.EndLineNumber,
78+
StartColumnNumber = configurationDefinitionAst.Extent.StartColumnNumber,
79+
EndColumnNumber = configurationDefinitionAst.Extent.EndColumnNumber
80+
};
81+
82+
this.SymbolReferences.Add(
83+
new SymbolReference(
84+
SymbolType.Configuration,
85+
nameExtent));
86+
87+
return AstVisitAction.Continue;
88+
}
89+
90+
private bool IsAssignedAtScriptScope(VariableExpressionAst variableExpressionAst)
91+
{
92+
Ast parent = variableExpressionAst.Parent;
93+
if (!(parent is AssignmentStatementAst))
94+
{
95+
return false;
96+
}
97+
98+
parent = parent.Parent;
99+
if (parent == null || parent.Parent == null || parent.Parent.Parent == null)
100+
{
101+
return true;
102+
}
103+
104+
return false;
105+
}
106+
}
107+
}

src/PowerShellEditorServices/Language/LanguageService.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,32 @@ public async Task<SymbolDetails> FindSymbolDetailsAtLocation(
203203
return symbolDetails;
204204
}
205205

206+
/// <summary>
207+
/// Finds all the symbols in a file.
208+
/// </summary>
209+
/// <param name="scriptFile">The ScriptFile in which the symbol can be located.</param>
210+
/// <returns></returns>
211+
public FindOccurrencesResult FindSymbolsInFile(ScriptFile scriptFile)
212+
{
213+
Validate.IsNotNull("scriptFile", scriptFile);
214+
215+
IEnumerable<SymbolReference> symbolReferencesinFile =
216+
AstOperations
217+
.FindSymbolsInDocument(scriptFile.ScriptAst)
218+
.Select(
219+
reference => {
220+
reference.SourceLine =
221+
scriptFile.GetLine(reference.ScriptRegion.StartLineNumber);
222+
reference.FilePath = scriptFile.FilePath;
223+
return reference;
224+
});
225+
226+
return
227+
new FindOccurrencesResult {
228+
FoundOccurrences = symbolReferencesinFile
229+
};
230+
}
231+
206232
/// <summary>
207233
/// Finds all the references of a symbol
208234
/// </summary>

src/PowerShellEditorServices/Language/SymbolType.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,17 @@ public enum SymbolType
2828
/// <summary>
2929
/// The symbol is a parameter
3030
/// </summary>
31-
Parameter
32-
}
31+
Parameter,
32+
33+
/// <summary>
34+
/// The symbol is a DSC configuration
35+
/// </summary>
36+
Configuration,
3337

38+
/// <summary>
39+
/// The symbol is a workflow
40+
/// </summary>
41+
Workflow,
42+
}
3443
}
3544

src/PowerShellEditorServices/PowerShellEditorServices.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
<Compile Include="Language\FindOccurrencesResult.cs" />
7474
<Compile Include="Language\FindReferencesResult.cs" />
7575
<Compile Include="Language\FindReferencesVisitor.cs" />
76+
<Compile Include="Language\FindSymbolsVisitor.cs" />
7677
<Compile Include="Language\FindSymbolVisitor.cs" />
7778
<Compile Include="Language\GetDefinitionResult.cs" />
7879
<Compile Include="Language\LanguageService.cs" />

test/PowerShellEditorServices.Test.Shared/PowerShellEditorServices.Test.Shared.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,17 @@
5555
<Compile Include="References\FindsReferencesOnFunctionMultiFileDotSource.cs" />
5656
<Compile Include="References\FindsReferencesOnVariable.cs" />
5757
<Compile Include="SymbolDetails\FindsDetailsForBuiltInCommand.cs" />
58+
<Compile Include="Symbols\FindSymbolsInMultiSymbolFile.cs" />
59+
<Compile Include="Symbols\FindSymbolsInNoSymbolsFile.cs" />
5860
</ItemGroup>
5961
<ItemGroup>
6062
<None Include="Completion\CompletionExamples.psm1">
6163
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
6264
</None>
6365
<None Include="Debugging\VariableTest.ps1" />
6466
<None Include="SymbolDetails\SymbolDetails.ps1" />
67+
<None Include="Symbols\MultipleSymbols.ps1" />
68+
<None Include="Symbols\NoSymbols.ps1" />
6569
</ItemGroup>
6670
<ItemGroup>
6771
<ProjectReference Include="..\..\src\PowerShellEditorServices\PowerShellEditorServices.csproj">
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
//
4+
5+
namespace Microsoft.PowerShell.EditorServices.Test.Shared.Symbols
6+
{
7+
public class FindSymbolsInMultiSymbolFile
8+
{
9+
public static readonly ScriptRegion SourceDetails =
10+
new ScriptRegion {
11+
File = @"Symbols\MultipleSymbols.ps1"
12+
};
13+
}
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
//
4+
5+
namespace Microsoft.PowerShell.EditorServices.Test.Shared.Symbols
6+
{
7+
public class FindSymbolsInNoSymbolsFile
8+
{
9+
public static readonly ScriptRegion SourceDetails =
10+
new ScriptRegion {
11+
File = @"Symbols\NoSymbols.ps1"
12+
};
13+
}
14+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
$Global:GlobalVar = 0
2+
$UnqualifiedScriptVar = 1
3+
$Script:ScriptVar2 = 2
4+
5+
"`$Script:ScriptVar2 is $Script:ScriptVar2"
6+
7+
function AFunction {}
8+
9+
filter AFilter {$_}
10+
11+
function AnAdvancedFunction {
12+
begin {
13+
$LocalVar = 'LocalVar'
14+
function ANestedFunction() {
15+
$nestedVar = 42
16+
"`$nestedVar is $nestedVar"
17+
}
18+
}
19+
process {}
20+
end {}
21+
}
22+
23+
workflow AWorkflow {}
24+
25+
Configuration AConfiguration {
26+
Node "TEST-PC" {}
27+
}
28+
29+
AFunction
30+
1..3 | AFilter
31+
AnAdvancedFunction
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# This file represents a script with no symbols

test/PowerShellEditorServices.Test/Language/LanguageServiceTests.cs

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using Microsoft.PowerShell.EditorServices.Test.Shared.ParameterHint;
1010
using Microsoft.PowerShell.EditorServices.Test.Shared.References;
1111
using Microsoft.PowerShell.EditorServices.Test.Shared.SymbolDetails;
12+
using Microsoft.PowerShell.EditorServices.Test.Shared.Symbols;
1213
using System;
1314
using System.IO;
1415
using System.Linq;
@@ -224,7 +225,44 @@ await this.languageService.FindSymbolDetailsAtLocation(
224225
Assert.NotNull(symbolDetails.Documentation);
225226
Assert.NotEqual("", symbolDetails.Documentation);
226227
}
227-
228+
229+
[Fact]
230+
public void LanguageServiceFindsSymbolsInFile()
231+
{
232+
FindOccurrencesResult symbolsResult =
233+
this.FindSymbolsInFile(
234+
FindSymbolsInMultiSymbolFile.SourceDetails);
235+
236+
Assert.Equal(4, symbolsResult.FoundOccurrences.Where(r => r.SymbolType == SymbolType.Function).Count());
237+
Assert.Equal(3, symbolsResult.FoundOccurrences.Where(r => r.SymbolType == SymbolType.Variable).Count());
238+
Assert.Equal(1, symbolsResult.FoundOccurrences.Where(r => r.SymbolType == SymbolType.Workflow).Count());
239+
240+
SymbolReference firstFunctionSymbol = symbolsResult.FoundOccurrences.Where(r => r.SymbolType == SymbolType.Function).First();
241+
Assert.Equal("AFunction", firstFunctionSymbol.SymbolName);
242+
Assert.Equal(7, firstFunctionSymbol.ScriptRegion.StartLineNumber);
243+
Assert.Equal(1, firstFunctionSymbol.ScriptRegion.StartColumnNumber);
244+
245+
SymbolReference lastVariableSymbol = symbolsResult.FoundOccurrences.Where(r => r.SymbolType == SymbolType.Variable).Last();
246+
Assert.Equal("$Script:ScriptVar2", lastVariableSymbol.SymbolName);
247+
Assert.Equal(3, lastVariableSymbol.ScriptRegion.StartLineNumber);
248+
Assert.Equal(1, lastVariableSymbol.ScriptRegion.StartColumnNumber);
249+
250+
SymbolReference firstWorkflowSymbol = symbolsResult.FoundOccurrences.Where(r => r.SymbolType == SymbolType.Workflow).First();
251+
Assert.Equal("AWorkflow", firstWorkflowSymbol.SymbolName);
252+
Assert.Equal(23, firstWorkflowSymbol.ScriptRegion.StartLineNumber);
253+
Assert.Equal(1, firstWorkflowSymbol.ScriptRegion.StartColumnNumber);
254+
}
255+
256+
[Fact]
257+
public void LanguageServiceFindsSymbolsInNoSymbolsFile()
258+
{
259+
FindOccurrencesResult symbolsResult =
260+
this.FindSymbolsInFile(
261+
FindSymbolsInNoSymbolsFile.SourceDetails);
262+
263+
Assert.Equal(0, symbolsResult.FoundOccurrences.Count());
264+
}
265+
228266
private ScriptFile GetScriptFile(ScriptRegion scriptRegion)
229267
{
230268
const string baseSharedScriptPath =
@@ -304,5 +342,12 @@ private FindOccurrencesResult GetOccurrences(ScriptRegion scriptRegion)
304342
scriptRegion.StartLineNumber,
305343
scriptRegion.StartColumnNumber);
306344
}
345+
346+
private FindOccurrencesResult FindSymbolsInFile(ScriptRegion scriptRegion)
347+
{
348+
return
349+
this.languageService.FindSymbolsInFile(
350+
GetScriptFile(scriptRegion));
351+
}
307352
}
308353
}

0 commit comments

Comments
 (0)