Skip to content

Adding static analysis for help #1767

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 3, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions tools/StaticAnalysis/AppDomainHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// ----------------------------------------------------------------------------------
//
// Copyright Microsoft Corporation
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ----------------------------------------------------------------------------------

using System;

namespace StaticAnalysis
{
public static class AppDomainHelpers
{
/// <summary>
/// Create a new AppDomain and create a remote instance of AssemblyLoader we can use there
/// </summary>
/// <param name="directoryPath">directory containing assemblies</param>
/// <param name="testDomain">A new AppDomain, where assemblies can be loaded</param>
/// <returns>A proxy to the AssemblyLoader running in the newly created app domain</returns>
public static T CreateProxy<T>(string directoryPath, out AppDomain testDomain) where T:MarshalByRefObject
{
if (string.IsNullOrWhiteSpace(directoryPath))
{
throw new ArgumentException("directoryPath");
}

var setup = new AppDomainSetup();
setup.ApplicationBase = directoryPath;
setup.ApplicationName = "TestDomain";
setup.ApplicationTrust = AppDomain.CurrentDomain.ApplicationTrust;
setup.DisallowApplicationBaseProbing = false;
setup.DisallowCodeDownload = false;
setup.DisallowBindingRedirects = false;
setup.DisallowPublisherPolicy = false;
testDomain = AppDomain.CreateDomain("TestDomain", null, setup);
return testDomain.CreateInstanceFromAndUnwrap(typeof(T).Assembly.Location,
typeof(T).FullName) as T;
}
}
}
26 changes: 0 additions & 26 deletions tools/StaticAnalysis/DependencyAnalyzer/AssemblyLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,31 +71,5 @@ public AssemblyMetadata GetReflectedAssemblyFromFile(string assemblyPath)

return result;
}

/// <summary>
/// Create a new AppDomain and create a remote instance of AssemblyLoader we can use there
/// </summary>
/// <param name="directoryPath">directory containing assemblies</param>
/// <param name="testDomain">A new AppDomain, where assemblies can be loaded</param>
/// <returns>A proxy to the AssemblyLoader running in the newly created app domain</returns>
public static AssemblyLoader Create(string directoryPath, out AppDomain testDomain)
{
if (string.IsNullOrWhiteSpace(directoryPath))
{
throw new ArgumentException("directoryPath");
}

var setup = new AppDomainSetup();
setup.ApplicationBase = directoryPath;
setup.ApplicationName = "TestDomain";
setup.ApplicationTrust = AppDomain.CurrentDomain.ApplicationTrust;
setup.DisallowApplicationBaseProbing = false;
setup.DisallowCodeDownload = false;
setup.DisallowBindingRedirects = false;
setup.DisallowPublisherPolicy = false;
testDomain = AppDomain.CreateDomain("TestDomain", null, setup);
return testDomain.CreateInstanceFromAndUnwrap(typeof(AssemblyLoader).Assembly.Location,
typeof(AssemblyLoader).FullName) as AssemblyLoader;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ private void ProcessDirectory(string directoryPath)
{
var savedDirectory = Directory.GetCurrentDirectory();
Directory.SetCurrentDirectory(directoryPath);
_loader = AssemblyLoader.Create(directoryPath, out _testDomain);
_loader = AppDomainHelpers.CreateProxy<AssemblyLoader>(directoryPath, out _testDomain);
foreach (var file in Directory.GetFiles(directoryPath).Where(file => file.EndsWith(".dll")))
{
AssemblyRecord assembly = CreateAssemblyRecord(file);
Expand Down
32 changes: 32 additions & 0 deletions tools/StaticAnalysis/HelpAnalyzer/CmdletHelpMetadata.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// ----------------------------------------------------------------------------------
//
// Copyright Microsoft Corporation
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ----------------------------------------------------------------------------------

using System;

namespace StaticAnalysis.HelpAnalyzer
{
[Serializable]
public class CmdletHelpMetadata
{
/// <summary>
/// The cmdlet name
/// </summary>
public string CmdletName { get; set; }

/// <summary>
/// The class name implementing the cmdlet
/// </summary>
public string ClassName { get; set; }
}
}
85 changes: 85 additions & 0 deletions tools/StaticAnalysis/HelpAnalyzer/CmdletHelpParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// ----------------------------------------------------------------------------------
//
// Copyright Microsoft Corporation
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ----------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.IO;
using System.Xml.Linq;

namespace StaticAnalysis.HelpAnalyzer
{
/// <summary>
/// Parse the cmdlet help file
/// </summary>
public class CmdletHelpParser
{
public const string MamlSchemaUri = "http://schemas.microsoft.com/maml/2004/10";
public const string MamlDevSchemaUri = "http://schemas.microsoft.com/maml/dev/2004/10";
public const string CommandSchemaUri = "http://schemas.microsoft.com/maml/dev/command/2004/10";
public static IList<string> GetHelpTopics(string helpPath, ReportLogger<HelpIssue> logger)
{
IList<string> cmdlets = new List<string>();
try
{
XDocument document = XDocument.Parse(File.ReadAllText(helpPath));
var root = document.Root;
foreach (var command in root.GetChildElements("command"))
{
if (command.ContainsChildElement("details"))
{
var details = command.GetChildElement("details");
if (details.ContainsChildElement("name"))
{
cmdlets.Add(details.GetChildElement("name").Value.Trim());
}
else
{
logger.LogRecord(new HelpIssue
{
HelpFile = helpPath,
Severity = 0,
Description = string.Format("Missing command:name element for file {0}", helpPath),
Remediation = "Correct the xml format of the help file"
});

}
}
else
{

logger.LogRecord(new HelpIssue
{
HelpFile = helpPath,
Severity = 0,
Description = string.Format("Missing command:details element for file {0}", helpPath),
Remediation = "Correct the xml format of the help file"
});
}
}
}
catch (Exception e)
{
logger.LogRecord(new HelpIssue
{
HelpFile = helpPath,
Severity = 0,
Description = string.Format("Parsing error for help file {0}: {1}", helpPath, e.ToString()),
Remediation = "Correct the xml format of the help file"
});
}

return cmdlets;
}
}
}
57 changes: 57 additions & 0 deletions tools/StaticAnalysis/HelpAnalyzer/CmdletLoader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// ----------------------------------------------------------------------------------
//
// Copyright Microsoft Corporation
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ----------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Linq;
using System.Management.Automation;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using StaticAnalysis.help;

namespace StaticAnalysis.HelpAnalyzer
{
public class CmdletLoader : MarshalByRefObject
{
/// <summary>
/// Get cmdlets from the given assembly
/// </summary>
/// <param name="assemblyPath"></param>
/// <returns></returns>
public IList<CmdletHelpMetadata> GetCmdlets(string assemblyPath)
{
IList<CmdletHelpMetadata> result = new List<CmdletHelpMetadata>();
try
{
var assembly = Assembly.LoadFrom(assemblyPath);
foreach (var type in assembly.GetCmdletTypes())
{
var cmdlet = type.GetAttribute<CmdletAttribute>();
result.Add(
new CmdletHelpMetadata
{
ClassName = type.FullName,
CmdletName = string.Format("{0}-{1}", cmdlet.VerbName, cmdlet.NounName)
});
}
}
catch
{
}

return result;
}
}
}
103 changes: 103 additions & 0 deletions tools/StaticAnalysis/HelpAnalyzer/HelpAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// ----------------------------------------------------------------------------------
//
// Copyright Microsoft Corporation
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ----------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace StaticAnalysis.HelpAnalyzer
{
/// <summary>
/// Static analyzer for PowerShell Help
/// </summary>
public class HelpAnalyzer : IStaticAnalyzer
{
public HelpAnalyzer()
{
Name = "Help Analyzer";
}
public AnalysisLogger Logger { get; set; }
public string Name { get; private set; }

private AppDomain _appDomain;

/// <summary>
/// Given a set of directory paths containing PowerShell module folders, analyze the help
/// in the module folders and report any issues
/// </summary>
/// <param name="scopes"></param>
public void Analyze(IEnumerable<string> scopes)
{
var savedDirectory = Directory.GetCurrentDirectory();
var processedHelpFiles = new List<string>();
var helpLogger = Logger.CreateLogger<HelpIssue>("HelpIssues.csv");
foreach (var baseDirectory in scopes.Where(s => Directory.Exists(Path.GetFullPath(s))))
{
foreach (var directory in Directory.EnumerateDirectories(Path.GetFullPath(baseDirectory)))
{
var helpFiles = Directory.EnumerateFiles(directory, "*.dll-Help.xml")
.Where(f => !processedHelpFiles.Contains(Path.GetFileName(f),
StringComparer.OrdinalIgnoreCase)).ToList();
if (helpFiles.Any())
{
Directory.SetCurrentDirectory(directory);
foreach (var helpFile in helpFiles)
{
var cmdletFile = helpFile.Substring(0, helpFile.Length - "-Help.xml".Length);
var helpFileName = Path.GetFileName(helpFile);
var cmdletFileName = Path.GetFileName(cmdletFile);
if (File.Exists(cmdletFile) )
{
processedHelpFiles.Add(helpFileName);
helpLogger.Decorator.AddDecorator((h) =>
{
h.HelpFile = helpFileName;
h.Assembly = cmdletFileName;
}, "Cmdlet");
var proxy = AppDomainHelpers.CreateProxy<CmdletLoader>(directory, out _appDomain);
var cmdlets = proxy.GetCmdlets(cmdletFile);
var helpRecords = CmdletHelpParser.GetHelpTopics(helpFile, helpLogger);
ValidateHelpRecords(cmdlets, helpRecords, helpLogger);
helpLogger.Decorator.Remove("Cmdlet");
AppDomain.Unload(_appDomain);
}
}

Directory.SetCurrentDirectory(savedDirectory);
}
}
}
}

private void ValidateHelpRecords(IList<CmdletHelpMetadata> cmdlets, IList<string> helpRecords,
ReportLogger<HelpIssue> helpLogger)
{
foreach (var cmdlet in cmdlets)
{
if (!helpRecords.Contains(cmdlet.CmdletName, StringComparer.OrdinalIgnoreCase))
{
helpLogger.LogRecord(new HelpIssue
{
Target = cmdlet.ClassName,
Severity = 1,
Description = string.Format("Help missing for cmdlet {0} implemented by class {1}",
cmdlet.CmdletName, cmdlet.ClassName),
Remediation = string.Format("Add Help record for cmdlet {0} to help file.", cmdlet.CmdletName)
});
}
}
}
}
}
Loading