diff --git a/tools/StaticAnalysis/AppDomainHelpers.cs b/tools/StaticAnalysis/AppDomainHelpers.cs new file mode 100644 index 000000000000..cdf32c4cc754 --- /dev/null +++ b/tools/StaticAnalysis/AppDomainHelpers.cs @@ -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 + { + /// + /// Create a new AppDomain and create a remote instance of AssemblyLoader we can use there + /// + /// directory containing assemblies + /// A new AppDomain, where assemblies can be loaded + /// A proxy to the AssemblyLoader running in the newly created app domain + public static T CreateProxy(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; + } + } +} diff --git a/tools/StaticAnalysis/DependencyAnalyzer/AssemblyLoader.cs b/tools/StaticAnalysis/DependencyAnalyzer/AssemblyLoader.cs index 15cd6731a4c8..8b98a38c3f0f 100644 --- a/tools/StaticAnalysis/DependencyAnalyzer/AssemblyLoader.cs +++ b/tools/StaticAnalysis/DependencyAnalyzer/AssemblyLoader.cs @@ -71,31 +71,5 @@ public AssemblyMetadata GetReflectedAssemblyFromFile(string assemblyPath) return result; } - - /// - /// Create a new AppDomain and create a remote instance of AssemblyLoader we can use there - /// - /// directory containing assemblies - /// A new AppDomain, where assemblies can be loaded - /// A proxy to the AssemblyLoader running in the newly created app domain - 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; - } } } diff --git a/tools/StaticAnalysis/DependencyAnalyzer/DependencyAnalyzer.cs b/tools/StaticAnalysis/DependencyAnalyzer/DependencyAnalyzer.cs index 829b78e3e343..62f8ee2786ff 100644 --- a/tools/StaticAnalysis/DependencyAnalyzer/DependencyAnalyzer.cs +++ b/tools/StaticAnalysis/DependencyAnalyzer/DependencyAnalyzer.cs @@ -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(directoryPath, out _testDomain); foreach (var file in Directory.GetFiles(directoryPath).Where(file => file.EndsWith(".dll"))) { AssemblyRecord assembly = CreateAssemblyRecord(file); diff --git a/tools/StaticAnalysis/HelpAnalyzer/CmdletHelpMetadata.cs b/tools/StaticAnalysis/HelpAnalyzer/CmdletHelpMetadata.cs new file mode 100644 index 000000000000..6f2dc31d0f93 --- /dev/null +++ b/tools/StaticAnalysis/HelpAnalyzer/CmdletHelpMetadata.cs @@ -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 + { + /// + /// The cmdlet name + /// + public string CmdletName { get; set; } + + /// + /// The class name implementing the cmdlet + /// + public string ClassName { get; set; } + } +} diff --git a/tools/StaticAnalysis/HelpAnalyzer/CmdletHelpParser.cs b/tools/StaticAnalysis/HelpAnalyzer/CmdletHelpParser.cs new file mode 100644 index 000000000000..c1d7d1e0476f --- /dev/null +++ b/tools/StaticAnalysis/HelpAnalyzer/CmdletHelpParser.cs @@ -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 +{ + /// + /// Parse the cmdlet help file + /// + 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 GetHelpTopics(string helpPath, ReportLogger logger) + { + IList cmdlets = new List(); + 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; + } + } +} diff --git a/tools/StaticAnalysis/HelpAnalyzer/CmdletLoader.cs b/tools/StaticAnalysis/HelpAnalyzer/CmdletLoader.cs new file mode 100644 index 000000000000..4d79e1275739 --- /dev/null +++ b/tools/StaticAnalysis/HelpAnalyzer/CmdletLoader.cs @@ -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 + { + /// + /// Get cmdlets from the given assembly + /// + /// + /// + public IList GetCmdlets(string assemblyPath) + { + IList result = new List(); + try + { + var assembly = Assembly.LoadFrom(assemblyPath); + foreach (var type in assembly.GetCmdletTypes()) + { + var cmdlet = type.GetAttribute(); + result.Add( + new CmdletHelpMetadata + { + ClassName = type.FullName, + CmdletName = string.Format("{0}-{1}", cmdlet.VerbName, cmdlet.NounName) + }); + } + } + catch + { + } + + return result; + } + } +} diff --git a/tools/StaticAnalysis/HelpAnalyzer/HelpAnalyzer.cs b/tools/StaticAnalysis/HelpAnalyzer/HelpAnalyzer.cs new file mode 100644 index 000000000000..dbbeabfb237f --- /dev/null +++ b/tools/StaticAnalysis/HelpAnalyzer/HelpAnalyzer.cs @@ -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 +{ + /// + /// Static analyzer for PowerShell Help + /// + public class HelpAnalyzer : IStaticAnalyzer + { + public HelpAnalyzer() + { + Name = "Help Analyzer"; + } + public AnalysisLogger Logger { get; set; } + public string Name { get; private set; } + + private AppDomain _appDomain; + + /// + /// Given a set of directory paths containing PowerShell module folders, analyze the help + /// in the module folders and report any issues + /// + /// + public void Analyze(IEnumerable scopes) + { + var savedDirectory = Directory.GetCurrentDirectory(); + var processedHelpFiles = new List(); + var helpLogger = Logger.CreateLogger("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(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 cmdlets, IList helpRecords, + ReportLogger 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) + }); + } + } + } + } +} diff --git a/tools/StaticAnalysis/HelpAnalyzer/HelpIssue.cs b/tools/StaticAnalysis/HelpAnalyzer/HelpIssue.cs new file mode 100644 index 000000000000..a35c80aafd31 --- /dev/null +++ b/tools/StaticAnalysis/HelpAnalyzer/HelpIssue.cs @@ -0,0 +1,50 @@ +// ---------------------------------------------------------------------------------- +// +// 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; + + +namespace StaticAnalysis.HelpAnalyzer +{ + public class HelpIssue : IReportRecord + { + /// + /// The assembly containing the help issue + /// + public string Assembly { get; set; } + /// + /// The associated help file. + /// + public string HelpFile { get; set; } + /// + /// The target of the report (cmdlet name, etc..) + /// + public string Target { get; set; } + public string Description { get; set; } + public string Remediation { get; set; } + public int Severity { get; set; } + public string PrintHeaders() + { + return "\"Assembly\",\"HelpFile\",\"Target\",\"Severity\",\"Description\",\"Remediation\""; + } + + public string FormatRecord() + { + return string.Format("\"{0}\",\"{1}\",\"{2}\",\"{3}\",\"{4}\",\"{5}\"", + Assembly, HelpFile, Target, Severity, Description, Remediation); + } + } +} diff --git a/tools/StaticAnalysis/HelpAnalyzer/ReflectionExtensions.cs b/tools/StaticAnalysis/HelpAnalyzer/ReflectionExtensions.cs new file mode 100644 index 000000000000..0156997106b7 --- /dev/null +++ b/tools/StaticAnalysis/HelpAnalyzer/ReflectionExtensions.cs @@ -0,0 +1,64 @@ +// ---------------------------------------------------------------------------------- +// +// 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; + +namespace StaticAnalysis.help + +{ + public static class ReflectionExtensions + { + public static T GetAttribute(this Type decoratedType) where T : Attribute + { + return decoratedType.GetTypeInfo().GetCustomAttribute(typeof(T), true) as T; + } + + public static T GetAttribute(this PropertyInfo decoratedProperty) where T : Attribute + { + return decoratedProperty.GetCustomAttribute(typeof(T), true) as T; + } + + public static IEnumerable GetAttributes(this Type decoratedType) where T : Attribute + { + return decoratedType.GetTypeInfo().GetCustomAttributes(typeof(T), false).Select(a => a as T); + } + + public static IEnumerable GetAttributes(this PropertyInfo decoratedProeprty) where T : Attribute + { + return decoratedProeprty.GetCustomAttributes(typeof(T), false).Select(a => a as T); + } + + public static bool HasAttribute(this Type decoratedType) where T : Attribute + { + return decoratedType.GetTypeInfo().CustomAttributes.Any(d => d.AttributeType == typeof (T)); + + } + + public static bool HasAttribute(this PropertyInfo decoratedProperty) where T : Attribute + { + return decoratedProperty.CustomAttributes.Any(d => d.AttributeType == typeof(T)); + + } + + public static IEnumerable GetCmdletTypes(this Assembly assembly) + { + return assembly.GetTypes().Where(t => t.HasAttribute()); + } + + } +} diff --git a/tools/StaticAnalysis/HelpAnalyzer/XmlExtensions.cs b/tools/StaticAnalysis/HelpAnalyzer/XmlExtensions.cs new file mode 100644 index 000000000000..f2d562a48c3f --- /dev/null +++ b/tools/StaticAnalysis/HelpAnalyzer/XmlExtensions.cs @@ -0,0 +1,128 @@ +// ---------------------------------------------------------------------------------- +// +// 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.Xml.Linq; + +namespace StaticAnalysis +{ + public static class XmlExtensions + { + /// + /// Get all child elements with the given name + /// + /// The element to search + /// The child element name to search for + /// An enumeration of child elements with the given name, or null if none + /// are found. + public static IEnumerable GetChildElements(this XElement parent, string name) + { + return parent.Descendants().Where(e => String.Equals(e.Name.LocalName, name)); + } + + /// + /// Get all child elements with the given name that satisfy the given predicate + /// + /// The element to search + /// The child element name to search for + /// The additional condition to satisfy + /// An enumeration of child elements with the given name that satisfy the predicate, + /// or null if none are found. + public static IEnumerable GetChildElements(this XElement parent, string name, + Func predicate ) + { + return parent.Descendants().Where(e => String.Equals(e.Name.LocalName, name) + && predicate(e)); + } + + /// + /// Get the first child element with the given name + /// + /// The element to search + /// The child element name to search for + /// The child element with the given name, or null if none are found. + public static XElement GetChildElement(this XElement parent, string name) + { + return parent.Descendants().FirstOrDefault(e => String.Equals(e.Name.LocalName, name)); + } + + /// + /// Get the first child element with the given name that satisfies the given predicate + /// + /// The element to search + /// The child element name to search for + /// Additional conditions over the desired child element + /// + public static XElement GetChildElement(this XElement parent, string name, + Func predicate ) + { + return parent.Descendants().FirstOrDefault(e => String.Equals(e.Name.LocalName, name) + && predicate(e)); + } + + /// + /// Determines if the given element contains a child element of the given name + /// + /// The element to search + /// The child element name to search for + /// true if the given child element exists, otherwise false + public static bool ContainsChildElement(this XElement element, string name) + { + return element.Descendants().Any(e => string.Equals(e.Name.LocalName, name)); + } + + /// + /// Determines if the given element contains a child element of the given name that satisfies + /// the specified predicate + /// + /// The element to search + /// The child element name to search for + /// An additional condition on descendant elements + /// true if the given child element exists, otherwise false + public static bool ContainsChildElement(this XElement element, string name, + Func predicate) + { + return element.Descendants().Any(e => string.Equals(e.Name.LocalName, name) + && predicate(e)); + } + + /// + /// Determines if the given element contains a child element with any of the provided names + /// + /// The element to search + /// The child element names to search for + /// true if any of the given child elements exists, otherwise false + public static bool ContainsChildElement(this XElement element, IEnumerable names) + { + return names.Any(element.ContainsChildElement); + } + + /// + /// Determines if the given element contains a child element with any of the provided names that + /// satisfies the given predicate + /// + /// The element to search + /// The child element names to search for + /// An additional condition to check on descendant elements + /// True if any child element has the specified name and satisfies the specified + /// predicate. + public static bool ContainsChildElement(this XElement element, IEnumerable names, + Func predicate ) + { + return names.Any( n => element.ContainsChildElement(n, predicate)); + } + } +} diff --git a/tools/StaticAnalysis/Program.cs b/tools/StaticAnalysis/Program.cs index e4c94f164179..9f989039b0b1 100644 --- a/tools/StaticAnalysis/Program.cs +++ b/tools/StaticAnalysis/Program.cs @@ -16,13 +16,18 @@ using System.Collections.Generic; using System.IO; -namespace StaticAnalysis.DependencyAnalyzer +namespace StaticAnalysis { /// /// Runner for all static analysis tools. /// public class Program { + static readonly IList Analyzers = new List() + { + new HelpAnalyzer.HelpAnalyzer(), + new DependencyAnalyzer.DependencyAnalyzer() + }; public static void Main(string[] args) { if (args == null || args.Length < 1) @@ -58,11 +63,15 @@ public static void Main(string[] args) reportsDirectory); } - var analyzer = new DependencyAnalyzer { Logger = logger }; - logger.WriteMessage("Executing analyzer: {0}", analyzer.Name); - analyzer.Analyze(directories); + foreach (var analyzer in Analyzers) + { + analyzer.Logger = logger; + logger.WriteMessage("Executing analyzer: {0}", analyzer.Name); + analyzer.Analyze(directories); + logger.WriteMessage("Processing complete for analyzer: {0}", analyzer.Name); + } + logger.WriteReports(); - logger.WriteMessage("Processing complete for analyzer: {0}", analyzer.Name); } } } diff --git a/tools/StaticAnalysis/StaticAnalysis.csproj b/tools/StaticAnalysis/StaticAnalysis.csproj index cb94c8f63485..21a772b5b05c 100644 --- a/tools/StaticAnalysis/StaticAnalysis.csproj +++ b/tools/StaticAnalysis/StaticAnalysis.csproj @@ -34,6 +34,10 @@ + + packages\System.Management.Automation_PowerShell_3.0.6.3.9600.17400\lib\net40\System.Management.Automation.dll + True + @@ -42,6 +46,7 @@ + @@ -53,12 +58,22 @@ + + + + + + + + + +