Repository navigation
Expand file tree
/
Copy pathCompatibilityRule.cs
More file actions
276 lines (240 loc) · 11.1 KB
/
Copy pathCompatibilityRule.cs
File metadata and controls
276 lines (240 loc) · 11.1 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
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Management.Automation.Language;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.PowerShell.CrossCompatibility.Query;
using Microsoft.PowerShell.CrossCompatibility.Retrieval;
using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic;
namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules
{
/// <summary>
/// Convenient work-sharing base class for PSSA rules that require compatiblity profiles.
/// </summary>
public abstract class CompatibilityRule : ConfigurableRule
{
// The name of the directory where compatibility profiles are looked for by default.
private const string PROFILE_DIR_NAME = "compatibility_profiles";
// The full path of the directory where compatiblity profiles are looked for by default.
private static readonly string s_defaultProfileDirPath;
// Memoized path to the module root of PSScriptAnalyzer.
private static readonly Lazy<string> s_moduleRootDirPath;
// A regex to differentiate profiles without extensions (but with dots in the names)
private static readonly Regex s_falseProfileExtensionPattern = new Regex(
"\\d+_(core|framework)",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
static CompatibilityRule()
{
s_moduleRootDirPath = new Lazy<string>(() => GetModuleRootDirPath());
s_defaultProfileDirPath = Path.Combine(s_moduleRootDirPath.Value, PROFILE_DIR_NAME);
}
private readonly CompatibilityProfileLoader _profileLoader;
private DirectoryInfo _profileDir;
/// <summary>
/// Create a new compatibility rule with the default profile loading directory.
/// </summary>
protected CompatibilityRule()
: this(s_defaultProfileDirPath)
{
}
/// <summary>
/// Create a new compatiblity rule with the configured profile loading directory.
/// </summary>
/// <param name="profileDirPath">The directory to search for profiles in by default.</param>
protected CompatibilityRule(string profileDirPath)
{
_profileDir = new DirectoryInfo(profileDirPath);
_profileLoader = CompatibilityProfileLoader.StaticInstance;
}
/// <summary>
/// The profile names or filenames of profiles to load from the profile directory,
/// as well as absolute paths to other profiles.
/// </summary>
[ConfigurableRuleProperty(defaultValue: new string[] {})]
public string[] TargetProfiles { get; set; }
/// <summary>
/// Override property to set the directory where profiles
/// given by name will be looked for and loaded from.
/// </summary>
/// <remarks>
/// This property should default to s_defaultProfileDirPath,
/// but rather than use a default property, we are required to
/// specify a value in the attribute, which only allows constant strings.
/// We should fix the ConfigurableRule logic to change this.
/// </remarks>
[ConfigurableRuleProperty(defaultValue: "")]
public string ProfileDirPath { get; set; }
/// <summary>
/// The severity of diagnostics generated by this rule.
/// </summary>
public virtual DiagnosticSeverity DiagnosticSeverity => DiagnosticSeverity.Warning;
/// <summary>
/// Method to be override to generate the visitor for AST analysis.
/// The pattern suggested is to simply override this method for visitor creation
/// and let this parent class hook up the AST analysis call.
/// </summary>
/// <param name="fileName">The full path of the script being analyzed.</param>
/// <returns>An AST visitor that generates diagnostics for PowerShell compatibility.</returns>
protected abstract CompatibilityVisitor CreateVisitor(string fileName);
/// <summary>
/// Analyze a given ast and provide any warning diagnostics about possible compatiblity issues.
/// </summary>
/// <param name="ast">The PowerShell AST to analyze.</param>
/// <param name="fileName">The file path of the PowerShell script being analyzed.</param>
/// <returns>Any diagnostics detailing compatibility issues with the given AST.</returns>
public override IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
{
CompatibilityVisitor compatibilityVisitor = CreateVisitor(fileName);
ast.Visit(compatibilityVisitor);
return compatibilityVisitor.GetDiagnosticRecords();
}
/// <summary>
/// Gets the severity of this rule.
/// </summary>
public override RuleSeverity GetSeverity()
{
return RuleSeverity.Warning;
}
/// <summary>
/// Gets the source type of this rule.
/// </summary>
public override SourceType GetSourceType()
{
return SourceType.Builtin;
}
/// <summary>
/// Gets the name of the source of this rule.
/// </summary>
public override string GetSourceName()
{
return string.Format(CultureInfo.CurrentCulture, Strings.SourceName);
}
/// <summary>
/// Overrides the ConfigurableRule method to allow setting of a non-constant
/// profile directory.
/// </summary>
/// <param name="paramValueMap">The configuration values passed in to set the rule.</param>
public override void ConfigureRule(IDictionary<string, object> paramValueMap)
{
base.ConfigureRule(paramValueMap);
// Override the ProfileDirPath if one is given
if (string.IsNullOrEmpty(ProfileDirPath))
{
return;
}
_profileDir = new DirectoryInfo(ProfileDirPath.TrimEnd('/', '\\'));
}
/// <summary>
/// Call this method to load the compatibilty profiles configured for this rule.
/// </summary>
/// <returns>The any profile and a list of target profiles for this rule.</returns>
protected IEnumerable<CompatibilityProfileData> LoadCompatibilityProfiles(out CompatibilityProfileData unionProfile)
{
if (TargetProfiles == null)
{
throw new InvalidOperationException($"{nameof(TargetProfiles)} cannot be null");
}
if (TargetProfiles.Length == 0)
{
throw new InvalidOperationException($"{nameof(TargetProfiles)} cannot be empty");
}
return _profileLoader.GetProfilesWithUnion(_profileDir, TargetProfiles.Select(path => NormalizeProfileNameToAbsolutePath(path)), out unionProfile);
}
private string NormalizeProfileNameToAbsolutePath(string profileName)
{
// Reject null or empty paths
if (string.IsNullOrEmpty(profileName))
{
throw new ArgumentException($"{nameof(profileName)} cannot be null or empty");
}
// Accept absolute paths verbatim. There may be issues with paths like "/here" in Windows
if (Path.IsPathRooted(profileName))
{
return profileName;
}
// Reject relative paths
if (profileName.Contains("\\")
|| profileName.Contains("/")
|| profileName.Equals(".")
|| profileName.Equals(".."))
{
throw new ArgumentException($"Compatibility profile specified as '{profileName}'. Compatibility profiles cannot be specified by relative path.");
}
// Profiles might be given by pure name, in which case tack ".json" onto the end
string extension = Path.GetExtension(profileName);
if (string.IsNullOrEmpty(extension) || s_falseProfileExtensionPattern.IsMatch(extension))
{
profileName = profileName + ".json";
}
// Names get looked for in the known profile directory
return Path.Combine(_profileDir.FullName, profileName);
}
/// <summary>
/// Get the path of PSScriptAnalyzer on the file system.
/// </summary>
/// <returns>The absolute path of the PSScriptAnalyzer module root.</returns>
private static string GetModuleRootDirPath()
{
// Start from the directory containing the Rules DLL,
// which may be in the module root or in a child directory (ex: coreclr)
string asmDirLocation = Path.GetDirectoryName(typeof(CompatibilityRule).Assembly.Location);
// Search down the directory structure from the assembly location looking for the module root
// We may be in a versioned directory ("PSScriptAnalyzer/1.18.0" vs "PSScriptAnalyzer"), so can't search that way
// Instead we look for the PSSA module manifest
const string manifestName = "PSScriptAnalyzer.psd1";
// Look for PSScriptAnalyzer.psd1 next to the Rules DLL
string manifestPath = Path.Combine(asmDirLocation, manifestName);
if (File.Exists(manifestPath))
{
return asmDirLocation;
}
// Look for PSScriptAnalyzer.psd1 in the directory above the Rules DLL
string dirUpOneLevel = Path.GetDirectoryName(asmDirLocation);
manifestPath = Path.Combine(dirUpOneLevel, manifestName);
if (File.Exists(manifestPath))
{
return dirUpOneLevel;
}
// Unable to find the root of the module where it should be, so we give up
throw new FileNotFoundException("Unable to find the PSScriptAnalyzer module root");
}
}
/// <summary>
/// Base class for an AST visitor that generates diagnostics about compatibility with
/// target PowerShell runtimes, to be used in conjunction with the CompatibilityRule class.
/// </summary>
public abstract class CompatibilityVisitor : AstVisitor
{
public abstract IEnumerable<DiagnosticRecord> GetDiagnosticRecords();
}
/// <summary>
/// Base class for compatibility diagnostics with some assumptions baked in about
/// diagnostic severity and rule ID.
/// </summary>
public abstract class CompatibilityDiagnostic : DiagnosticRecord
{
protected CompatibilityDiagnostic(
string message,
IScriptExtent extent,
string ruleName,
string ruleId,
string analyzedFileName,
IEnumerable<CorrectionExtent> suggestedCorrections)
: base(
message,
extent,
ruleName,
DiagnosticSeverity.Warning,
analyzedFileName,
ruleId,
suggestedCorrections: suggestedCorrections)
{
}
}
}