-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathAstHelper.cs
More file actions
333 lines (277 loc) · 15.2 KB
/
AstHelper.cs
File metadata and controls
333 lines (277 loc) · 15.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
// Copyright (c) James Draycott. All Rights Reserved.
// Licensed under the GPL3 License, See LICENSE in the project root for license information.
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Management.Automation;
using System.Management.Automation.Language;
using System.Text;
using Compiler.Analyser;
using Compiler.Text;
using LanguageExt;
using LanguageExt.UnsafeValueAccess;
using NLog;
using Pastel;
namespace Compiler;
public static class AstHelper {
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
public static Dictionary<string, Dictionary<string, object>> FindDeclaredModules(Ast ast) {
var modules = new Dictionary<string, Dictionary<string, object>>();
foreach (var usingStatement in ast.FindAll(testAst => testAst is UsingStatementAst usingAst && usingAst.UsingStatementKind == UsingStatementKind.Module, true)) {
switch (usingStatement) {
case UsingStatementAst usingStatementAst when usingStatementAst.Name is not null:
modules.Add(usingStatementAst.Name.Value, new Dictionary<string, object> {
{ "AST", usingStatementAst }
});
break;
case UsingStatementAst usingStatementAst when usingStatementAst.ModuleSpecification is not null:
var table = new Dictionary<string, object>
{
{ "AST", usingStatementAst }
};
var pairs = usingStatementAst.ModuleSpecification.KeyValuePairs.GetEnumerator();
while (pairs.MoveNext()) {
var key = (string)pairs.Current.Item1.SafeGetValue();
var value = pairs.Current.Item2.SafeGetValue();
table.Add(key, value);
}
if (!table.TryGetValue("ModuleName", out var moduleName)) throw new InvalidDataException("ModuleSpecification does not contain a 'ModuleName' key.");
if (table.TryGetValue("Guid", out var guid)) table["Guid"] = Guid.Parse((string)guid);
foreach (var versionKey in new[] { "ModuleVersion", "MaximumVersion", "RequiredVersion" }) {
if (table.TryGetValue(versionKey, out var version)) table[versionKey] = Version.Parse((string)version);
}
modules.Add((string)moduleName, table);
break;
default:
Logger.Error($"Unknown UsingStatementAst type from: {usingStatement}");
break;
}
}
if (ast is ScriptBlockAst scriptBlockAst) {
if (scriptBlockAst.ScriptRequirements is null) return modules;
scriptBlockAst.ScriptRequirements.RequiredModules.ToList().ForEach(module => {
Logger.Debug($"Found required module: {module.Name}");
var table = new Dictionary<string, object>();
if (module.Version is not null) table.Add("ModuleVersion", module.Version);
if (module.MaximumVersion is not null) table.Add("MaximumVersion", module.MaximumVersion);
if (module.RequiredVersion is not null) table.Add("RequiredVersion", module.RequiredVersion);
if (module.Guid is not null) table.Add("Guid", module.Guid);
modules.TryAdd(module.Name, table);
});
}
return modules;
}
public static List<UsingStatementAst> FindDeclaredNamespaces(Ast ast) {
var namespaces = new List<UsingStatementAst>();
ast.FindAll(testAst => testAst is UsingStatementAst usingAst && usingAst.UsingStatementKind == UsingStatementKind.Namespace, true)
.Cast<UsingStatementAst>()
.ToList()
.ForEach(namespaces.Add);
return namespaces;
}
public static List<FunctionDefinitionAst> FindAvailableFunctions(Ast ast, bool onlyExported) {
var allDefinedFunctions = ast
.FindAll(testAst => testAst is FunctionDefinitionAst, true)
.Cast<FunctionDefinitionAst>()
.ToList();
// Short circuit so we don't have to do any more work if we are not filtering for only exported functions.
if (!onlyExported) return allDefinedFunctions;
return [.. GetWantedExports(ast, "Function", allDefinedFunctions, function => NameWithoutNamespace(function.Name))];
}
public static IEnumerable<string> FindAvailableAliases(Ast ast, bool onlyExported) {
var allAstFunctionCalls = ast
.FindAll(testAst => testAst is CommandAst commandAst && commandAst.CommandElements[0].Extent.Text is "Set-Alias" or "New-Alias", true)
.Cast<CommandAst>()
.ToList();
var allFunctionsWithAliases = ast
.FindAll(testAst => testAst is FunctionDefinitionAst, true)
.Cast<FunctionDefinitionAst>()
.Where(function => function.Body.ParamBlock != null)
.Where(functionDefinition => functionDefinition.Body.ParamBlock!.Attributes.Any(attribute => attribute.TypeName.GetReflectionType() == typeof(AliasAttribute)));
var availableAliases = new List<string>();
var attributeType = typeof(AliasAttribute);
availableAliases.AddRange(allFunctionsWithAliases.SelectMany(function => function.Body.ParamBlock.Attributes
.Where(attribute => attribute.TypeName.GetReflectionAttributeType() == attributeType)
.SelectMany(attribute => {
var aliasesObjects = new List<ExpressionAst>();
if (attribute.NamedArguments.Any(namedArg => namedArg.ArgumentName is "aliasNames")) {
aliasesObjects.Add(attribute.NamedArguments.First(namedArg => namedArg.ArgumentName is "aliasNames").Argument);
} else if (attribute.PositionalArguments.Count != 0) {
aliasesObjects.AddRange(attribute.PositionalArguments);
}
if (aliasesObjects is null) return [];
var aliases = new List<string>();
foreach (var aliasesObject in aliasesObjects) {
switch (aliasesObject) {
case ArrayLiteralAst arrayLiteralAst:
aliases.AddRange(arrayLiteralAst.Elements.Select(element => element.SafeGetValue()).Cast<string>());
break;
case StringConstantExpressionAst stringConstantAst:
aliases.Add(stringConstantAst.Value);
break;
default:
Logger.Error($"Unknown alias type: {aliasesObject}");
break;
}
}
return aliases;
})
));
availableAliases.AddRange(allAstFunctionCalls.SelectMany(static commandAst => commandAst.CommandElements
.OfType<CommandParameterAst>()
.Where(static param => param.ParameterName == "Name")
.Select(param => {
var value = param.Argument ?? commandAst.CommandElements[commandAst.CommandElements.IndexOf(param) + 1] as ExpressionAst;
return value switch {
StringConstantExpressionAst stringConstantAst => stringConstantAst.Value,
_ => null
};
})
.Where(static alias => !string.IsNullOrWhiteSpace(alias))
.Cast<string>()
// .Select(static ast => ast.Value)
));
if (!onlyExported) return availableAliases;
return GetWantedExports(ast, "Alias", availableAliases, NameWithoutNamespace);
}
private static IEnumerable<T> GetWantedExports<T>(Ast ast, string kind, IEnumerable<T> availableItems, Func<T, string> getName) {
var exportCommands = ast.FindAll(testAst =>
testAst is CommandAst commandAst && commandAst.CommandElements[0].Extent.Text == "Export-ModuleMember", true
).Cast<CommandAst>();
// If there are no Export-ModuleMember commands, we are exporting everything.
if (!exportCommands.Any()) {
return availableItems;
}
var wantingToExport = new List<string>();
foreach (var exportCommand in exportCommands) {
// TODO - Support unnamed export param eg `Export-ModuleMember *`
var namedParameters = exportCommand.CommandElements
.OfType<CommandParameterAst>()
.Where(param => param.ParameterName.Equals(kind, StringComparison.OrdinalIgnoreCase));
foreach (var namedParameter in namedParameters) {
var value = namedParameter.Argument
?? exportCommand.CommandElements[exportCommand.CommandElements.IndexOf(namedParameter) + 1] as ExpressionAst;
var objects = value switch {
StringConstantExpressionAst starAst when starAst.Value == "*" => availableItems.Select(getName),
StringConstantExpressionAst stringConstantAst => [stringConstantAst.Value],
ArrayLiteralAst arrayLiteralAst => arrayLiteralAst.Elements.Select(element => element.SafeGetValue()),
_ => [], // We don't know what to do with this value, so we will just ignore it.
};
wantingToExport.AddRange(objects.Cast<string>());
}
}
return availableItems.Where(item => wantingToExport.Contains(getName(item)));
}
/// <summary>
/// Parse the given content into an AST, reporting any errors.
/// </summary>
/// <param name="astContent">
/// The content to parse into the AST.
/// </param>
/// <param name="filePath">
/// The file path of the content, or None if it is not from a file.
/// </param>
/// <param name="ignoredErrors">
/// A list of ErrorIds to ignore if they are encountered.
/// </param>
/// <returns>
/// The AST of the content if it was successfully parsed.
/// </returns>
[return: NotNull]
public static Fin<ScriptBlockAst> GetAstReportingErrors(
[NotNull] string astContent,
[NotNull] Option<string> filePath,
[NotNull] IEnumerable<string> ignoredErrors,
out Token[] tokens
) {
ArgumentNullException.ThrowIfNull(astContent);
ArgumentNullException.ThrowIfNull(ignoredErrors);
var ast = Parser.ParseInput(astContent, filePath.ValueUnsafe(), out tokens, out var parserErrors);
parserErrors = [.. parserErrors.Where(error => !ignoredErrors.Contains(error.ErrorId))];
if (parserErrors.Length != 0) {
var issues = parserErrors.Select(error => Issue.Error(error.Message, error.Extent, ast));
var errors = Error.Many(issues.ToArray());
return new WrappedErrorWithDebuggableContent(None, astContent, errors);
}
return ast;
}
// TODO - ability to translate the virtual cleaned line numbers to the actual line numbers in the file.
[return: NotNull]
public static string GetPrettyAstError(
[NotNull] IScriptExtent extent,
[NotNull] Ast parentAst,
[NotNull] Option<string> message,
[NotNull] Option<string> realFilePath = default,
[NotNull] IssueSeverity severity = IssueSeverity.Error) {
ArgumentNullException.ThrowIfNull(extent);
ArgumentNullException.ThrowIfNull(parentAst);
var problemColour = severity switch {
IssueSeverity.Error => "#8b0000",
IssueSeverity.Warning => "#f9f1a5",
_ => "#808080"
};
var startingLine = extent.StartLineNumber;
var endingLine = extent.EndLineNumber;
var startingColumn = extent.StartColumnNumber - 1;
var endingColumn = extent.EndColumnNumber - 1;
var firstColumnIndent = Math.Max(endingLine.ToString(CultureInfo.InvariantCulture).Length + 1, 5);
var firstColumnIndentString = new string(' ', firstColumnIndent);
var colouredPipe = "|".Pastel(ConsoleColor.Cyan);
while (parentAst.Parent != null) {
parentAst = parentAst.Parent;
}
var extentRegion = parentAst.Extent.Text.Split('\n')[(startingLine - 1)..endingLine];
var printableLines = new string[extentRegion.Length];
for (var i = 0; i < extentRegion.Length; i++) {
var line = extentRegion[i];
line = i switch {
0 when i == extentRegion.Length - 1 => string.Concat(line[0..startingColumn], line[startingColumn..endingColumn].Pastel(problemColour), line[endingColumn..]),
0 => string.Concat(line[0..startingColumn], line[startingColumn..].Pastel(problemColour)),
var _ when i == extentRegion.Length - 1 => string.Concat(line[0..endingColumn].Pastel(problemColour), line[endingColumn..]),
_ => line.Pastel(problemColour)
};
var sb = new StringBuilder()
.Append((i + startingLine).ToString(CultureInfo.InvariantCulture).PadRight(firstColumnIndent).Pastel(ConsoleColor.Cyan))
.Append(colouredPipe)
.Append(' ')
.Append(line);
printableLines[i] = sb.ToString();
}
string errorPointer;
if (startingLine == endingLine) {
errorPointer = string.Concat([new(' ', startingColumn), new('~', endingColumn - startingColumn)]);
} else {
var squigleEndColumn = extentRegion.Max(line => line.TrimEnd().Length);
var leastWhitespaceBeforeText = extentRegion.Min(line => line.Length - line.TrimStart().Length);
errorPointer = string.Concat([new(' ', leastWhitespaceBeforeText), new('~', squigleEndColumn - leastWhitespaceBeforeText)]);
}
var fileName = realFilePath.UnwrapOrElse(() => parentAst.Extent.File is null ? "Unknown file" : parentAst.Extent.File);
var location = TextSpan.New(startingLine, startingColumn, endingLine, endingColumn).Unwrap(); // Safety: Extents should always be valid.
return $"""
{"File".PadRight(firstColumnIndent).Pastel(ConsoleColor.Cyan)}{colouredPipe} {fileName.Pastel(ConsoleColor.Gray)}
{"Where".PadRight(firstColumnIndent).Pastel(ConsoleColor.Cyan)}{colouredPipe} {location.ToString().Pastel(ConsoleColor.Gray)}
{string.Join('\n', printableLines)}
{firstColumnIndentString}{colouredPipe} {errorPointer.Pastel(problemColour)}
{firstColumnIndentString}{colouredPipe} {message.UnwrapOrElse(static () => "Unknown Error").Pastel(problemColour)}
""";
}
public static ParamBlockAst? FindClosestParamBlock(Ast ast) {
var parent = ast;
while (parent != null) {
if (parent is ScriptBlockAst scriptBlock && scriptBlock.ParamBlock != null) return scriptBlock.ParamBlock;
parent = parent.Parent;
}
return null;
}
[return: NotNullIfNotNull(nameof(ast))]
public static Ast FindRoot([NotNull] Ast ast) {
ArgumentNullException.ThrowIfNull(ast);
var parent = ast;
while (parent.Parent != null) {
parent = parent.Parent;
}
return parent;
}
[return: NotNull]
private static string NameWithoutNamespace(string name) => name.Contains(':') ? name.Split(':').Last() : name;
}