From d83a8fee0b1e226ecdb6c6766577f34dbcde1fbd Mon Sep 17 00:00:00 2001 From: Sven Date: Tue, 24 May 2022 13:46:00 +0200 Subject: [PATCH] Init Module Analyzer --- ModuleAnalyzer.sln | 43 --- README.md | 63 +++- .../Factory/DefaultCommandVisitorFactory.cs | 14 - .../Output/MermaidOutput.cs | 48 --- Source/ModuleAnalyzer.App/Program.cs | 25 -- .../PsModuleAnalyzer.App.csproj | 16 - .../Visitor/DefaultCommandVisitor.cs | 70 ----- .../Anaylzer/ModuleAnalyzer.cs | 62 ---- .../Anaylzer/ModuleAnalyzerBuilder.cs | 43 --- .../Factory/CommandVisitorFactory.cs | 10 - .../Interfaces/IAnalyzerOutput.cs | 9 - .../Model/IModuleCommand.cs | 15 - .../Model/ModuleCommand.cs | 73 ----- .../Model/ModuleCommandCall.cs | 24 -- .../Model/ModuleCommandParameter.cs | 24 -- .../Model/ModuleDefinition.cs | 143 --------- .../Model/ModuleExternalCommand.cs | 32 -- .../PsModuleAnalyzer.Core.csproj | 15 - .../Visitor/CommandVisitor.cs | 17 -- .../DgmlFormatter.cs | 59 ---- .../DgmlFormatterExtension.cs | 13 - src/.editorconfig | 273 ++++++++++++++++++ .../CommandParser/CommandLineOptions.cs | 18 ++ .../CommandParser/CommandLineParser.cs | 12 + .../PowerShell.ModuleAnalyzer.App.csproj | 20 ++ src/PowerShell.ModuleAnalyzer.App/Program.cs | 38 +++ .../Properties/launchSettings.json | 8 + .../Anaylzer/ModuleAnalyzer.cs | 90 ++++++ .../Anaylzer/ModuleAnalyzerBuilder.cs | 71 +++++ .../Exceptions/ModuleInvalidException.cs | 5 + .../Interfaces/IAnalyzerOutputFormatter.cs | 11 + .../Model/IModuleCommand.cs | 46 +++ .../Model/ModuleCommand.cs | 49 ++++ .../Model/ModuleCommandCall.cs | 17 ++ .../Model/ModuleCommandParameter.cs | 23 ++ .../Model/ModuleDefinition.cs | 194 +++++++++++++ .../Model/ModuleExternalCommand.cs | 35 +++ .../Model/ModuleNamespace.cs | 11 + .../Output/ShortStatFormatter.cs | 46 +++ .../PowerShell.ModuleAnalyzer.Core.csproj | 204 +++++++++++++ .../Runner/PowerShellRunner.cs | 38 +++ .../Visitor/ModuleCommandVisitor.cs | 16 + .../Visitor/ModuleCommandVisitorFactory.cs | 8 + .../Visitor/ModuleDefaultCommandVisitor.cs | 62 ++++ .../ModuleDefaultCommandVisitorFactory.cs | 11 + .../CsvCompactFormatter.cs | 75 +++++ .../CsvFormatter.cs | 57 ++++ .../CsvFormatterExtension.cs | 16 + .../DgmlFormatter.cs | 67 +++++ .../DgmlFormatterExtension.cs | 17 ++ .../DgmlLinkReferenceWeightAnalyzer.cs | 95 ++++++ .../JsonFormatter.cs | 31 ++ .../JsonFormatterExtension.cs | 16 + ...PowerShell.ModuleAnalyzer.Formatter.csproj | 3 +- src/PowerShell.ModuleAnalyzer.sln | 42 +++ 55 files changed, 1786 insertions(+), 757 deletions(-) delete mode 100644 ModuleAnalyzer.sln delete mode 100644 Source/ModuleAnalyzer.App/Factory/DefaultCommandVisitorFactory.cs delete mode 100644 Source/ModuleAnalyzer.App/Output/MermaidOutput.cs delete mode 100644 Source/ModuleAnalyzer.App/Program.cs delete mode 100644 Source/ModuleAnalyzer.App/PsModuleAnalyzer.App.csproj delete mode 100644 Source/ModuleAnalyzer.App/Visitor/DefaultCommandVisitor.cs delete mode 100644 Source/ModuleAnalyzer.Core/Anaylzer/ModuleAnalyzer.cs delete mode 100644 Source/ModuleAnalyzer.Core/Anaylzer/ModuleAnalyzerBuilder.cs delete mode 100644 Source/ModuleAnalyzer.Core/Factory/CommandVisitorFactory.cs delete mode 100644 Source/ModuleAnalyzer.Core/Interfaces/IAnalyzerOutput.cs delete mode 100644 Source/ModuleAnalyzer.Core/Model/IModuleCommand.cs delete mode 100644 Source/ModuleAnalyzer.Core/Model/ModuleCommand.cs delete mode 100644 Source/ModuleAnalyzer.Core/Model/ModuleCommandCall.cs delete mode 100644 Source/ModuleAnalyzer.Core/Model/ModuleCommandParameter.cs delete mode 100644 Source/ModuleAnalyzer.Core/Model/ModuleDefinition.cs delete mode 100644 Source/ModuleAnalyzer.Core/Model/ModuleExternalCommand.cs delete mode 100644 Source/ModuleAnalyzer.Core/PsModuleAnalyzer.Core.csproj delete mode 100644 Source/ModuleAnalyzer.Core/Visitor/CommandVisitor.cs delete mode 100644 Source/ModuleAnalyzer.DgmlFormatter/DgmlFormatter.cs delete mode 100644 Source/ModuleAnalyzer.DgmlFormatter/DgmlFormatterExtension.cs create mode 100644 src/.editorconfig create mode 100644 src/PowerShell.ModuleAnalyzer.App/CommandParser/CommandLineOptions.cs create mode 100644 src/PowerShell.ModuleAnalyzer.App/CommandParser/CommandLineParser.cs create mode 100644 src/PowerShell.ModuleAnalyzer.App/PowerShell.ModuleAnalyzer.App.csproj create mode 100644 src/PowerShell.ModuleAnalyzer.App/Program.cs create mode 100644 src/PowerShell.ModuleAnalyzer.App/Properties/launchSettings.json create mode 100644 src/PowerShell.ModuleAnalyzer.Core/Anaylzer/ModuleAnalyzer.cs create mode 100644 src/PowerShell.ModuleAnalyzer.Core/Anaylzer/ModuleAnalyzerBuilder.cs create mode 100644 src/PowerShell.ModuleAnalyzer.Core/Exceptions/ModuleInvalidException.cs create mode 100644 src/PowerShell.ModuleAnalyzer.Core/Interfaces/IAnalyzerOutputFormatter.cs create mode 100644 src/PowerShell.ModuleAnalyzer.Core/Model/IModuleCommand.cs create mode 100644 src/PowerShell.ModuleAnalyzer.Core/Model/ModuleCommand.cs create mode 100644 src/PowerShell.ModuleAnalyzer.Core/Model/ModuleCommandCall.cs create mode 100644 src/PowerShell.ModuleAnalyzer.Core/Model/ModuleCommandParameter.cs create mode 100644 src/PowerShell.ModuleAnalyzer.Core/Model/ModuleDefinition.cs create mode 100644 src/PowerShell.ModuleAnalyzer.Core/Model/ModuleExternalCommand.cs create mode 100644 src/PowerShell.ModuleAnalyzer.Core/Model/ModuleNamespace.cs create mode 100644 src/PowerShell.ModuleAnalyzer.Core/Output/ShortStatFormatter.cs create mode 100644 src/PowerShell.ModuleAnalyzer.Core/PowerShell.ModuleAnalyzer.Core.csproj create mode 100644 src/PowerShell.ModuleAnalyzer.Core/Runner/PowerShellRunner.cs create mode 100644 src/PowerShell.ModuleAnalyzer.Core/Visitor/ModuleCommandVisitor.cs create mode 100644 src/PowerShell.ModuleAnalyzer.Core/Visitor/ModuleCommandVisitorFactory.cs create mode 100644 src/PowerShell.ModuleAnalyzer.Core/Visitor/ModuleDefaultCommandVisitor.cs create mode 100644 src/PowerShell.ModuleAnalyzer.Core/Visitor/ModuleDefaultCommandVisitorFactory.cs create mode 100644 src/PowerShell.ModuleAnalyzer.Formatter/CsvCompactFormatter.cs create mode 100644 src/PowerShell.ModuleAnalyzer.Formatter/CsvFormatter.cs create mode 100644 src/PowerShell.ModuleAnalyzer.Formatter/CsvFormatterExtension.cs create mode 100644 src/PowerShell.ModuleAnalyzer.Formatter/DgmlFormatter.cs create mode 100644 src/PowerShell.ModuleAnalyzer.Formatter/DgmlFormatterExtension.cs create mode 100644 src/PowerShell.ModuleAnalyzer.Formatter/DgmlLinkReferenceWeightAnalyzer.cs create mode 100644 src/PowerShell.ModuleAnalyzer.Formatter/JsonFormatter.cs create mode 100644 src/PowerShell.ModuleAnalyzer.Formatter/JsonFormatterExtension.cs rename Source/ModuleAnalyzer.DgmlFormatter/PsModuleAnalyzer.DgmlFormatter.csproj => src/PowerShell.ModuleAnalyzer.Formatter/PowerShell.ModuleAnalyzer.Formatter.csproj (70%) create mode 100644 src/PowerShell.ModuleAnalyzer.sln diff --git a/ModuleAnalyzer.sln b/ModuleAnalyzer.sln deleted file mode 100644 index 572fdcc..0000000 --- a/ModuleAnalyzer.sln +++ /dev/null @@ -1,43 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.1.32210.238 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PsModuleAnalyzer.App", "Source\ModuleAnalyzer.App\PsModuleAnalyzer.App.csproj", "{9F832377-2197-4C1A-9D5E-DD490B190EC2}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PsModuleAnalyzer.Core", "Source\ModuleAnalyzer.Core\PsModuleAnalyzer.Core.csproj", "{71724629-BEAB-4573-99FF-198211505FF2}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PsModuleAnalyzer.DgmlFormatter", "Source\ModuleAnalyzer.DgmlFormatter\PsModuleAnalyzer.DgmlFormatter.csproj", "{B4DFC143-D3FA-4F46-9274-FB09C94F1228}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PsModuleAnaylzer.CSVFormatter", "..\PsModuleAnaylzer.CSV\PsModuleAnaylzer.CSVFormatter.csproj", "{DDF936E4-61F3-4199-A6C6-39A79A09BBD7}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {9F832377-2197-4C1A-9D5E-DD490B190EC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9F832377-2197-4C1A-9D5E-DD490B190EC2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9F832377-2197-4C1A-9D5E-DD490B190EC2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9F832377-2197-4C1A-9D5E-DD490B190EC2}.Release|Any CPU.Build.0 = Release|Any CPU - {71724629-BEAB-4573-99FF-198211505FF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {71724629-BEAB-4573-99FF-198211505FF2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {71724629-BEAB-4573-99FF-198211505FF2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {71724629-BEAB-4573-99FF-198211505FF2}.Release|Any CPU.Build.0 = Release|Any CPU - {B4DFC143-D3FA-4F46-9274-FB09C94F1228}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B4DFC143-D3FA-4F46-9274-FB09C94F1228}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B4DFC143-D3FA-4F46-9274-FB09C94F1228}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B4DFC143-D3FA-4F46-9274-FB09C94F1228}.Release|Any CPU.Build.0 = Release|Any CPU - {DDF936E4-61F3-4199-A6C6-39A79A09BBD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DDF936E4-61F3-4199-A6C6-39A79A09BBD7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DDF936E4-61F3-4199-A6C6-39A79A09BBD7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DDF936E4-61F3-4199-A6C6-39A79A09BBD7}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {78D220D8-1857-455F-8223-386193116D22} - EndGlobalSection -EndGlobal diff --git a/README.md b/README.md index b34078d..bac6017 100644 --- a/README.md +++ b/README.md @@ -1 +1,62 @@ -ModuleAnalyzer \ No newline at end of file +# PowerShell Module Anaylzer + +I developed this tool to analyze a PSModule I developed with my team for around 1 1/2 years. As the time went by, it become clear that the amount of technical debt has become very large and that we needed some way to analyze and identify critical hotspots in the tool. + +## Description + +The PowerShell analyzer is a little prototype which is made for analyzing PowerShell module commands, parameters and function calls with the purpose to make technical debt visible. + +- The analyzer will list all module functions, how often they are referenced by and referencing other functions. +- In addition the parameters and how often they are used are also considered. +- It calculates the stability index. This index describes from 0 (low) to 1 (high) how many other functions are relying on a specific function. + - A high stability means, that a change of the function comes with high cost. + +--- +## HowTo +### Startup Arguments + +| Parameter | Parameter (long) | Required | Description | +| --------- | ---------------- | -------- | ---------------------------------------------------------- | +| -p | --path | true | Path to the PowerShell module | +| -d | --debug | true | Enables debug logging | +| -o | --outputdir | true | Path to a directory for the output of the analysis results | +| -n | --outputname | true | Identifier for the output | + + +### Output + +The analyzer comes with multiple output formatters and is able to write the results as CSV, JSON or as Directed Graph Markup Language (dgml). +The DGML Output helps you to visualize the module and shows the function dependencies as a graph. + +#### CSV Compact + +- All commands from the module and the basic stats are listed here. + +| Column | Description | +| ------------------ | ---------------------------------------------------------------------------------------------------------- | +| Name | Name of the function. | +| Namespace | The namespace is the directory structure. | +| Invokes (Module) | Number of your own module functions that are executed by this function. | +| Invokes (External) | Number of external functions performed by this function. Such as "Get-Item". | +| InvokedBy | Number of invocations by other module functions. | +| Invokes (Unique) | Number of unique functions executed by this function. This means that each function is counted only once. | +| InvokedBy (Unique) | Number of your own unique module functions executed by this function. | +| StabilityIndex | Number of unique module functions that call this function. | +| LinesOfCode | Number of lines of code for this function. | +| Parameters | List of parameters and how often they are used. | + +#### CSV Invokes + +- Lists all invocations between the module functions. + +| Column | Description | +| --------------- | ----------------------------------------------------------------------- | +| Name | Name of the function. | +| Namespace | The namespace is the directory structure. | +| Target (Module) | Number of your own module functions that are executed by this function. | +| Parameters | List of parameters that are used while executing the command. | + + +#### DGML + +- DGML is used to visualise all invocations between the module functions. This format is experimental and can be displayed e.g. with Visual Studio. diff --git a/Source/ModuleAnalyzer.App/Factory/DefaultCommandVisitorFactory.cs b/Source/ModuleAnalyzer.App/Factory/DefaultCommandVisitorFactory.cs deleted file mode 100644 index 55e0b1d..0000000 --- a/Source/ModuleAnalyzer.App/Factory/DefaultCommandVisitorFactory.cs +++ /dev/null @@ -1,14 +0,0 @@ -using PsModuleAnalyzer.App.Visitor; -using PsModuleAnalyzer.Core.Factory; -using PsModuleAnalyzer.Core.Model; - -namespace PsModuleAnalyzer.App.Factory -{ - internal class DefaultCommandVisitorFactory : CommandVisitorFactory - { - public override DefaultCommandVisitor Create(ModuleCommand moduleCommand, ModuleDefinition moduleRepository) - { - return new DefaultCommandVisitor(moduleCommand, moduleRepository); - } - } -} diff --git a/Source/ModuleAnalyzer.App/Output/MermaidOutput.cs b/Source/ModuleAnalyzer.App/Output/MermaidOutput.cs deleted file mode 100644 index af0fab1..0000000 --- a/Source/ModuleAnalyzer.App/Output/MermaidOutput.cs +++ /dev/null @@ -1,48 +0,0 @@ -using PsModuleAnalyzer.Core.Interfaces; -using PsModuleAnalyzer.Core.Model; - -namespace PsModuleAnalyzer.App.Output -{ - public class MermaidOutput : IAnalyzerOutput - { - private const string TEXT_INDENTION = " "; - private readonly string _destinationPath; - - public MermaidOutput(string destinationPath) - { - _destinationPath = destinationPath; - } - - public void CreateAnalyzerOutupt(ModuleDefinition moduleDefinition) - { - var content = CreateMarkdownContent(moduleDefinition.ModuleCommandCalls); - WriteMarkdownFile(content); - } - - private List CreateMarkdownContent(List moduleCommandCalls) - { - var markdownContent = new List(); - markdownContent.Add("## Module Graph"); - markdownContent.Add("```mermaid"); - markdownContent.Add("graph TD;"); - - moduleCommandCalls.Where(s => !s.Target.IsExternal).ToList().ForEach(command => - { - markdownContent.Add(AddGraphConnection(command)); - }); - - markdownContent.Add("```"); - return markdownContent; - } - - private string AddGraphConnection(ModuleCommandCall commandCall) - { - return $"{TEXT_INDENTION}{commandCall.Source.Name}-->{commandCall.Target.Name}"; - } - - private void WriteMarkdownFile(List markdownContent) - { - File.WriteAllLines(_destinationPath, markdownContent); - } - } -} diff --git a/Source/ModuleAnalyzer.App/Program.cs b/Source/ModuleAnalyzer.App/Program.cs deleted file mode 100644 index 8ee8c33..0000000 --- a/Source/ModuleAnalyzer.App/Program.cs +++ /dev/null @@ -1,25 +0,0 @@ -using PsModuleAnalyzer.App.Factory; -using PsModuleAnalyzer.App.Output; -using PsModuleAnalyzer.Core.Anaylzer; -using PsModuleAnalyzer.DgmlFormatter; -using PsModuleAnaylzer.CSV; - -namespace PsModuleAnalyzer.App -{ - public class Program - { - private static void Main(string[] args) - { - ModuleAnalyzer? analyzer = ModuleAnalyzerBuilder.Create("D:\\Repositories\\Go\\Source\\Go.psd1") - .AddCommandVisitor() - .AddDgmlFormatter("d:\\module.dgml") - .AddCSVFormatter("d:\\module.csv") - .AddCSVFormatter("d:\\module-compact.csv", true) - .AddOutputFormatter(new MermaidOutput("d:\\module.md")) - .Build(); - - analyzer.Analyze(); - - } - } -} diff --git a/Source/ModuleAnalyzer.App/PsModuleAnalyzer.App.csproj b/Source/ModuleAnalyzer.App/PsModuleAnalyzer.App.csproj deleted file mode 100644 index 2c8a3e6..0000000 --- a/Source/ModuleAnalyzer.App/PsModuleAnalyzer.App.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - - Exe - net6.0 - enable - enable - - - - - - - - - diff --git a/Source/ModuleAnalyzer.App/Visitor/DefaultCommandVisitor.cs b/Source/ModuleAnalyzer.App/Visitor/DefaultCommandVisitor.cs deleted file mode 100644 index 845d979..0000000 --- a/Source/ModuleAnalyzer.App/Visitor/DefaultCommandVisitor.cs +++ /dev/null @@ -1,70 +0,0 @@ -using PsModuleAnalyzer.Core.Model; -using PsModuleAnalyzer.Core.Visitor; -using System.Management.Automation.Language; - -namespace PsModuleAnalyzer.App.Visitor -{ - public class DefaultCommandVisitor : CommandVisitor - { - public DefaultCommandVisitor(ModuleCommand moduleCommand, ModuleDefinition moduleDefinition) : - base(moduleCommand, moduleDefinition) - { - - } - - public override AstVisitAction VisitCommand(CommandAst commandAst) - { - string? targetCommandName = commandAst.GetCommandName(); - - IModuleCommand? targetCommand = _moduleRepository.GetModuleCommand(targetCommandName); - - if (targetCommand == null) - { - targetCommand = _moduleRepository.AddExternalModuleCommand(targetCommandName); - } - - ModuleCommandCall? moduleCommandCall = new ModuleCommandCall(_moduleCommand, targetCommand, _moduleRepository); - - _moduleRepository.AddModuleCommandCall(moduleCommandCall); - - return AstVisitAction.Continue; - } - - public override AstVisitAction VisitCommandParameter(CommandParameterAst parameterAst) - { - var currentCall = _moduleRepository.ModuleCommandCalls.Last(); - var currentParam = _moduleRepository.GetModuleCommandParameter(currentCall.Target.Name, parameterAst.ParameterName); - - if(currentParam != null) - currentCall.Parameters.Add(currentParam); - - return AstVisitAction.Continue; - } - - public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst commandAst) - { - string? targetCommandName = commandAst.Name; - - IModuleCommand? targetCommand = _moduleRepository.GetModuleCommand(targetCommandName); - - if (targetCommand == null) - { - targetCommand = _moduleRepository.AddExternalModuleCommand(targetCommandName); - } - - ModuleCommandCall? moduleCommandCall = new ModuleCommandCall(_moduleCommand, targetCommand, _moduleRepository); - if(commandAst.Parameters != null) - { - foreach(var cmd in commandAst.Parameters) - { - var cmdParam = new ModuleCommandParameter(_moduleCommand, cmd.Name.ToString(), cmd.StaticType.Name); - moduleCommandCall.Parameters.Add(cmdParam); - } - } - - _moduleRepository.AddModuleCommandCall(moduleCommandCall); - - return AstVisitAction.Continue; - } - } -} diff --git a/Source/ModuleAnalyzer.Core/Anaylzer/ModuleAnalyzer.cs b/Source/ModuleAnalyzer.Core/Anaylzer/ModuleAnalyzer.cs deleted file mode 100644 index 2283ed9..0000000 --- a/Source/ModuleAnalyzer.Core/Anaylzer/ModuleAnalyzer.cs +++ /dev/null @@ -1,62 +0,0 @@ -using PsModuleAnalyzer.Core.Factory; -using PsModuleAnalyzer.Core.Interfaces; -using PsModuleAnalyzer.Core.Model; -using PsModuleAnalyzer.Core.Visitor; -using System.Management.Automation; -using System.Management.Automation.Language; - -namespace PsModuleAnalyzer.Core.Anaylzer -{ - public class ModuleAnalyzer - { - public CommandVisitorFactory Factory { get; set; } - private readonly ModuleDefinition _moduleDefinition; - private readonly List AnalyzerOutputFormatters = new(); - - internal ModuleAnalyzer(string modulePath) - { - _moduleDefinition = new ModuleDefinition(modulePath); - } - - public void AddOutputFormat(IAnalyzerOutput output) - { - AnalyzerOutputFormatters.Add(output); - } - - public void SetCommandVisitorFactory(CommandVisitorFactory factory) - { - Factory = factory; - } - - public void Analyze() - { - foreach (ModuleCommand? command in _moduleDefinition.ModuleCommands) - { - ScriptBlockAst ast = Parser.ParseInput( - command.Definition, - out Token[] tokens, - out ParseError[] errors - ); - - List paramastList = new(); - var newAst = (ScriptBlockAst) ast.Copy(); - if(ast.ParamBlock != null) - { - foreach (var param in ast.ParamBlock.Parameters) - paramastList.Add((ParameterAst)param.Copy()); - } - - FunctionDefinitionAst fast = new FunctionDefinitionAst(newAst.Extent, false, false, command.Name, paramastList, newAst); - CommandVisitor? commandVisitor = Factory.Create(command, _moduleDefinition); - fast.Visit(commandVisitor); - } - - CreateOutput(); - } - - private void CreateOutput() - { - AnalyzerOutputFormatters.ForEach(output => output.CreateAnalyzerOutupt(_moduleDefinition)); - } - } -} diff --git a/Source/ModuleAnalyzer.Core/Anaylzer/ModuleAnalyzerBuilder.cs b/Source/ModuleAnalyzer.Core/Anaylzer/ModuleAnalyzerBuilder.cs deleted file mode 100644 index d7999fe..0000000 --- a/Source/ModuleAnalyzer.Core/Anaylzer/ModuleAnalyzerBuilder.cs +++ /dev/null @@ -1,43 +0,0 @@ -using PsModuleAnalyzer.Core.Factory; -using PsModuleAnalyzer.Core.Interfaces; - -namespace PsModuleAnalyzer.Core.Anaylzer -{ - public class ModuleAnalyzerBuilder - { - private readonly string _modulePath; - private readonly List _analyzerOutputs = new(); - private CommandVisitorFactory _factory; - - private ModuleAnalyzerBuilder(string modulePath) - { - _modulePath = modulePath; - } - - public static ModuleAnalyzerBuilder Create(string modulePath) - { - return new ModuleAnalyzerBuilder(modulePath); - } - - public ModuleAnalyzerBuilder AddOutputFormatter(IAnalyzerOutput output) - { - _analyzerOutputs.Add(output); - return this; - } - - public ModuleAnalyzerBuilder AddCommandVisitor() where CommandVisitorFactoryClass : CommandVisitorFactory, new() - { - _factory = new CommandVisitorFactoryClass(); - return this; - } - - public ModuleAnalyzer Build() - { - ModuleAnalyzer? analyzer = new ModuleAnalyzer(_modulePath); - analyzer.SetCommandVisitorFactory(_factory); - - _analyzerOutputs.ForEach(output => analyzer.AddOutputFormat(output)); - return analyzer; - } - } -} diff --git a/Source/ModuleAnalyzer.Core/Factory/CommandVisitorFactory.cs b/Source/ModuleAnalyzer.Core/Factory/CommandVisitorFactory.cs deleted file mode 100644 index 86ddacd..0000000 --- a/Source/ModuleAnalyzer.Core/Factory/CommandVisitorFactory.cs +++ /dev/null @@ -1,10 +0,0 @@ -using PsModuleAnalyzer.Core.Model; -using PsModuleAnalyzer.Core.Visitor; - -namespace PsModuleAnalyzer.Core.Factory -{ - public abstract class CommandVisitorFactory - { - public abstract CommandVisitor Create(ModuleCommand moduleCommand, ModuleDefinition moduleRepository); - } -} diff --git a/Source/ModuleAnalyzer.Core/Interfaces/IAnalyzerOutput.cs b/Source/ModuleAnalyzer.Core/Interfaces/IAnalyzerOutput.cs deleted file mode 100644 index 589aeb5..0000000 --- a/Source/ModuleAnalyzer.Core/Interfaces/IAnalyzerOutput.cs +++ /dev/null @@ -1,9 +0,0 @@ -using PsModuleAnalyzer.Core.Model; - -namespace PsModuleAnalyzer.Core.Interfaces -{ - public interface IAnalyzerOutput - { - public void CreateAnalyzerOutupt(ModuleDefinition moduleDefinition); - } -} diff --git a/Source/ModuleAnalyzer.Core/Model/IModuleCommand.cs b/Source/ModuleAnalyzer.Core/Model/IModuleCommand.cs deleted file mode 100644 index 20b7d22..0000000 --- a/Source/ModuleAnalyzer.Core/Model/IModuleCommand.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace PsModuleAnalyzer.Core.Model -{ - public interface IModuleCommand - { - string Name { get; } - string Namespace { get; } - int NumberOfReferencedBy { get; set; } - List Parameters { get; } - List References { get; } - List ReferencedBy { get; } - bool IsExternal { get; } - - Dictionary GetParametersAsDictionary(); - } -} \ No newline at end of file diff --git a/Source/ModuleAnalyzer.Core/Model/ModuleCommand.cs b/Source/ModuleAnalyzer.Core/Model/ModuleCommand.cs deleted file mode 100644 index d23aaa6..0000000 --- a/Source/ModuleAnalyzer.Core/Model/ModuleCommand.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System.Linq; - -namespace PsModuleAnalyzer.Core.Model -{ - public class ModuleCommand : IModuleCommand - { - private readonly string _commandFile; - - public string Name { get; private set; } - public string Definition { get; private set; } - public ModuleDefinition Module { get; private set; } - public List References { get => CalculateReferences(); } - public List ReferencedBy { get => CalculateReferencedBy(); } - public List Parameters { get; private set; } = new(); - public int NumberOfReferencedBy { get; set; } = 0; - public bool IsExternal { get; } - public string Namespace { get; private set; } - - public decimal StabilityIndex { get => CalculateStabilityIndex(); } - - - public ModuleCommand(string name, string definition, string commandFile, ModuleDefinition module) - { - Name = name; - Definition = definition; - IsExternal = false; - Module = module; - _commandFile = commandFile; - - ConfigureNamespace(); - } - - public Dictionary GetParametersAsDictionary() - { - var parameters = new Dictionary(); - foreach (var parameter in Parameters) - { - parameters.Add(parameter.Name, parameter.Type); - } - return parameters; - } - - private List CalculateReferencedBy() - { - return Module.ModuleCommandCalls - .Where(cmd => cmd.Target.Name == Name) - .Select(cmd => cmd.Source) - .ToList(); - } - - private List CalculateReferences() - { - return Module.ModuleCommandCalls - .Where(cmd => cmd.Source.Name == Name) - .Select(cmd => cmd.Source) - .ToList(); - } - - private decimal CalculateStabilityIndex() - { - return Math.Round((decimal) CalculateReferences().Count / (CalculateReferences().Count + CalculateReferencedBy().Count), 3); - } - - private void ConfigureNamespace() - { - Namespace = _commandFile - .Replace(Module.Path, "") - .Replace($"{Name}.ps1", "") - .Trim('\\') - .Replace("\\", "."); - } - } -} diff --git a/Source/ModuleAnalyzer.Core/Model/ModuleCommandCall.cs b/Source/ModuleAnalyzer.Core/Model/ModuleCommandCall.cs deleted file mode 100644 index 7621a0b..0000000 --- a/Source/ModuleAnalyzer.Core/Model/ModuleCommandCall.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Reflection; - -namespace PsModuleAnalyzer.Core.Model -{ - public class ModuleCommandCall - { - private readonly ModuleDefinition _module; - public readonly IModuleCommand Source; - public readonly IModuleCommand Target; - public readonly List Parameters = new(); - public int References { get => CalculateReferences(); } - - public ModuleCommandCall(IModuleCommand source, IModuleCommand target, ModuleDefinition module) - { - _module = module; - Source = source; - Target = target; - } - - private int CalculateReferences() - => _module.ModuleCommandCalls - .Count(calls => calls.Source.Name == Source.Name && calls.Target.Name == Target.Name); - } -} diff --git a/Source/ModuleAnalyzer.Core/Model/ModuleCommandParameter.cs b/Source/ModuleAnalyzer.Core/Model/ModuleCommandParameter.cs deleted file mode 100644 index 1026218..0000000 --- a/Source/ModuleAnalyzer.Core/Model/ModuleCommandParameter.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace PsModuleAnalyzer.Core.Model -{ - public class ModuleCommandParameter - { - public readonly ModuleCommand Command; - public readonly string Name; - public readonly string Type; - public int References { get => CalculateReferences(); } - - public ModuleCommandParameter(ModuleCommand command, string name, string type) - { - Command = command; - Name = name; - Type = type; - } - - private int CalculateReferences() - { - return Command.Module.ModuleCommandCalls - .Where(call => call.Target.Name == Command.Name) - .Sum(call => call.Parameters.Count(param => param.Name == Name)); - } - } -} diff --git a/Source/ModuleAnalyzer.Core/Model/ModuleDefinition.cs b/Source/ModuleAnalyzer.Core/Model/ModuleDefinition.cs deleted file mode 100644 index 1ebcc7a..0000000 --- a/Source/ModuleAnalyzer.Core/Model/ModuleDefinition.cs +++ /dev/null @@ -1,143 +0,0 @@ -using System.Management.Automation; - -namespace PsModuleAnalyzer.Core.Model -{ - public class ModuleDefinition - { - private readonly PSModuleInfo _psModuleInfo; - - public readonly string Name; - public readonly string Path; - public readonly HashSet ModuleCommands; - public readonly List ModuleCommandCalls = new(); - public readonly List ModuleCommandParameters = new(); - public readonly List ModuleExternalCommand = new(); - - public ModuleDefinition(string modulePath) - { - _psModuleInfo = LoadModuleFromPath(modulePath); - - Name = _psModuleInfo.Name; - Path = _psModuleInfo.ModuleBase; - - ModuleCommands = CreateModuleCommandList(_psModuleInfo); - } - - public ModuleCommand? GetModuleCommand(string name) - { - return ModuleCommands.FirstOrDefault(command => command.Name == name); - } - - public ModuleExternalCommand? GetModuleExternalCommand(string name) - { - return ModuleExternalCommand.FirstOrDefault(command => command.Name == name); - } - - public HashSet AddModuleCommands(HashSet moduleCommands) - { - foreach (var command in moduleCommands) - { - ModuleCommands.Add(command); - } - return ModuleCommands; - } - - public ModuleExternalCommand AddExternalModuleCommand(string name) - { - ModuleExternalCommand? moduleCommand = GetModuleExternalCommand(name); - if (moduleCommand == null) - { - moduleCommand = new ModuleExternalCommand(name); - ModuleExternalCommand.Add(moduleCommand); - } - - return moduleCommand; - } - - public ModuleCommandCall AddModuleCommandCall(ModuleCommandCall moduleCommand) - { - ModuleCommandCalls.Add(moduleCommand); - - RefreshTargetCommandReferences(moduleCommand.Target); - return moduleCommand; - } - - public ModuleCommandParameter? GetModuleCommandParameter(string commandName, string parameterName) - { - var moduleCommand = GetModuleCommand(commandName); - if(moduleCommand != null) - { - return moduleCommand.Parameters.FirstOrDefault(param => param.Name == parameterName); - } - - var externalCommand = GetModuleExternalCommand(commandName); - if(externalCommand != null) - { - return externalCommand.Parameters.FirstOrDefault(param => param.Name == parameterName); - } - - return null; - } - - public ModuleExternalCommand AddModuleExternalCommandCall(string name, IModuleCommand calledBy) - { - ModuleExternalCommand? moduleCommand = GetModuleExternalCommand(name); - if (moduleCommand == null) - { - moduleCommand = new ModuleExternalCommand(name); - ModuleExternalCommand.Add(moduleCommand); - } - - ModuleCommandCalls.Add(new ModuleCommandCall(calledBy, moduleCommand, this)); - return moduleCommand; - } - - private void RefreshTargetCommandReferences(IModuleCommand moduleCommand) - { - moduleCommand.NumberOfReferencedBy = ModuleCommandCalls.Count(c => c.Target.Name == moduleCommand.Name); - } - - private static PSModuleInfo LoadModuleFromPath(string modulePath) - { - PowerShell? ps = PowerShell.Create(); - ps.AddScript("Set-ExecutionPolicy -Scope Process -ExecutionPolicy Unrestricted; Get-ExecutionPolicy"); - ps.Commands.AddScript($"Import-Module {modulePath} -PassThru"); - return ps.Invoke().First(); - } - - private HashSet CreateModuleCommandList(PSModuleInfo moduleInfo) - { - var defaultParams = new List - { - "Debug", "Verbose", "ErrorAction", "WarningAction", "InformationAction", "WarningVariable", "ErrorVariable", "OutVariable", "InformationVariable" ,"OutBuffer","PipelineVariable","WhatIf" - }; - - HashSet? moduleCommandList = new (); - foreach (KeyValuePair function in moduleInfo.ExportedFunctions) - { - var command = function.Value; - ModuleCommand? moduleCommand = new(command.Name, command.Definition, command.ScriptBlock.File, this); - try - { - foreach (KeyValuePair param in command.Parameters) - { - if (defaultParams.Contains(param.Key)) - continue; - - ModuleCommandParameter? moduleCommandParam = new(moduleCommand, param.Value.Name, param.Value.ParameterType.Name); - moduleCommand.Parameters.Add(moduleCommandParam); - } - } - catch - { - Console.WriteLine("Param Error"); - } - - - moduleCommandList.Add(moduleCommand); - } - return moduleCommandList; - } - - } -} diff --git a/Source/ModuleAnalyzer.Core/Model/ModuleExternalCommand.cs b/Source/ModuleAnalyzer.Core/Model/ModuleExternalCommand.cs deleted file mode 100644 index eb0a51f..0000000 --- a/Source/ModuleAnalyzer.Core/Model/ModuleExternalCommand.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace PsModuleAnalyzer.Core.Model -{ - public class ModuleExternalCommand : IModuleCommand - { - public string Name { get; private set; } - public List ReferencedBy { get; } - public List References { get => new(); } - public List Parameters { get => new(); } - public int NumberOfReferencedBy { get; set; } = 0; - public bool IsExternal { get; } - public string _commandFile { get; private set; } = "External"; - - public string Namespace { get => ""; } - - - public ModuleExternalCommand(string name) - { - Name = name; - IsExternal = true; - } - - public Dictionary GetParametersAsDictionary() - { - var parameters = new Dictionary(); - foreach (var parameter in Parameters) - { - parameters.Add(parameter.Name, parameter.Type); - } - return parameters; - } - } -} diff --git a/Source/ModuleAnalyzer.Core/PsModuleAnalyzer.Core.csproj b/Source/ModuleAnalyzer.Core/PsModuleAnalyzer.Core.csproj deleted file mode 100644 index 1194932..0000000 --- a/Source/ModuleAnalyzer.Core/PsModuleAnalyzer.Core.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - Library - net6.0 - enable - enable - - - - - - - - diff --git a/Source/ModuleAnalyzer.Core/Visitor/CommandVisitor.cs b/Source/ModuleAnalyzer.Core/Visitor/CommandVisitor.cs deleted file mode 100644 index c710472..0000000 --- a/Source/ModuleAnalyzer.Core/Visitor/CommandVisitor.cs +++ /dev/null @@ -1,17 +0,0 @@ -using PsModuleAnalyzer.Core.Model; -using System.Management.Automation.Language; - -namespace PsModuleAnalyzer.Core.Visitor -{ - public abstract class CommandVisitor : AstVisitor2 - { - protected readonly ModuleDefinition _moduleRepository; - protected readonly ModuleCommand _moduleCommand; - - public CommandVisitor(ModuleCommand moduleCommand, ModuleDefinition moduleRepository) - { - _moduleRepository = moduleRepository; - _moduleCommand = moduleCommand; - } - } -} diff --git a/Source/ModuleAnalyzer.DgmlFormatter/DgmlFormatter.cs b/Source/ModuleAnalyzer.DgmlFormatter/DgmlFormatter.cs deleted file mode 100644 index b0ebe6e..0000000 --- a/Source/ModuleAnalyzer.DgmlFormatter/DgmlFormatter.cs +++ /dev/null @@ -1,59 +0,0 @@ -using OpenSoftware.DgmlTools; -using OpenSoftware.DgmlTools.Analyses; -using OpenSoftware.DgmlTools.Builders; -using OpenSoftware.DgmlTools.Model; -using PsModuleAnalyzer.Core.Interfaces; -using PsModuleAnalyzer.Core.Model; - -namespace PsModuleAnalyzer.DgmlFormatter -{ - public class DgmlFormatter : IAnalyzerOutput - { - private readonly string _destinationFile; - - public DgmlFormatter(string destinationFile) - { - _destinationFile = destinationFile; - } - - public void CreateAnalyzerOutupt(ModuleDefinition moduleDefinition) - { - var commandCalls = moduleDefinition.ModuleCommandCalls.Where(commands => !commands.Target.IsExternal).ToList(); - DgmlBuilder? builder = new DgmlBuilder(new HubNodeAnalysis(), new NodeReferencedAnalysis(), new CategoryColorAnalysis()) - { - - CategoryBuilders = new CategoryBuilder[] - { - new CategoryBuilder(x => new Category - { - Id = x.Source.Namespace, - Label = x.Source.Namespace - }) - }, - NodeBuilders = new NodeBuilder[] - { - new NodeBuilder( - x => new Node - { - Id = x.Source.Name, - Label = x.Source.Name, - Category = x.Source.Namespace, - Group = x.Source.Namespace, - }) - }, - LinkBuilders = new LinkBuilder[] - { - new LinkBuilder( - x => new Link - { - Source = x.Source.Name, - Target = x.Target.Name, - Stroke = "#2269E0" - }) - } - }; - DirectedGraph? graph = builder.Build(commandCalls); - graph.WriteToFile(_destinationFile); - } - } -} diff --git a/Source/ModuleAnalyzer.DgmlFormatter/DgmlFormatterExtension.cs b/Source/ModuleAnalyzer.DgmlFormatter/DgmlFormatterExtension.cs deleted file mode 100644 index 8ae0d98..0000000 --- a/Source/ModuleAnalyzer.DgmlFormatter/DgmlFormatterExtension.cs +++ /dev/null @@ -1,13 +0,0 @@ -using PsModuleAnalyzer.Core.Anaylzer; - -namespace PsModuleAnalyzer.DgmlFormatter -{ - public static class DgmlFormatterExtension - { - public static ModuleAnalyzerBuilder AddDgmlFormatter(this ModuleAnalyzerBuilder moduleAnalyzerBuilder, string destinationPath) - { - moduleAnalyzerBuilder.AddOutputFormatter(new DgmlFormatter(destinationPath)); - return moduleAnalyzerBuilder; - } - } -} \ No newline at end of file diff --git a/src/.editorconfig b/src/.editorconfig new file mode 100644 index 0000000..97f0794 --- /dev/null +++ b/src/.editorconfig @@ -0,0 +1,273 @@ +# Rules in this file were initially inferred by Visual Studio IntelliCode from the D:\Repositories\ps-module-analyzer\ModuleAnalyzer\Source codebase based on best match to current usage at 25.05.2022 +# You can modify the rules from these initially generated values to suit your own policies +# You can learn more about editorconfig here: https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference +[*.cs] + + +#Core editorconfig formatting - indentation + +#use soft tabs (spaces) for indentation +indent_style = space + +#Formatting - new line options + +#place catch statements on a new line +csharp_new_line_before_catch = true +#place else statements on a new line +csharp_new_line_before_else = true +#require members of object intializers to be on separate lines +csharp_new_line_before_members_in_object_initializers = true +#require braces to be on a new line for lambdas, types, methods, and control_blocks (also known as "Allman" style) +csharp_new_line_before_open_brace = lambdas, types, methods, control_blocks + +#Formatting - organize using options + +#do not place System.* using directives before other using directives +dotnet_sort_system_directives_first = false + +#Formatting - spacing options + +#require NO space between a cast and the value +csharp_space_after_cast = false +#require a space before the colon for bases or interfaces in a type declaration +csharp_space_after_colon_in_inheritance_clause = true +#require a space after a keyword in a control flow statement such as a for loop +csharp_space_after_keywords_in_control_flow_statements = true +#require a space before the colon for bases or interfaces in a type declaration +csharp_space_before_colon_in_inheritance_clause = true +#remove space within empty argument list parentheses +csharp_space_between_method_call_empty_parameter_list_parentheses = false +#remove space between method call name and opening parenthesis +csharp_space_between_method_call_name_and_opening_parenthesis = false +#do not place space characters after the opening parenthesis and before the closing parenthesis of a method call +csharp_space_between_method_call_parameter_list_parentheses = false +#remove space within empty parameter list parentheses for a method declaration +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +#place a space character after the opening parenthesis and before the closing parenthesis of a method declaration parameter list. +csharp_space_between_method_declaration_parameter_list_parentheses = false + +#Formatting - wrapping options + +#leave code block on single line +csharp_preserve_single_line_blocks = true + +#Style - Code block preferences + +#prefer no curly braces if allowed +csharp_prefer_braces = false:suggestion + +#Style - expression bodied member options + +#prefer block bodies for constructors +csharp_style_expression_bodied_constructors = false:suggestion +#prefer block bodies for methods +csharp_style_expression_bodied_methods = true:suggestion +#prefer expression-bodied members for properties +csharp_style_expression_bodied_properties = true:suggestion + +#Style - expression level options + +#prefer out variables to be declared inline in the argument list of a method call when possible +csharp_style_inlined_variable_declaration = true:suggestion +#prefer the language keyword for member access expressions, instead of the type name, for types that have a keyword to represent them +dotnet_style_predefined_type_for_member_access = true:suggestion + +#Style - Expression-level preferences + +#prefer objects to be initialized using object initializers when possible +dotnet_style_object_initializer = true:suggestion + +#Style - implicit and explicit types + +#prefer var over explicit type in all cases, unless overridden by another code style rule +csharp_style_var_elsewhere = true:suggestion +#prefer var is used to declare variables with built-in system types such as int +csharp_style_var_for_built_in_types = true:suggestion +#prefer var when the type is already mentioned on the right-hand side of a declaration expression +csharp_style_var_when_type_is_apparent = true:suggestion + +#Style - language keyword and framework type options + +#prefer the language keyword for local variables, method parameters, and class members, instead of the type name, for types that have a keyword to represent them +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion + +#Style - modifier options + +#prefer accessibility modifiers to be declared except for public interface members. This will currently not differ from always and will act as future proofing for if C# adds default interface methods. +dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion + +#Style - Modifier preferences + +#when this rule is set to a list of modifiers, prefer the specified ordering. +csharp_preferred_modifier_order = public,private,internal,protected,readonly,static,override,abstract:suggestion + +#Style - Pattern matching + +#prefer pattern matching instead of is expression with type casts +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion + +#Style - qualification options + +#prefer fields not to be prefaced with this. or Me. in Visual Basic +dotnet_style_qualification_for_field = false:suggestion +#prefer methods not to be prefaced with this. or Me. in Visual Basic +dotnet_style_qualification_for_method = false:suggestion +#prefer properties not to be prefaced with this. or Me. in Visual Basic +dotnet_style_qualification_for_property = false:suggestion +csharp_indent_labels = one_less_than_current +csharp_using_directive_placement = outside_namespace:silent +csharp_prefer_simple_using_statement = true:silent +csharp_style_namespace_declarations = file_scoped:silent +csharp_style_expression_bodied_operators = when_on_single_line:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = true:silent + +[*.{cs,vb}] +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +end_of_line = crlf +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:silent +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = false:silent +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = false:silent +[*.cs] +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = error +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.private_or_internal_field_should_be_private_field.severity = warning +dotnet_naming_rule.private_or_internal_field_should_be_private_field.symbols = private_or_internal_field +dotnet_naming_rule.private_or_internal_field_should_be_private_field.style = private_field + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.private_or_internal_field.applicable_kinds = field +dotnet_naming_symbols.private_or_internal_field.applicable_accessibilities = internal, private, private_protected +dotnet_naming_symbols.private_or_internal_field.required_modifiers = + +# Naming styles + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.private_field.required_prefix = _ +dotnet_naming_style.private_field.required_suffix = +dotnet_naming_style.private_field.word_separator = +dotnet_naming_style.private_field.capitalization = camel_case +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_prefer_tuple_swap = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = false:suggestion +csharp_style_deconstructed_variable_declaration = true:silent +csharp_style_unused_value_expression_statement_preference = discard_variable:silent +csharp_style_unused_value_assignment_preference = discard_variable:silent +csharp_prefer_static_local_function = false:silent +csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent +csharp_style_conditional_delegate_call = true:suggestion +csharp_style_prefer_switch_expression = true:suggestion +csharp_style_prefer_pattern_matching = true:silent +csharp_style_pattern_matching_over_is_with_cast_check = true:silent +csharp_style_prefer_not_pattern = true:silent +csharp_style_prefer_extended_property_pattern = true:suggestion + +[*.vb] +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +# Naming styles + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +[*.{cs,vb}] +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case +dotnet_style_namespace_match_folder = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = false:silent +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_readonly_field = true:suggestion +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion +dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion +dotnet_style_allow_multiple_blank_lines_experimental = true:silent +dotnet_style_allow_statement_immediately_after_block_experimental = true:silent +dotnet_code_quality_unused_parameters = all:suggestion +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_event = false:silent diff --git a/src/PowerShell.ModuleAnalyzer.App/CommandParser/CommandLineOptions.cs b/src/PowerShell.ModuleAnalyzer.App/CommandParser/CommandLineOptions.cs new file mode 100644 index 0000000..aa8f851 --- /dev/null +++ b/src/PowerShell.ModuleAnalyzer.App/CommandParser/CommandLineOptions.cs @@ -0,0 +1,18 @@ +using CommandLine; + +namespace ModuleAnalyzer.App.CommandParser; + +public class CommandLineOptions +{ + [Option('d', "debug", Required = false, HelpText = "Set output to verbose messages.")] + public bool Verbose { get; set; } + + [Option('p', "path", Required = true, HelpText = "Set module path.")] + public string Path { get; set; } + + [Option('o', "outputdir", Required = true, HelpText = "Directory for the result.")] + public string OutputDir { get; set; } + + [Option('n', "outputname", Required = false, HelpText = "Directory for the result.")] + public string OutputName { get; set; } = "result"; +} \ No newline at end of file diff --git a/src/PowerShell.ModuleAnalyzer.App/CommandParser/CommandLineParser.cs b/src/PowerShell.ModuleAnalyzer.App/CommandParser/CommandLineParser.cs new file mode 100644 index 0000000..1e99dc0 --- /dev/null +++ b/src/PowerShell.ModuleAnalyzer.App/CommandParser/CommandLineParser.cs @@ -0,0 +1,12 @@ +using CommandLine; + +namespace ModuleAnalyzer.App.CommandParser; + +internal static class CommandLineParser +{ + internal static void Execute(IEnumerable args, Action action) + { + Parser.Default.ParseArguments(args) + .WithParsed(action); + } +} diff --git a/src/PowerShell.ModuleAnalyzer.App/PowerShell.ModuleAnalyzer.App.csproj b/src/PowerShell.ModuleAnalyzer.App/PowerShell.ModuleAnalyzer.App.csproj new file mode 100644 index 0000000..fa97911 --- /dev/null +++ b/src/PowerShell.ModuleAnalyzer.App/PowerShell.ModuleAnalyzer.App.csproj @@ -0,0 +1,20 @@ + + + + Exe + net6.0 + enable + enable + ModuleAnalyzer.App + + + + + + + + + + + + diff --git a/src/PowerShell.ModuleAnalyzer.App/Program.cs b/src/PowerShell.ModuleAnalyzer.App/Program.cs new file mode 100644 index 0000000..42d67a9 --- /dev/null +++ b/src/PowerShell.ModuleAnalyzer.App/Program.cs @@ -0,0 +1,38 @@ +using ModuleAnalyzer.App.CommandParser; +using ModuleAnalyzer.Core.Anaylzer; +using ModuleAnalyzer.Core.Visitor; +using ModuleAnalyzer.Formatter; + +namespace ModuleAnalyzer.App; + +public class Program +{ + private static void Main(string[] args) + { + CommandLineParser.Execute(args, options => + { + CreateOutputDirectory(options.OutputDir); + + var outputFile = Path.Combine(options.OutputDir, options.OutputName); + var analyzer = ModuleAnalyzerBuilder.Create(options.Path) + .AddCommandVisitor() + .AddDgmlFormatter(CreateOutputFilePath(options.OutputDir, options.OutputName, "dgml")) + .AddCSVFormatter(CreateOutputFilePath(options.OutputDir, $"{options.OutputName}-invokes", "csv")) + .AddCSVFormatter(CreateOutputFilePath(options.OutputDir, $"{options.OutputName}-compact", "csv"), true) + //.AddJsonFormatter(CreateOutputFilePath(options.OutputDir, options.OutputName, "json")) + .AddLogger() + .Build(); + + analyzer.Analyze(); + }); + } + + private static string CreateOutputFilePath(string outputDirectoryPath, string outputName, string outputExtension) + => Path.Combine(outputDirectoryPath, $"{outputName}-{DateTime.Now:ddMMyyyy}.{outputExtension.TrimStart('.')}"); + + private static void CreateOutputDirectory(string outputDirectoryPath) + { + if (!Directory.Exists(outputDirectoryPath)) + Directory.CreateDirectory(outputDirectoryPath); + } +} \ No newline at end of file diff --git a/src/PowerShell.ModuleAnalyzer.App/Properties/launchSettings.json b/src/PowerShell.ModuleAnalyzer.App/Properties/launchSettings.json new file mode 100644 index 0000000..25ce10a --- /dev/null +++ b/src/PowerShell.ModuleAnalyzer.App/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "PsModuleAnalyzer.App": { + "commandName": "Project", + "commandLineArgs": "-p \"D:\\Repositories\\navcontainerhelper\\BcContainerHelper.psd1\" -o \"d:\\test\" -n \"test\"" + } + } +} \ No newline at end of file diff --git a/src/PowerShell.ModuleAnalyzer.Core/Anaylzer/ModuleAnalyzer.cs b/src/PowerShell.ModuleAnalyzer.Core/Anaylzer/ModuleAnalyzer.cs new file mode 100644 index 0000000..394c3aa --- /dev/null +++ b/src/PowerShell.ModuleAnalyzer.Core/Anaylzer/ModuleAnalyzer.cs @@ -0,0 +1,90 @@ +using Microsoft.Extensions.Logging; +using ModuleAnalyzer.Core.Interfaces; +using ModuleAnalyzer.Core.Model; +using ModuleAnalyzer.Core.Visitor; +using System.Management.Automation.Language; + +namespace ModuleAnalyzer.Core.Anaylzer; + +/// +/// This class is responsible to load the target PowerShell module, +/// execute the analyzation and to call the output formatters. +/// +public class ModuleAnalyzer +{ + private readonly List _analyzerOutputFormatters = new(); + private readonly ILogger? _logger; + + public ModuleCommandVisitorFactory? Factory { get; set; } + public ModuleDefinition ModuleDefinition { get; private set; } + + internal ModuleAnalyzer(string modulePath, ILogger? logger = null) + { + ModuleDefinition = new ModuleDefinition(modulePath, logger); + _logger = logger; + } + + /// + /// Add an output formatter to the analyzer. + /// Its possible to add multiple analyzer.  + /// + /// Output formatter + public void AddOutputFormat(IAnalyzerOutputFormatter outputFormatter) + => _analyzerOutputFormatters.Add(outputFormatter); + + public void SetCommandVisitorFactory(ModuleCommandVisitorFactory factory) + => Factory = factory; + + /// + /// Executes the analysis and traverses the script tree. + /// The result will be stored in the Module Definition. + /// + public void Analyze() + { + try + { + _logger?.LogInformation("Start to analyze module {name}", ModuleDefinition.Name); + foreach (var command in ModuleDefinition.ModuleCommands) + { + var scriptBlockAst = Parser.ParseInput(command.Definition, out var tokens, out var errors); + var scriptBlockAstCopy = (ScriptBlockAst) scriptBlockAst.Copy(); + + var paramastList = CreateParameterListFromScriptBlock(scriptBlockAst); + + var functionDefinitionAst = new FunctionDefinitionAst(scriptBlockAstCopy.Extent, false, false, command.Name, paramastList, scriptBlockAstCopy); + + var commandVisitor = Factory.Create(command, ModuleDefinition); + + functionDefinitionAst.Visit(commandVisitor); + } + + _logger?.LogInformation("Analysis finished. Write results"); + CreateOutput(); + + _logger?.LogInformation("Done"); + } + catch (Exception ex) + { + _logger?.LogCritical("-------------------------------------------------"); + _logger?.LogCritical(exception: ex, "Error during analysis."); + _logger?.LogCritical("-------------------------------------------------"); + } + + } + + private static List CreateParameterListFromScriptBlock(ScriptBlockAst scriptBlockAst) + { + var paramastList = new List(); + if (scriptBlockAst.ParamBlock is not null) + foreach (var param in scriptBlockAst.ParamBlock.Parameters) + paramastList.Add((ParameterAst)param.Copy()); + + return paramastList; + } + + /// + /// This function will loop through the output writers. + /// + private void CreateOutput() + => _analyzerOutputFormatters.ForEach(output => output.CreateAnalyzerOutupt(ModuleDefinition)); +} diff --git a/src/PowerShell.ModuleAnalyzer.Core/Anaylzer/ModuleAnalyzerBuilder.cs b/src/PowerShell.ModuleAnalyzer.Core/Anaylzer/ModuleAnalyzerBuilder.cs new file mode 100644 index 0000000..b093adf --- /dev/null +++ b/src/PowerShell.ModuleAnalyzer.Core/Anaylzer/ModuleAnalyzerBuilder.cs @@ -0,0 +1,71 @@ +using Microsoft.Extensions.Logging; +using ModuleAnalyzer.Core.Interfaces; +using ModuleAnalyzer.Core.Output; +using ModuleAnalyzer.Core.Visitor; + +namespace ModuleAnalyzer.Core.Anaylzer; + +public class ModuleAnalyzerBuilder +{ + private readonly string _modulePath; + private readonly List _analyzerOutputs = new(); + private ModuleCommandVisitorFactory? _factory; + private ILogger? _logger; + + private ModuleAnalyzerBuilder(string modulePath) + { + _modulePath = modulePath; + _analyzerOutputs.Add(new ShortStatFormatter()); + } + + public static ModuleAnalyzerBuilder Create(string modulePath) => new ModuleAnalyzerBuilder(modulePath); + + public ModuleAnalyzerBuilder AddOutputFormatter(IAnalyzerOutputFormatter output) + { + _logger?.LogDebug("{object}: Output formatter {output} added", nameof(ModuleAnalyzerBuilder), nameof(output)); + _analyzerOutputs.Add(output); + return this; + } + + public ModuleAnalyzerBuilder AddLogger(ILogger? logger = null) + { + _logger = logger; + return this; + } + + public ModuleAnalyzerBuilder AddCommandVisitor() where CommandVisitorFactoryClass : ModuleCommandVisitorFactory, new() + { + _logger?.LogDebug("{object}: CommandVisitorFactory {factory} added", nameof(ModuleAnalyzerBuilder), nameof(CommandVisitorFactoryClass)); + _factory = new CommandVisitorFactoryClass(); + return this; + } + + public ModuleAnalyzer Build() + { + if (_logger is null) + _logger = CreateDefaultLogger(); + + if (_factory is null) + _factory = new ModuleDefaultCommandVisitorFactory(); + + var analyzer = new ModuleAnalyzer(_modulePath, _logger); + analyzer.SetCommandVisitorFactory(_factory); + + _analyzerOutputs.ForEach(output => analyzer.AddOutputFormat(output)); + return analyzer; + } + + private static ILogger CreateDefaultLogger() + { + return LoggerFactory.Create(builder => + { + builder.AddSimpleConsole(options => + { + options.SingleLine = true; + options.TimestampFormat = "hh:mm:ss "; + options.IncludeScopes = false; + }) + .SetMinimumLevel(LogLevel.Debug); + }).CreateLogger(""); + } +} diff --git a/src/PowerShell.ModuleAnalyzer.Core/Exceptions/ModuleInvalidException.cs b/src/PowerShell.ModuleAnalyzer.Core/Exceptions/ModuleInvalidException.cs new file mode 100644 index 0000000..f85b736 --- /dev/null +++ b/src/PowerShell.ModuleAnalyzer.Core/Exceptions/ModuleInvalidException.cs @@ -0,0 +1,5 @@ +namespace ModuleAnalyzer.Core.Exceptions; + +public class ModuleInvalidException : Exception +{ +} diff --git a/src/PowerShell.ModuleAnalyzer.Core/Interfaces/IAnalyzerOutputFormatter.cs b/src/PowerShell.ModuleAnalyzer.Core/Interfaces/IAnalyzerOutputFormatter.cs new file mode 100644 index 0000000..0db7c72 --- /dev/null +++ b/src/PowerShell.ModuleAnalyzer.Core/Interfaces/IAnalyzerOutputFormatter.cs @@ -0,0 +1,11 @@ +using ModuleAnalyzer.Core.Model; + +namespace ModuleAnalyzer.Core.Interfaces; + +/// +/// Interface to implement custom outputs for the analyzer. +/// +public interface IAnalyzerOutputFormatter +{ + public void CreateAnalyzerOutupt(ModuleDefinition moduleDefinition); +} diff --git a/src/PowerShell.ModuleAnalyzer.Core/Model/IModuleCommand.cs b/src/PowerShell.ModuleAnalyzer.Core/Model/IModuleCommand.cs new file mode 100644 index 0000000..42d42b5 --- /dev/null +++ b/src/PowerShell.ModuleAnalyzer.Core/Model/IModuleCommand.cs @@ -0,0 +1,46 @@ +namespace ModuleAnalyzer.Core.Model; + +public interface IModuleCommand +{ + /// + /// Name of the command. + /// + string Name { get; } + + /// + /// Tags the command as external. + /// An external command is a function which is called by the module and + /// not defined as part of the module. + /// + bool IsExternal { get; } + + /// + /// Returns how often this command is called by other commands. + /// + int TotalReferencedBy { get; } + + /// + /// The namespace is the local path of the source file inside the module. + /// + ModuleNamespace Namespace { get; } + + /// + /// Contains all command parameters. + /// + ICollection Parameters { get; } + + /// + /// Contains all the command calls of this command. + /// + ICollection References { get; } + + /// + /// Contains all the commands that refer to this command. + /// + ICollection ReferencedBy { get; } + + /// + /// Returns the parameters as dictionary. + /// + IDictionary GetParametersAsDictionary(); +} diff --git a/src/PowerShell.ModuleAnalyzer.Core/Model/ModuleCommand.cs b/src/PowerShell.ModuleAnalyzer.Core/Model/ModuleCommand.cs new file mode 100644 index 0000000..2475616 --- /dev/null +++ b/src/PowerShell.ModuleAnalyzer.Core/Model/ModuleCommand.cs @@ -0,0 +1,49 @@ +namespace ModuleAnalyzer.Core.Model; + +public class ModuleCommand : IModuleCommand +{ + public readonly string CommandFile; + + public string Name { get; private set; } + internal string Definition { get; set; } + + public int TotalReferencedBy => ReferencedBy.Count; + public bool IsExternal { get; } + public ModuleNamespace Namespace { get; set; } + + public ICollection Parameters { get; } = new HashSet(); + public ICollection References { get; } = new HashSet(); + public ICollection ReferencedBy { get; } = new HashSet(); + + public int TotalUniqueReferencedBy => ReferencedBy.GroupBy(cmd => cmd.Target).Count(); + public int TotalUniqueReferenced => ReferencedBy.GroupBy(cmd => cmd.Source).Count(); + + public decimal StabilityIndex => CalculateStabilityIndex(); + public int LinesOfCode = 0; + + public ModuleCommand(string name, string definition, string commandFile) + { + Name = name; + Definition = definition; + IsExternal = false; + CommandFile = commandFile; + } + + public IDictionary GetParametersAsDictionary() + { + var parameters = new Dictionary(); + foreach (var parameter in Parameters) + { + parameters.Add(parameter.Name, parameter.Type); + } + return parameters; + } + + private decimal CalculateStabilityIndex() + { + if (References.Count + ReferencedBy.Count == 0) + return 0; + + return Math.Round((decimal)References.Count / (References.Count + ReferencedBy.Count), 3); + } +} diff --git a/src/PowerShell.ModuleAnalyzer.Core/Model/ModuleCommandCall.cs b/src/PowerShell.ModuleAnalyzer.Core/Model/ModuleCommandCall.cs new file mode 100644 index 0000000..2c66c8b --- /dev/null +++ b/src/PowerShell.ModuleAnalyzer.Core/Model/ModuleCommandCall.cs @@ -0,0 +1,17 @@ +namespace ModuleAnalyzer.Core.Model; + +public class ModuleCommandCall +{ + public readonly IModuleCommand Source; + public readonly IModuleCommand Target; + public readonly HashSet Parameters = new(); + + public ModuleCommandCall(IModuleCommand source, IModuleCommand target) + { + Source = source; + Target = target; + + source.References.Add(this); + target.ReferencedBy.Add(this); + } +} diff --git a/src/PowerShell.ModuleAnalyzer.Core/Model/ModuleCommandParameter.cs b/src/PowerShell.ModuleAnalyzer.Core/Model/ModuleCommandParameter.cs new file mode 100644 index 0000000..f59304a --- /dev/null +++ b/src/PowerShell.ModuleAnalyzer.Core/Model/ModuleCommandParameter.cs @@ -0,0 +1,23 @@ +namespace ModuleAnalyzer.Core.Model; + +public class ModuleCommandParameter +{ + public readonly IModuleCommand Command; + public readonly string Name; + public readonly string Type; + + public int References => CalculateReferences(); + + public ModuleCommandParameter(IModuleCommand command, string name, string type) + { + Command = command; + Name = name; + Type = type; + } + + /// + /// Calculates the number of calls for this parameter. + /// + private int CalculateReferences() + => Command.ReferencedBy.Sum(commandCall => commandCall.Parameters.Count(param => param.Equals(this))); +} diff --git a/src/PowerShell.ModuleAnalyzer.Core/Model/ModuleDefinition.cs b/src/PowerShell.ModuleAnalyzer.Core/Model/ModuleDefinition.cs new file mode 100644 index 0000000..9609710 --- /dev/null +++ b/src/PowerShell.ModuleAnalyzer.Core/Model/ModuleDefinition.cs @@ -0,0 +1,194 @@ +using Microsoft.Extensions.Logging; +using ModuleAnalyzer.Core.Exceptions; +using ModuleAnalyzer.Core.Runner; +using System.Management.Automation; +using System.Text.RegularExpressions; +using MathNet.Numerics.Statistics; + +namespace ModuleAnalyzer.Core.Model; + +public class ModuleDefinition +{ + private readonly string[] DEFAULT_PARAMETERS = new[] + { + "Debug", "Verbose", "ErrorAction", "WarningAction", "InformationAction", "WarningVariable", "ErrorVariable", "OutVariable", "InformationVariable" ,"OutBuffer","PipelineVariable","WhatIf" + }; + + private readonly PSModuleInfo _psModuleInfo; + private readonly ILogger? _logger; + + public readonly string Name; + public readonly string Path; + + public readonly HashSet ModuleCommands = new(); + public readonly HashSet ModuleNamespaces = new(); + public readonly HashSet ModuleCommandCalls = new(); + public readonly HashSet ModuleCommandParameters = new(); + public readonly HashSet ModuleExternalCommand = new(); + + public double LineOfCodeMedian => CalculateLineOfCodeMedian(); + public double CommandParameterMedian => CalculateCommandParameterMedian(); + + + private double CalculateLineOfCodeMedian() + => ModuleCommands.Select(command => (double) command.LinesOfCode).Median(); + + private double CalculateCommandParameterMedian() + => ModuleCommands.Select(command => (double) command.Parameters.Count).Median(); + + public ModuleDefinition(string modulePath, ILogger? logger = null) + { + _logger = logger; + _psModuleInfo = LoadModuleFromPath(modulePath); + + Name = _psModuleInfo.Name; + Path = _psModuleInfo.ModuleBase; + + ModuleCommands = CreateModuleCommandList(_psModuleInfo); + } + + public ModuleCommand? GetModuleCommand(string name) + => ModuleCommands.FirstOrDefault(command => command.Name == name); + + public ModuleExternalCommand? GetModuleExternalCommand(string name) + => ModuleExternalCommand.FirstOrDefault(command => command.Name == name); + + public ModuleNamespace AddModuleNamespace(ModuleCommand command) + { + var namespaceString = ExtractNamespaceString(command); + + var moduleNamespace = ModuleNamespaces.FirstOrDefault(moduleNamespace => moduleNamespace.Id == namespaceString); + if (moduleNamespace is null) + moduleNamespace = CreateModuleNamespace(namespaceString); + + command.Namespace = moduleNamespace; + + return moduleNamespace; + } + + public HashSet AddModuleCommands(HashSet moduleCommands) + { + _logger?.LogDebug("{object}: Module {count} commands added", nameof(ModuleDefinition), moduleCommands.Count); + + foreach (var command in moduleCommands) + { + ModuleCommands.Add(command); + } + return ModuleCommands; + } + + public ModuleExternalCommand AddExternalModuleCommand(string name) + { + var moduleCommand = GetModuleExternalCommand(name); + if (moduleCommand is null) + { + moduleCommand = new ModuleExternalCommand(name); + ModuleExternalCommand.Add(moduleCommand); + } + + return moduleCommand; + } + + public ModuleCommandCall AddModuleCommandCall(ModuleCommandCall moduleCommand) + { + ModuleCommandCalls.Add(moduleCommand); + return moduleCommand; + } + + public ModuleCommandParameter? GetModuleCommandParameter(string commandName, string parameterName) + { + var moduleCommand = GetModuleCommand(commandName); + if (moduleCommand != null) + return moduleCommand.Parameters.FirstOrDefault(param => param.Name == parameterName); + + var externalCommand = GetModuleExternalCommand(commandName); + if (externalCommand != null) + return externalCommand.Parameters.FirstOrDefault(param => param.Name == parameterName); + + return null; + } + + public ModuleExternalCommand AddModuleExternalCommandCall(string name, IModuleCommand calledBy) + { + var moduleCommand = GetModuleExternalCommand(name); + if (moduleCommand is null) + { + moduleCommand = new ModuleExternalCommand(name); + ModuleExternalCommand.Add(moduleCommand); + } + + ModuleCommandCalls.Add(new ModuleCommandCall(calledBy, moduleCommand)); + return moduleCommand; + } + + private string ExtractNamespaceString(ModuleCommand command) => Regex.Match(command.CommandFile, @$"(?<={Regex.Escape(Path)}).*(?=\\.+\.ps1)").Value.Trim('\\'); + + private PSModuleInfo LoadModuleFromPath(string modulePath) + { + _logger?.LogDebug("{object}: Try to create powershell and import module {modulePath}", nameof(ModuleDefinition), modulePath); + var powerShell = PowerShellRunner.CreatePowerShell(_logger); + + powerShell.AddScript("Set-ExecutionPolicy -Scope Process -ExecutionPolicy Unrestricted; Get-ExecutionPolicy;"); + powerShell.AddScript($"Import-Module {modulePath} -PassThru"); + + var moduleInfo = powerShell.Invoke(); + + if (moduleInfo.FirstOrDefault() is null) + throw new ModuleInvalidException(); + + return moduleInfo.First(); + } + + private ModuleNamespace CreateModuleNamespace(string namespaceString) + { + var moduleNamespace = new ModuleNamespace(namespaceString); + ModuleNamespaces.Add(moduleNamespace); + _logger?.LogTrace("{object}: Namespace {name} added", nameof(ModuleDefinition), namespaceString); + return moduleNamespace; + } + + private HashSet CreateModuleCommandList(PSModuleInfo moduleInfo) + { + var moduleCommandList = new HashSet(); + foreach (var function in moduleInfo.ExportedFunctions) + { + var command = function.Value; + var moduleCommand = new ModuleCommand(command.Name, command.Definition, command.ScriptBlock.File) + { + LinesOfCode = CalculateLinesOfCode(command) + }; + + AddModuleNamespace(moduleCommand); + CreateModuleCommandParameterList(command, moduleCommand); + + moduleCommandList.Add(moduleCommand); + } + return moduleCommandList; + } + + private static int CalculateLinesOfCode(FunctionInfo command) + => (command.ScriptBlock.StartPosition.EndLine - command.ScriptBlock.StartPosition.StartLine); + + private void CreateModuleCommandParameterList(FunctionInfo command, ModuleCommand moduleCommand) + { + try + { + foreach (var param in command.Parameters) + { + if (DEFAULT_PARAMETERS.Contains(param.Key)) + continue; + + var moduleCommandParameter = new ModuleCommandParameter(moduleCommand, param.Value.Name, param.Value.ParameterType.Name); + moduleCommand.Parameters.Add(moduleCommandParameter); + ModuleCommandParameters.Add(moduleCommandParameter); + + _logger?.LogTrace("{object}: Parameter {parameterName} added to {commandName}", + nameof(ModuleDefinition), param.Value.Name, command.Name); + } + } + catch (Exception ex) + { + _logger?.LogDebug("{object}: Exception while trying to get command parameter details [{message}]", nameof(ModuleDefinition), ex.Message); + } + } +} diff --git a/src/PowerShell.ModuleAnalyzer.Core/Model/ModuleExternalCommand.cs b/src/PowerShell.ModuleAnalyzer.Core/Model/ModuleExternalCommand.cs new file mode 100644 index 0000000..7e469f2 --- /dev/null +++ b/src/PowerShell.ModuleAnalyzer.Core/Model/ModuleExternalCommand.cs @@ -0,0 +1,35 @@ +namespace ModuleAnalyzer.Core.Model; + +public class ModuleExternalCommand : IModuleCommand +{ + public string Name { get; private set; } + public int TotalReferencedBy { get; set; } = 0; + public bool IsExternal { get; } + public string _commandFile { get; private set; } = "External"; + + public ModuleNamespace Namespace => new(""); + + public ICollection Parameters { get; } = new HashSet(); + + public ICollection References { get; } = new HashSet(); + + public ICollection ReferencedBy { get; } = new HashSet(); + + public ModuleExternalCommand(string name) + { + Name = name; + IsExternal = true; + } + + public Dictionary GetParametersAsDictionary() + { + var parameters = new Dictionary(); + foreach (var parameter in Parameters) + { + parameters.Add(parameter.Name, parameter.Type); + } + return parameters; + } + + IDictionary IModuleCommand.GetParametersAsDictionary() => throw new NotImplementedException(); +} diff --git a/src/PowerShell.ModuleAnalyzer.Core/Model/ModuleNamespace.cs b/src/PowerShell.ModuleAnalyzer.Core/Model/ModuleNamespace.cs new file mode 100644 index 0000000..c92b17b --- /dev/null +++ b/src/PowerShell.ModuleAnalyzer.Core/Model/ModuleNamespace.cs @@ -0,0 +1,11 @@ +namespace ModuleAnalyzer.Core.Model; + +public class ModuleNamespace +{ + public string Id { get; set; } + + public ModuleNamespace(string @namespace) + { + Id = @namespace; + } +} diff --git a/src/PowerShell.ModuleAnalyzer.Core/Output/ShortStatFormatter.cs b/src/PowerShell.ModuleAnalyzer.Core/Output/ShortStatFormatter.cs new file mode 100644 index 0000000..a16c1be --- /dev/null +++ b/src/PowerShell.ModuleAnalyzer.Core/Output/ShortStatFormatter.cs @@ -0,0 +1,46 @@ +using Microsoft.Extensions.Logging; +using ModuleAnalyzer.Core.Interfaces; +using ModuleAnalyzer.Core.Model; + +namespace ModuleAnalyzer.Core.Output; + +public class ShortStatFormatter : IAnalyzerOutputFormatter +{ + private readonly ILogger _logger; + + public ShortStatFormatter(ILogger? logger = null) + { + _logger = logger ?? CreateDefaultLogger(); + } + + public void CreateAnalyzerOutupt(ModuleDefinition moduleDefinition) + { + _logger.LogInformation("Short Stat -----------------------------------------"); + _logger.LogInformation(ToListItem("Name", moduleDefinition.Name)); + _logger.LogInformation(ToListItem("Path", moduleDefinition.Path)); + _logger.LogInformation(ToListItem("Total commands", moduleDefinition.ModuleCommands.Count)); + _logger.LogInformation(ToListItem("Total invoked commands", moduleDefinition.ModuleCommandCalls.Count)); + _logger.LogInformation(ToListItem("Total parameters", moduleDefinition.ModuleCommandParameters.Count)); + _logger.LogInformation(ToListItem("Total namespaces", moduleDefinition.ModuleNamespaces.Count)); + _logger.LogInformation(ToListItem("Total external commands", moduleDefinition.ModuleExternalCommand.Count)); + _logger.LogInformation(ToListItem("Line of code per command (median)", moduleDefinition.LineOfCodeMedian)); + _logger.LogInformation(ToListItem("Parameters per command (median)", moduleDefinition.CommandParameterMedian)); + _logger.LogInformation("----------------------------------------------------"); + } + + private static string ToListItem(string key, object value) => string.Format("{0,-35}: {1}", key, value); + + private static ILogger CreateDefaultLogger() + { + return LoggerFactory.Create(builder => + { + builder.AddSimpleConsole(options => + { + options.SingleLine = true; + options.TimestampFormat = "hh:mm:ss "; + options.IncludeScopes = false; + }) + .SetMinimumLevel(LogLevel.Debug); + }).CreateLogger(""); + } +} diff --git a/src/PowerShell.ModuleAnalyzer.Core/PowerShell.ModuleAnalyzer.Core.csproj b/src/PowerShell.ModuleAnalyzer.Core/PowerShell.ModuleAnalyzer.Core.csproj new file mode 100644 index 0000000..301cd56 --- /dev/null +++ b/src/PowerShell.ModuleAnalyzer.Core/PowerShell.ModuleAnalyzer.Core.csproj @@ -0,0 +1,204 @@ + + + + Library + net6.0 + enable + enable + ModuleAnalyzer.Core + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/PowerShell.ModuleAnalyzer.Core/Runner/PowerShellRunner.cs b/src/PowerShell.ModuleAnalyzer.Core/Runner/PowerShellRunner.cs new file mode 100644 index 0000000..e2953d8 --- /dev/null +++ b/src/PowerShell.ModuleAnalyzer.Core/Runner/PowerShellRunner.cs @@ -0,0 +1,38 @@ +using Microsoft.Extensions.Logging; +using System.Management.Automation; + +namespace ModuleAnalyzer.Core.Runner; + +internal class PowerShellRunner +{ + public static PowerShell CreatePowerShell(ILogger? logger = null) + { + var powerShell = PowerShell.Create(); + + powerShell.Streams.Error.DataAdded += (sender, e) + => LogErrror(sender, e, logger); + + powerShell.Streams.Information.DataAdded += (sender, e) + => LogProgress(sender, e, logger); + + powerShell.Streams.Progress.DataAdded += (sender, e) + => LogProgress(sender, e, logger); + + powerShell.Streams.Warning.DataAdded += (sender, e) + => LogProgress(sender, e, logger); + + return powerShell; + } + + private static void LogProgress(object? sender, DataAddedEventArgs e, ILogger? logger) + { + if (sender is PSDataCollection psDataCollection) + logger?.LogTrace("PowerShell Stream: {data}", psDataCollection[e.Index]?.ToString()); + } + + private static void LogErrror(object? sender, DataAddedEventArgs e, ILogger? logger) + { + if (sender is PSDataCollection psDataCollection) + logger?.LogCritical("PowerShell Stream: {data}", psDataCollection[e.Index]?.ToString()); + } +} diff --git a/src/PowerShell.ModuleAnalyzer.Core/Visitor/ModuleCommandVisitor.cs b/src/PowerShell.ModuleAnalyzer.Core/Visitor/ModuleCommandVisitor.cs new file mode 100644 index 0000000..51ac96d --- /dev/null +++ b/src/PowerShell.ModuleAnalyzer.Core/Visitor/ModuleCommandVisitor.cs @@ -0,0 +1,16 @@ +using ModuleAnalyzer.Core.Model; +using System.Management.Automation.Language; + +namespace ModuleAnalyzer.Core.Visitor; + +public abstract class ModuleCommandVisitor : AstVisitor2 +{ + protected readonly ModuleDefinition _moduleRepository; + protected readonly IModuleCommand _moduleCommand; + + public ModuleCommandVisitor(IModuleCommand moduleCommand, ModuleDefinition moduleRepository) + { + _moduleRepository = moduleRepository; + _moduleCommand = moduleCommand; + } +} diff --git a/src/PowerShell.ModuleAnalyzer.Core/Visitor/ModuleCommandVisitorFactory.cs b/src/PowerShell.ModuleAnalyzer.Core/Visitor/ModuleCommandVisitorFactory.cs new file mode 100644 index 0000000..e1e96d7 --- /dev/null +++ b/src/PowerShell.ModuleAnalyzer.Core/Visitor/ModuleCommandVisitorFactory.cs @@ -0,0 +1,8 @@ +using ModuleAnalyzer.Core.Model; + +namespace ModuleAnalyzer.Core.Visitor; + +public abstract class ModuleCommandVisitorFactory +{ + public abstract ModuleCommandVisitor Create(ModuleCommand moduleCommand, ModuleDefinition moduleRepository); +} diff --git a/src/PowerShell.ModuleAnalyzer.Core/Visitor/ModuleDefaultCommandVisitor.cs b/src/PowerShell.ModuleAnalyzer.Core/Visitor/ModuleDefaultCommandVisitor.cs new file mode 100644 index 0000000..ca16d47 --- /dev/null +++ b/src/PowerShell.ModuleAnalyzer.Core/Visitor/ModuleDefaultCommandVisitor.cs @@ -0,0 +1,62 @@ +using ModuleAnalyzer.Core.Model; +using System.Management.Automation.Language; + +namespace ModuleAnalyzer.Core.Visitor; + +public class ModuleDefaultCommandVisitor : ModuleCommandVisitor +{ + private ModuleCommandCall? _moduleCommandCall; + private IModuleCommand? _moduleTargetCommand; + + public ModuleDefaultCommandVisitor(IModuleCommand moduleCommand, ModuleDefinition moduleDefinition) : + base(moduleCommand, moduleDefinition) + { + + } + + public override AstVisitAction VisitCommand(CommandAst commandAst) + { + _moduleTargetCommand = RegisterModuleCommand(commandAst); + + _moduleCommandCall = CreateModuleCommandCall(_moduleTargetCommand); + + _moduleRepository.AddModuleCommandCall(_moduleCommandCall); + + return AstVisitAction.Continue; + } + + public override AstVisitAction VisitCommandParameter(CommandParameterAst parameterAst) + { + RegisterModuleCommandCallParameter(parameterAst.ParameterName); + + return AstVisitAction.Continue; + } + + private void RegisterModuleCommandCallParameter(string parameterName) + { + if(_moduleCommandCall is not null) + { + var moduleCommandParameter = _moduleRepository.GetModuleCommandParameter(_moduleCommandCall.Target.Name, parameterName); + + if (moduleCommandParameter is not null) + _moduleCommandCall.Parameters.Add(moduleCommandParameter); + } + } + + private IModuleCommand RegisterModuleCommand(CommandAst commandAst) + { + var targetCommandName = commandAst.GetCommandName(); + + IModuleCommand? targetCommand = _moduleRepository.GetModuleCommand(targetCommandName); + + if (targetCommand is null) + targetCommand = _moduleRepository.AddExternalModuleCommand(targetCommandName); + + return targetCommand; + } + + private ModuleCommandCall CreateModuleCommandCall(IModuleCommand targetCommand) + => new ModuleCommandCall(_moduleCommand, targetCommand); + +} + diff --git a/src/PowerShell.ModuleAnalyzer.Core/Visitor/ModuleDefaultCommandVisitorFactory.cs b/src/PowerShell.ModuleAnalyzer.Core/Visitor/ModuleDefaultCommandVisitorFactory.cs new file mode 100644 index 0000000..4fe5df3 --- /dev/null +++ b/src/PowerShell.ModuleAnalyzer.Core/Visitor/ModuleDefaultCommandVisitorFactory.cs @@ -0,0 +1,11 @@ +using ModuleAnalyzer.Core.Model; + +namespace ModuleAnalyzer.Core.Visitor; + +public class ModuleDefaultCommandVisitorFactory : ModuleCommandVisitorFactory +{ + public override ModuleDefaultCommandVisitor Create(ModuleCommand moduleCommand, ModuleDefinition moduleRepository) + { + return new ModuleDefaultCommandVisitor(moduleCommand, moduleRepository); + } +} diff --git a/src/PowerShell.ModuleAnalyzer.Formatter/CsvCompactFormatter.cs b/src/PowerShell.ModuleAnalyzer.Formatter/CsvCompactFormatter.cs new file mode 100644 index 0000000..39e5bbb --- /dev/null +++ b/src/PowerShell.ModuleAnalyzer.Formatter/CsvCompactFormatter.cs @@ -0,0 +1,75 @@ +using ModuleAnalyzer.Core.Interfaces; +using ModuleAnalyzer.Core.Model; + +namespace ModuleAnalyzer.Formatter; + +public class CsvCompactFormatter : IAnalyzerOutputFormatter +{ + private readonly string _destinationFile; + + public CsvCompactFormatter(string destinationFile) + { + _destinationFile = destinationFile; + } + + public void CreateAnalyzerOutupt(ModuleDefinition moduleDefinition) + => WriteToFile(_destinationFile, CreateCsvContent(moduleDefinition.ModuleCommands)); + + private static List CreateCsvContent(HashSet moduleCommands) + { + var csvContent = new List() + { + CreateCsvContentHeader() + }; + + moduleCommands.ToList().ForEach(command => + { + csvContent.Add(CreateCsvContentLine(command)); + }); + + return csvContent; + } + + private static string CreateCsvContentHeader() + => string.Join(";", new List + { + "Name", + "Namespace", + "Invokes (Module)", + "Invokes (External)", + "InvokedBy", + "References (Unique)", + "ReferencedBy (Unique)", + "StabilityIndex", + "LinesOfCode", + "Parameters" + }); + + private static string CreateCsvContentLine(ModuleCommand moduleCommand) + => string.Join(";", new List + { + moduleCommand.Name, + moduleCommand.Namespace?.Id, + moduleCommand.References.Where(cmd => !cmd.Target.IsExternal).ToList().Count.ToString(), + moduleCommand.References.Where(cmd => cmd.Target.IsExternal).ToList().Count.ToString(), + moduleCommand.ReferencedBy.Count.ToString(), + moduleCommand.TotalUniqueReferenced.ToString(), + moduleCommand.TotalUniqueReferencedBy.ToString(), + moduleCommand.StabilityIndex.ToString(), + moduleCommand.LinesOfCode.ToString(), + CreateCsvParameterColumn(moduleCommand.Parameters) + }); + + private static string CreateCsvParameterColumn(ICollection moduleCommandParameters) + { + var paramlist = new List(); + foreach(var param in moduleCommandParameters) + paramlist.Add($"{param.Name} [{param.Type}]({param.References})"); + + return string.Join(" / ", paramlist); + } + + private static void WriteToFile(string destinationFile, List contentLines) + => File.WriteAllLines(destinationFile, contentLines); + +} diff --git a/src/PowerShell.ModuleAnalyzer.Formatter/CsvFormatter.cs b/src/PowerShell.ModuleAnalyzer.Formatter/CsvFormatter.cs new file mode 100644 index 0000000..600a89d --- /dev/null +++ b/src/PowerShell.ModuleAnalyzer.Formatter/CsvFormatter.cs @@ -0,0 +1,57 @@ +using ModuleAnalyzer.Core.Interfaces; +using ModuleAnalyzer.Core.Model; + +namespace ModuleAnalyzer.Formatter; + +public class CsvFormatter : IAnalyzerOutputFormatter +{ + private readonly string _destinationFile; + + public CsvFormatter(string destinationFile) + { + _destinationFile = destinationFile; + } + + public void CreateAnalyzerOutupt(ModuleDefinition moduleDefinition) + => WriteToFile(CreateCsvContent(moduleDefinition.ModuleCommandCalls)); + + private static List CreateCsvContent(ICollection moduleCommandCalls) + { + var csvContent = new List() { + CreateCsvContentHeader() + }; + + moduleCommandCalls.Where(s => !s.Target.IsExternal).ToList().ForEach(command => + { + csvContent.Add(CreateCsvContentLine(command)); + }); + + return csvContent; + } + + private static string CreateCsvContentLine(ModuleCommandCall commandCall) + { + var columns = new List + { + commandCall.Source.Name, + commandCall.Source.Namespace.Id, + commandCall.Target.Name + }; + + var parameters = commandCall.Parameters.Select(param => param.Name); + columns.AddRange(parameters); + + return string.Join(";", columns); + } + + private static string CreateCsvContentHeader() + => string.Join(";", new List + { + "Name", + "Namespace", + "Target", + "Parameters" + }); + + private void WriteToFile(List contentLines) => File.WriteAllLines(_destinationFile, contentLines); +} diff --git a/src/PowerShell.ModuleAnalyzer.Formatter/CsvFormatterExtension.cs b/src/PowerShell.ModuleAnalyzer.Formatter/CsvFormatterExtension.cs new file mode 100644 index 0000000..a1bf682 --- /dev/null +++ b/src/PowerShell.ModuleAnalyzer.Formatter/CsvFormatterExtension.cs @@ -0,0 +1,16 @@ +using ModuleAnalyzer.Core.Anaylzer; + +namespace ModuleAnalyzer.Formatter; + +public static class CsvFormatterExtension +{ + public static ModuleAnalyzerBuilder AddCSVFormatter(this ModuleAnalyzerBuilder moduleAnalyzerBuilder, string destinationPath, bool compact = false) + { + if (compact) + moduleAnalyzerBuilder.AddOutputFormatter(new CsvCompactFormatter(destinationPath)); + else + moduleAnalyzerBuilder.AddOutputFormatter(new CsvFormatter(destinationPath)); + + return moduleAnalyzerBuilder; + } +} diff --git a/src/PowerShell.ModuleAnalyzer.Formatter/DgmlFormatter.cs b/src/PowerShell.ModuleAnalyzer.Formatter/DgmlFormatter.cs new file mode 100644 index 0000000..d8da560 --- /dev/null +++ b/src/PowerShell.ModuleAnalyzer.Formatter/DgmlFormatter.cs @@ -0,0 +1,67 @@ +using ModuleAnalyzer.Core.Interfaces; +using ModuleAnalyzer.Core.Model; +using OpenSoftware.DgmlTools; +using OpenSoftware.DgmlTools.Analyses; +using OpenSoftware.DgmlTools.Builders; +using OpenSoftware.DgmlTools.Model; + +namespace ModuleAnalyzer.Formatter; + +internal class DgmlFormatter : IAnalyzerOutputFormatter +{ + private readonly string _destinationFile; + + internal DgmlFormatter(string destinationFile) + { + _destinationFile = destinationFile; + } + + public void CreateAnalyzerOutupt(ModuleDefinition moduleDefinition) + { + var commandCalls = FilterInternalModuleCalls(moduleDefinition); + var builder = new DgmlBuilder(new HubNodeAnalysis(), + new NodeReferencedAnalysis(), + new CategoryColorAnalysis(), + new DgmlLinkReferenceWeightAnalyzer()) { + CategoryBuilders = new CategoryBuilder[] + { + new CategoryBuilder(x => new Category + { + Id = x.Source.Namespace.Id, + Label = x.Source.Namespace.Id + }) + }, + NodeBuilders = new NodeBuilder[] + { + new NodeBuilder( + x => new Node + { + Id = x.Source.Name, + Label = x.Source.Name, + Category = x.Source.Namespace.Id, + Group = x.Source.Namespace.Id, + }) + }, + LinkBuilders = new LinkBuilder[] + { + new LinkBuilder( + x => new Link + { + Source = x.Source.Name, + Target = x.Target.Name, + Stroke = "#6BB3FF" + }) + } + }; + var graph = builder.Build(commandCalls); + + graph.WriteToFile(_destinationFile); + } + + private static List FilterInternalModuleCalls(ModuleDefinition moduleDefinition) + { + return moduleDefinition.ModuleCommandCalls + .Where(commands => !commands.Target.IsExternal) + .ToList(); + } +} diff --git a/src/PowerShell.ModuleAnalyzer.Formatter/DgmlFormatterExtension.cs b/src/PowerShell.ModuleAnalyzer.Formatter/DgmlFormatterExtension.cs new file mode 100644 index 0000000..f0b2116 --- /dev/null +++ b/src/PowerShell.ModuleAnalyzer.Formatter/DgmlFormatterExtension.cs @@ -0,0 +1,17 @@ +using ModuleAnalyzer.Core.Anaylzer; + +namespace ModuleAnalyzer.Formatter; + +public static class DgmlFormatterExtension +{ + /// + /// Formatter for the directed graph markup language. + /// The formatter generates a module graph. + /// + /// Destination path for the result + public static ModuleAnalyzerBuilder AddDgmlFormatter(this ModuleAnalyzerBuilder moduleAnalyzerBuilder, string destinationPath) + { + moduleAnalyzerBuilder.AddOutputFormatter(new DgmlFormatter(destinationPath)); + return moduleAnalyzerBuilder; + } +} diff --git a/src/PowerShell.ModuleAnalyzer.Formatter/DgmlLinkReferenceWeightAnalyzer.cs b/src/PowerShell.ModuleAnalyzer.Formatter/DgmlLinkReferenceWeightAnalyzer.cs new file mode 100644 index 0000000..993c8b3 --- /dev/null +++ b/src/PowerShell.ModuleAnalyzer.Formatter/DgmlLinkReferenceWeightAnalyzer.cs @@ -0,0 +1,95 @@ +using OpenSoftware.DgmlTools.Analyses; +using OpenSoftware.DgmlTools.Model; + +namespace ModuleAnalyzer.Formatter; + +internal class DgmlLinkReferenceWeightAnalyzer : IGraphAnalysis +{ + public void Execute(DirectedGraph graph) + { + + } + + public IEnumerable GetProperties(DirectedGraph graph) + { + return new List { + new Property + { + Id = "RefWeight", + DataType = "System.Int32", + Label = "RefWeight", + Description = "RefWeight" + }, + new Property + { + Id = "Description", + DataType = "System.Int32", + Label = "Description", + Description = "Description" + } + }; + } + + public IEnumerable