Skip to content

Commit 096dc34

Browse files
author
Hovsep
committed
Merge pull request Azure#1767 from markcowl/helpanalyzer
Adding static analysis for help
2 parents e84e613 + 49ce01e commit 096dc34

13 files changed

+600
-32
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// ----------------------------------------------------------------------------------
2+
//
3+
// Copyright Microsoft Corporation
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
// ----------------------------------------------------------------------------------
14+
15+
using System;
16+
17+
namespace StaticAnalysis
18+
{
19+
public static class AppDomainHelpers
20+
{
21+
/// <summary>
22+
/// Create a new AppDomain and create a remote instance of AssemblyLoader we can use there
23+
/// </summary>
24+
/// <param name="directoryPath">directory containing assemblies</param>
25+
/// <param name="testDomain">A new AppDomain, where assemblies can be loaded</param>
26+
/// <returns>A proxy to the AssemblyLoader running in the newly created app domain</returns>
27+
public static T CreateProxy<T>(string directoryPath, out AppDomain testDomain) where T:MarshalByRefObject
28+
{
29+
if (string.IsNullOrWhiteSpace(directoryPath))
30+
{
31+
throw new ArgumentException("directoryPath");
32+
}
33+
34+
var setup = new AppDomainSetup();
35+
setup.ApplicationBase = directoryPath;
36+
setup.ApplicationName = "TestDomain";
37+
setup.ApplicationTrust = AppDomain.CurrentDomain.ApplicationTrust;
38+
setup.DisallowApplicationBaseProbing = false;
39+
setup.DisallowCodeDownload = false;
40+
setup.DisallowBindingRedirects = false;
41+
setup.DisallowPublisherPolicy = false;
42+
testDomain = AppDomain.CreateDomain("TestDomain", null, setup);
43+
return testDomain.CreateInstanceFromAndUnwrap(typeof(T).Assembly.Location,
44+
typeof(T).FullName) as T;
45+
}
46+
}
47+
}

tools/StaticAnalysis/DependencyAnalyzer/AssemblyLoader.cs

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -71,31 +71,5 @@ public AssemblyMetadata GetReflectedAssemblyFromFile(string assemblyPath)
7171

7272
return result;
7373
}
74-
75-
/// <summary>
76-
/// Create a new AppDomain and create a remote instance of AssemblyLoader we can use there
77-
/// </summary>
78-
/// <param name="directoryPath">directory containing assemblies</param>
79-
/// <param name="testDomain">A new AppDomain, where assemblies can be loaded</param>
80-
/// <returns>A proxy to the AssemblyLoader running in the newly created app domain</returns>
81-
public static AssemblyLoader Create(string directoryPath, out AppDomain testDomain)
82-
{
83-
if (string.IsNullOrWhiteSpace(directoryPath))
84-
{
85-
throw new ArgumentException("directoryPath");
86-
}
87-
88-
var setup = new AppDomainSetup();
89-
setup.ApplicationBase = directoryPath;
90-
setup.ApplicationName = "TestDomain";
91-
setup.ApplicationTrust = AppDomain.CurrentDomain.ApplicationTrust;
92-
setup.DisallowApplicationBaseProbing = false;
93-
setup.DisallowCodeDownload = false;
94-
setup.DisallowBindingRedirects = false;
95-
setup.DisallowPublisherPolicy = false;
96-
testDomain = AppDomain.CreateDomain("TestDomain", null, setup);
97-
return testDomain.CreateInstanceFromAndUnwrap(typeof(AssemblyLoader).Assembly.Location,
98-
typeof(AssemblyLoader).FullName) as AssemblyLoader;
99-
}
10074
}
10175
}

tools/StaticAnalysis/DependencyAnalyzer/DependencyAnalyzer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ private void ProcessDirectory(string directoryPath)
209209
{
210210
var savedDirectory = Directory.GetCurrentDirectory();
211211
Directory.SetCurrentDirectory(directoryPath);
212-
_loader = AssemblyLoader.Create(directoryPath, out _testDomain);
212+
_loader = AppDomainHelpers.CreateProxy<AssemblyLoader>(directoryPath, out _testDomain);
213213
foreach (var file in Directory.GetFiles(directoryPath).Where(file => file.EndsWith(".dll")))
214214
{
215215
AssemblyRecord assembly = CreateAssemblyRecord(file);
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// ----------------------------------------------------------------------------------
2+
//
3+
// Copyright Microsoft Corporation
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
// ----------------------------------------------------------------------------------
14+
15+
using System;
16+
17+
namespace StaticAnalysis.HelpAnalyzer
18+
{
19+
[Serializable]
20+
public class CmdletHelpMetadata
21+
{
22+
/// <summary>
23+
/// The cmdlet name
24+
/// </summary>
25+
public string CmdletName { get; set; }
26+
27+
/// <summary>
28+
/// The class name implementing the cmdlet
29+
/// </summary>
30+
public string ClassName { get; set; }
31+
}
32+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// ----------------------------------------------------------------------------------
2+
//
3+
// Copyright Microsoft Corporation
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
// ----------------------------------------------------------------------------------
14+
15+
using System;
16+
using System.Collections.Generic;
17+
using System.IO;
18+
using System.Xml.Linq;
19+
20+
namespace StaticAnalysis.HelpAnalyzer
21+
{
22+
/// <summary>
23+
/// Parse the cmdlet help file
24+
/// </summary>
25+
public class CmdletHelpParser
26+
{
27+
public const string MamlSchemaUri = "http://schemas.microsoft.com/maml/2004/10";
28+
public const string MamlDevSchemaUri = "http://schemas.microsoft.com/maml/dev/2004/10";
29+
public const string CommandSchemaUri = "http://schemas.microsoft.com/maml/dev/command/2004/10";
30+
public static IList<string> GetHelpTopics(string helpPath, ReportLogger<HelpIssue> logger)
31+
{
32+
IList<string> cmdlets = new List<string>();
33+
try
34+
{
35+
XDocument document = XDocument.Parse(File.ReadAllText(helpPath));
36+
var root = document.Root;
37+
foreach (var command in root.GetChildElements("command"))
38+
{
39+
if (command.ContainsChildElement("details"))
40+
{
41+
var details = command.GetChildElement("details");
42+
if (details.ContainsChildElement("name"))
43+
{
44+
cmdlets.Add(details.GetChildElement("name").Value.Trim());
45+
}
46+
else
47+
{
48+
logger.LogRecord(new HelpIssue
49+
{
50+
HelpFile = helpPath,
51+
Severity = 0,
52+
Description = string.Format("Missing command:name element for file {0}", helpPath),
53+
Remediation = "Correct the xml format of the help file"
54+
});
55+
56+
}
57+
}
58+
else
59+
{
60+
61+
logger.LogRecord(new HelpIssue
62+
{
63+
HelpFile = helpPath,
64+
Severity = 0,
65+
Description = string.Format("Missing command:details element for file {0}", helpPath),
66+
Remediation = "Correct the xml format of the help file"
67+
});
68+
}
69+
}
70+
}
71+
catch (Exception e)
72+
{
73+
logger.LogRecord(new HelpIssue
74+
{
75+
HelpFile = helpPath,
76+
Severity = 0,
77+
Description = string.Format("Parsing error for help file {0}: {1}", helpPath, e.ToString()),
78+
Remediation = "Correct the xml format of the help file"
79+
});
80+
}
81+
82+
return cmdlets;
83+
}
84+
}
85+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// ----------------------------------------------------------------------------------
2+
//
3+
// Copyright Microsoft Corporation
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
// ----------------------------------------------------------------------------------
14+
15+
using System;
16+
using System.Collections.Generic;
17+
using System.Linq;
18+
using System.Management.Automation;
19+
using System.Reflection;
20+
using System.Text;
21+
using System.Threading.Tasks;
22+
using StaticAnalysis.help;
23+
24+
namespace StaticAnalysis.HelpAnalyzer
25+
{
26+
public class CmdletLoader : MarshalByRefObject
27+
{
28+
/// <summary>
29+
/// Get cmdlets from the given assembly
30+
/// </summary>
31+
/// <param name="assemblyPath"></param>
32+
/// <returns></returns>
33+
public IList<CmdletHelpMetadata> GetCmdlets(string assemblyPath)
34+
{
35+
IList<CmdletHelpMetadata> result = new List<CmdletHelpMetadata>();
36+
try
37+
{
38+
var assembly = Assembly.LoadFrom(assemblyPath);
39+
foreach (var type in assembly.GetCmdletTypes())
40+
{
41+
var cmdlet = type.GetAttribute<CmdletAttribute>();
42+
result.Add(
43+
new CmdletHelpMetadata
44+
{
45+
ClassName = type.FullName,
46+
CmdletName = string.Format("{0}-{1}", cmdlet.VerbName, cmdlet.NounName)
47+
});
48+
}
49+
}
50+
catch
51+
{
52+
}
53+
54+
return result;
55+
}
56+
}
57+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// ----------------------------------------------------------------------------------
2+
//
3+
// Copyright Microsoft Corporation
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
// ----------------------------------------------------------------------------------
14+
15+
using System;
16+
using System.Collections.Generic;
17+
using System.IO;
18+
using System.Linq;
19+
20+
namespace StaticAnalysis.HelpAnalyzer
21+
{
22+
/// <summary>
23+
/// Static analyzer for PowerShell Help
24+
/// </summary>
25+
public class HelpAnalyzer : IStaticAnalyzer
26+
{
27+
public HelpAnalyzer()
28+
{
29+
Name = "Help Analyzer";
30+
}
31+
public AnalysisLogger Logger { get; set; }
32+
public string Name { get; private set; }
33+
34+
private AppDomain _appDomain;
35+
36+
/// <summary>
37+
/// Given a set of directory paths containing PowerShell module folders, analyze the help
38+
/// in the module folders and report any issues
39+
/// </summary>
40+
/// <param name="scopes"></param>
41+
public void Analyze(IEnumerable<string> scopes)
42+
{
43+
var savedDirectory = Directory.GetCurrentDirectory();
44+
var processedHelpFiles = new List<string>();
45+
var helpLogger = Logger.CreateLogger<HelpIssue>("HelpIssues.csv");
46+
foreach (var baseDirectory in scopes.Where(s => Directory.Exists(Path.GetFullPath(s))))
47+
{
48+
foreach (var directory in Directory.EnumerateDirectories(Path.GetFullPath(baseDirectory)))
49+
{
50+
var helpFiles = Directory.EnumerateFiles(directory, "*.dll-Help.xml")
51+
.Where(f => !processedHelpFiles.Contains(Path.GetFileName(f),
52+
StringComparer.OrdinalIgnoreCase)).ToList();
53+
if (helpFiles.Any())
54+
{
55+
Directory.SetCurrentDirectory(directory);
56+
foreach (var helpFile in helpFiles)
57+
{
58+
var cmdletFile = helpFile.Substring(0, helpFile.Length - "-Help.xml".Length);
59+
var helpFileName = Path.GetFileName(helpFile);
60+
var cmdletFileName = Path.GetFileName(cmdletFile);
61+
if (File.Exists(cmdletFile) )
62+
{
63+
processedHelpFiles.Add(helpFileName);
64+
helpLogger.Decorator.AddDecorator((h) =>
65+
{
66+
h.HelpFile = helpFileName;
67+
h.Assembly = cmdletFileName;
68+
}, "Cmdlet");
69+
var proxy = AppDomainHelpers.CreateProxy<CmdletLoader>(directory, out _appDomain);
70+
var cmdlets = proxy.GetCmdlets(cmdletFile);
71+
var helpRecords = CmdletHelpParser.GetHelpTopics(helpFile, helpLogger);
72+
ValidateHelpRecords(cmdlets, helpRecords, helpLogger);
73+
helpLogger.Decorator.Remove("Cmdlet");
74+
AppDomain.Unload(_appDomain);
75+
}
76+
}
77+
78+
Directory.SetCurrentDirectory(savedDirectory);
79+
}
80+
}
81+
}
82+
}
83+
84+
private void ValidateHelpRecords(IList<CmdletHelpMetadata> cmdlets, IList<string> helpRecords,
85+
ReportLogger<HelpIssue> helpLogger)
86+
{
87+
foreach (var cmdlet in cmdlets)
88+
{
89+
if (!helpRecords.Contains(cmdlet.CmdletName, StringComparer.OrdinalIgnoreCase))
90+
{
91+
helpLogger.LogRecord(new HelpIssue
92+
{
93+
Target = cmdlet.ClassName,
94+
Severity = 1,
95+
Description = string.Format("Help missing for cmdlet {0} implemented by class {1}",
96+
cmdlet.CmdletName, cmdlet.ClassName),
97+
Remediation = string.Format("Add Help record for cmdlet {0} to help file.", cmdlet.CmdletName)
98+
});
99+
}
100+
}
101+
}
102+
}
103+
}

0 commit comments

Comments
 (0)