diff --git a/.github/workflows/build_test.yml b/.github/workflows/build_test.yml index c3522c69..b8ee2466 100644 --- a/.github/workflows/build_test.yml +++ b/.github/workflows/build_test.yml @@ -39,7 +39,9 @@ jobs: - name: Build run: dotnet build --configuration Release --no-restore src/Codelyzer.sln - name: Test - run: dotnet test --configuration Release --no-build --no-restore --verbosity normal src/Codelyzer.sln + run: dotnet test --configuration Release --no-build --no-restore --verbosity normal src/Codelyzer.sln --filter "FullyQualifiedName!~Codelyzer.Analysis.Workspace.Tests" + - name: Tests for Workspaces + run: dotnet test --configuration Release --no-build --no-restore --verbosity normal src/Codelyzer.sln --filter "FullyQualifiedName~Codelyzer.Analysis.Workspace.Tests" - name: Pack if: ${{ github.event_name == 'push' }} run: dotnet pack --configuration Release --no-restore -o dist src/Codelyzer.sln diff --git a/src/Analysis/Codelyzer.Analysis/Analyzer/CSharpAnalyzer.cs b/src/Analysis/Codelyzer.Analysis.Analyzer/CSharpAnalyzer.cs similarity index 55% rename from src/Analysis/Codelyzer.Analysis/Analyzer/CSharpAnalyzer.cs rename to src/Analysis/Codelyzer.Analysis.Analyzer/CSharpAnalyzer.cs index a20a1e86..febbbe0a 100644 --- a/src/Analysis/Codelyzer.Analysis/Analyzer/CSharpAnalyzer.cs +++ b/src/Analysis/Codelyzer.Analysis.Analyzer/CSharpAnalyzer.cs @@ -1,29 +1,23 @@ -using Codelyzer.Analysis.Build; -using Codelyzer.Analysis.Common; +using Codelyzer.Analysis.Common; using Codelyzer.Analysis.CSharp; using Codelyzer.Analysis.Model; using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Codelyzer.Analysis.Model.Build; -namespace Codelyzer.Analysis.Analyzer +namespace Codelyzer.Analysis.Analyzers { class CSharpAnalyzer : LanguageAnalyzer { public CSharpAnalyzer(AnalyzerConfiguration configuration, ILogger logger) - : base(configuration, logger) - { - } + : base(configuration, logger) {} public override string Language => LanguageOptions.CSharp; public override string ProjectFilePath { set; get; } public override RootUstNode AnalyzeFile(SourceFileBuildResult sourceFileBuildResult, string projectRootPath) { - CodeContext codeContext = new CodeContext(sourceFileBuildResult.PrePortSemanticModel, + var codeContext = new CodeContext(sourceFileBuildResult.PrePortSemanticModel, sourceFileBuildResult.SemanticModel, sourceFileBuildResult.SyntaxTree, projectRootPath, @@ -33,14 +27,15 @@ public override RootUstNode AnalyzeFile(SourceFileBuildResult sourceFileBuildRes Logger.LogDebug("Analyzing: " + sourceFileBuildResult.SourceFileFullPath); - using CSharpRoslynProcessor processor = new CSharpRoslynProcessor(codeContext); - - var result = (RootUstNode) processor.Visit(codeContext.SyntaxTree.GetRoot()); + using (var processor = new CSharpRoslynProcessor(codeContext)) + { + var result = (RootUstNode)processor.Visit(codeContext.SyntaxTree.GetRoot()); - result.LinesOfCode = sourceFileBuildResult.SyntaxTree.GetRoot().DescendantTrivia() - .Where(t => t.IsKind(SyntaxKind.EndOfLineTrivia)).Count(); + result.LinesOfCode = sourceFileBuildResult.SyntaxTree.GetRoot() + .DescendantTrivia().Count(t => t.IsKind(SyntaxKind.EndOfLineTrivia)); - return result as RootUstNode; + return result as RootUstNode; + } } } } diff --git a/src/Analysis/Codelyzer.Analysis/Analyzer/CSharpAnalyzerFactory.cs b/src/Analysis/Codelyzer.Analysis.Analyzer/CSharpAnalyzerFactory.cs similarity index 50% rename from src/Analysis/Codelyzer.Analysis/Analyzer/CSharpAnalyzerFactory.cs rename to src/Analysis/Codelyzer.Analysis.Analyzer/CSharpAnalyzerFactory.cs index ac97f4f6..e4ffcff2 100644 --- a/src/Analysis/Codelyzer.Analysis/Analyzer/CSharpAnalyzerFactory.cs +++ b/src/Analysis/Codelyzer.Analysis.Analyzer/CSharpAnalyzerFactory.cs @@ -5,21 +5,21 @@ using System.Text; using System.Threading.Tasks; -namespace Codelyzer.Analysis.Analyzer +namespace Codelyzer.Analysis.Analyzers { - class CSharpAnalyzerFactory : LanguageAnalyzerFactory + public class CSharpAnalyzerFactory : LanguageAnalyzerFactory { - protected readonly AnalyzerConfiguration _analyzerConfiguration; - protected readonly ILogger _logger; + protected readonly AnalyzerConfiguration AnalyzerConfiguration; + protected readonly ILogger Logger; public CSharpAnalyzerFactory(AnalyzerConfiguration analyzerConfiguration, ILogger logger) { - _analyzerConfiguration = analyzerConfiguration; - _logger = logger; + AnalyzerConfiguration = analyzerConfiguration; + Logger = logger; } public override LanguageAnalyzer GetLanguageAnalyzer() { - return new CSharpAnalyzer(_analyzerConfiguration, _logger); + return new CSharpAnalyzer(AnalyzerConfiguration, Logger); } } diff --git a/src/Analysis/Codelyzer.Analysis.Analyzer/CodeAnalyzer.cs b/src/Analysis/Codelyzer.Analysis.Analyzer/CodeAnalyzer.cs new file mode 100644 index 00000000..89faf823 --- /dev/null +++ b/src/Analysis/Codelyzer.Analysis.Analyzer/CodeAnalyzer.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Codelyzer.Analysis.Common; +using Codelyzer.Analysis.Model; +using Codelyzer.Analysis.Model.Build; +using Codelyzer.Analysis.Workspace; +using Microsoft.CodeAnalysis; +using Microsoft.Extensions.Logging; + +namespace Codelyzer.Analysis.Analyzers +{ + public class CodeAnalyzer + { + protected readonly AnalyzerConfiguration AnalyzerConfiguration; + protected readonly ILogger Logger; + + public CodeAnalyzer(AnalyzerConfiguration configuration, ILogger logger) + { + AnalyzerConfiguration = configuration; + Logger = logger; + } + + public async Task> Analyze(Solution solution) + { + var projectBuildResults = await new WorkspaceHelper(Logger).GetProjectBuildResults(solution); + return await Analyze(projectBuildResults); + } + + public async IAsyncEnumerable AnalyzeGeneratorAsync(Solution solution) + { + var projectBuildResultEnumerator = new WorkspaceHelper(Logger) + .GetProjectBuildResultsGeneratorAsync(solution) + .GetAsyncEnumerator(); + try + { + while (await projectBuildResultEnumerator.MoveNextAsync().ConfigureAwait(false)) + { + var projectBuildResult = projectBuildResultEnumerator.Current; + yield return await AnalyzeProjectBuildResult(projectBuildResult); + } + } + finally + { + await projectBuildResultEnumerator.DisposeAsync(); + } + } + + public async Task> Analyze(IEnumerable projectBuildResults) + { + var analyzerResults = new List(); + + foreach (var projectBuildResult in projectBuildResults) + { + analyzerResults.Add(await AnalyzeProjectBuildResult(projectBuildResult)); + } + await GenerateOptionalOutput(analyzerResults); + return analyzerResults; + } + + public async Task AnalyzeProjectBuildResult(ProjectBuildResult projectBuildResult) + { + var workspaceResult = await Task.Run(() => AnalyzeProject(projectBuildResult)); + workspaceResult.ProjectGuid = projectBuildResult.ProjectGuid; + workspaceResult.ProjectType = projectBuildResult.ProjectType; + + //Generate Output result + return AnalyzerConfiguration.MetaDataSettings.LoadBuildData + ? new AnalyzerResult() + { + ProjectResult = workspaceResult, + ProjectBuildResult = projectBuildResult + } + : new AnalyzerResult() + { + ProjectResult = workspaceResult + }; + } + + public async Task AnalyzeFile( + string filePath, + ProjectBuildResult incrementalBuildResult, + AnalyzerResult analyzerResult) + { + var newSourceFileBuildResult = + incrementalBuildResult.SourceFileBuildResults.FirstOrDefault(sourceFile => + sourceFile.SourceFileFullPath == filePath); + var languageAnalyzer = GetLanguageAnalyzerByFileType(Path.GetExtension(filePath)); + var fileAnalysis = languageAnalyzer.AnalyzeFile(newSourceFileBuildResult, + analyzerResult.ProjectResult.ProjectRootPath); + analyzerResult.ProjectResult.SourceFileResults.Add(fileAnalysis); + return analyzerResult; + } + + public async Task GenerateOptionalOutput(List analyzerResults) + { + if (AnalyzerConfiguration.ExportSettings.GenerateJsonOutput) + { + Directory.CreateDirectory(AnalyzerConfiguration.ExportSettings.OutputPath); + foreach (var analyzerResult in analyzerResults) + { + Logger.LogDebug("Generating Json file for " + analyzerResult.ProjectResult.ProjectName); + var jsonOutput = SerializeUtils.ToJson(analyzerResult.ProjectResult); + var jsonFilePath = await FileUtils.WriteFileAsync(AnalyzerConfiguration.ExportSettings.OutputPath, + analyzerResult.ProjectResult.ProjectName + ".json", jsonOutput); + analyzerResult.OutputJsonFilePath = jsonFilePath; + Logger.LogDebug("Generated Json file " + jsonFilePath); + } + } + } + + public LanguageAnalyzer GetLanguageAnalyzerByProjectType(string projType) + { + LanguageAnalyzerFactory languageAnalyzerFactory; + switch (projType.ToLower()) + { + case ".vbproj": + languageAnalyzerFactory = new VbAnalyzerFactory(AnalyzerConfiguration, Logger); + break; + case ".csproj": + languageAnalyzerFactory = new CSharpAnalyzerFactory(AnalyzerConfiguration, Logger); + break; + + default: + throw new Exception($"invalid project type {projType}"); + } + return languageAnalyzerFactory.GetLanguageAnalyzer(); + } + + private ProjectWorkspace AnalyzeProject(ProjectBuildResult projectResult) + { + // Logger.LogDebug("Analyzing the project: " + projectResult.ProjectPath); + var projType = Path.GetExtension(projectResult.ProjectPath)?.ToLower(); + LanguageAnalyzer languageAnalyzer = GetLanguageAnalyzerByProjectType(projType); + ProjectWorkspace workspace = new ProjectWorkspace(projectResult.ProjectPath) + { + SourceFiles = new UstList(projectResult.SourceFiles), + BuildErrors = projectResult.BuildErrors, + BuildErrorsCount = projectResult.BuildErrors.Count + }; + + if (AnalyzerConfiguration.MetaDataSettings.ReferenceData) + { + workspace.ExternalReferences = projectResult.ExternalReferences; + } + workspace.TargetFramework = projectResult.TargetFramework; + workspace.TargetFrameworks = projectResult.TargetFrameworks; + workspace.LinesOfCode = 0; + foreach (var fileBuildResult in projectResult.SourceFileBuildResults) + { + var fileAnalysis = languageAnalyzer.AnalyzeFile(fileBuildResult, workspace.ProjectRootPath); + workspace.LinesOfCode += fileAnalysis.LinesOfCode; + workspace.SourceFileResults.Add(fileAnalysis); + } + + return workspace; + } + + public LanguageAnalyzer GetLanguageAnalyzerByFileType(string fileType) + { + LanguageAnalyzerFactory languageAnalyzerFactory; + switch (fileType.ToLower()) + { + case ".vb": + languageAnalyzerFactory = new VbAnalyzerFactory(AnalyzerConfiguration, Logger); + break; + case ".cs": + languageAnalyzerFactory = new CSharpAnalyzerFactory(AnalyzerConfiguration, Logger); + break; + + default: + throw new Exception($"invalid project type {fileType}"); + } + return languageAnalyzerFactory.GetLanguageAnalyzer(); + + } + } +} diff --git a/src/Analysis/Codelyzer.Analysis.Analyzer/Codelyzer.Analysis.Analyzers.csproj b/src/Analysis/Codelyzer.Analysis.Analyzer/Codelyzer.Analysis.Analyzers.csproj new file mode 100644 index 00000000..6c4ffadb --- /dev/null +++ b/src/Analysis/Codelyzer.Analysis.Analyzer/Codelyzer.Analysis.Analyzers.csproj @@ -0,0 +1,16 @@ + + + + netstandard2.0 + 10 + + + + + + + + + + + diff --git a/src/Analysis/Codelyzer.Analysis/Analyzer/LanguageAnalyzer.cs b/src/Analysis/Codelyzer.Analysis.Analyzer/LanguageAnalyzer.cs similarity index 57% rename from src/Analysis/Codelyzer.Analysis/Analyzer/LanguageAnalyzer.cs rename to src/Analysis/Codelyzer.Analysis.Analyzer/LanguageAnalyzer.cs index 12d2dbcf..e985d2d9 100644 --- a/src/Analysis/Codelyzer.Analysis/Analyzer/LanguageAnalyzer.cs +++ b/src/Analysis/Codelyzer.Analysis.Analyzer/LanguageAnalyzer.cs @@ -1,16 +1,15 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text; using System.Threading.Tasks; using Codelyzer.Analysis.Build; using Codelyzer.Analysis.Common; using Codelyzer.Analysis.Model; +using Codelyzer.Analysis.Model.Build; using Microsoft.CodeAnalysis; using Microsoft.Extensions.Logging; -namespace Codelyzer.Analysis.Analyzer +namespace Codelyzer.Analysis.Analyzers { public abstract class LanguageAnalyzer { @@ -27,40 +26,36 @@ public LanguageAnalyzer(AnalyzerConfiguration configuration, ILogger logger) Logger = logger; } - public async Task AnalyzeFile(string filePath, AnalyzerResult analyzerResult) + public async Task AnalyzeFile( + string filePath, + ProjectBuildResult incrementalBuildResult, + AnalyzerResult analyzerResult) { - if (!File.Exists(filePath)) - { - throw new FileNotFoundException(filePath); - } - - var projectBuildResult = analyzerResult.ProjectBuildResult; - var oldSourceFileResult = analyzerResult.ProjectResult.SourceFileResults.FirstOrDefault(sourceFile => sourceFile.FileFullPath == filePath); - - analyzerResult.ProjectResult.SourceFileResults.Remove(oldSourceFileResult); - - ProjectBuildHandler projectBuildHandler = new ProjectBuildHandler(Logger, - analyzerResult.ProjectBuildResult.Project, - analyzerResult.ProjectBuildResult.Compilation, - analyzerResult.ProjectBuildResult.PrePortCompilation, - AnalyzerConfiguration); - - analyzerResult.ProjectBuildResult = await projectBuildHandler.IncrementalBuild(filePath, analyzerResult.ProjectBuildResult); - var newSourceFileBuildResult = projectBuildResult.SourceFileBuildResults.FirstOrDefault(sourceFile => sourceFile.SourceFileFullPath == filePath); - - var fileAnalysis = AnalyzeFile(newSourceFileBuildResult, analyzerResult.ProjectResult.ProjectRootPath); + var newSourceFileBuildResult = + incrementalBuildResult.SourceFileBuildResults.FirstOrDefault(sourceFile => + sourceFile.SourceFileFullPath == filePath); + var fileAnalysis = AnalyzeFile(newSourceFileBuildResult, + analyzerResult.ProjectResult.ProjectRootPath); analyzerResult.ProjectResult.SourceFileResults.Add(fileAnalysis); - return analyzerResult; } - public async Task AnalyzeFile(string projectPath, string filePath, List frameworkMetaReferences, List coreMetaReferences) + + public async Task AnalyzeFile( + string projectPath, + string filePath, + List frameworkMetaReferences, + List coreMetaReferences) { var fileInfo = new Dictionary(); var content = File.ReadAllText(filePath); fileInfo.Add(filePath, content); return await AnalyzeFile(projectPath, fileInfo, frameworkMetaReferences, coreMetaReferences); } - public async Task AnalyzeFile(string projectPath, List filePaths, List frameworkMetaReferences, List coreMetaReferences) + public async Task AnalyzeFile( + string projectPath, + List filePaths, + List frameworkMetaReferences, + List coreMetaReferences) { var fileInfo = new Dictionary(); filePaths.ForEach(filePath => { @@ -69,13 +64,22 @@ public async Task AnalyzeFile(string projectPath, List AnalyzeFile(string projectPath, string filePath, string fileContent, List frameworkMetaReferences, List coreMetaReferences) + public async Task AnalyzeFile( + string projectPath, + string filePath, + string fileContent, + List frameworkMetaReferences, + List coreMetaReferences) { var fileInfo = new Dictionary(); fileInfo.Add(filePath, fileContent); return await AnalyzeFile(projectPath, fileInfo, frameworkMetaReferences, coreMetaReferences); } - public async Task AnalyzeFile(string projectPath, Dictionary fileInfo, List frameworkMetaReferences, List coreMetaReferences) + public async Task AnalyzeFile( + string projectPath, + Dictionary fileInfo, + List frameworkMetaReferences, + List coreMetaReferences) { var result = new IDEProjectResult(); @@ -90,7 +94,11 @@ public async Task AnalyzeFile(string projectPath, Dictionary< return result; } - public async Task AnalyzeFile(string projectPath, Dictionary fileInfo, IEnumerable frameworkMetaReferences, List coreMetaReferences) + + public async Task AnalyzeFile( + string projectPath, Dictionary fileInfo, + IEnumerable frameworkMetaReferences, + List coreMetaReferences) { var result = new IDEProjectResult(); @@ -105,8 +113,6 @@ public async Task AnalyzeFile(string projectPath, Dictionary< return result; } - - } } diff --git a/src/Analysis/Codelyzer.Analysis.Analyzer/LanguageAnalyzerFactory.cs b/src/Analysis/Codelyzer.Analysis.Analyzer/LanguageAnalyzerFactory.cs new file mode 100644 index 00000000..83801392 --- /dev/null +++ b/src/Analysis/Codelyzer.Analysis.Analyzer/LanguageAnalyzerFactory.cs @@ -0,0 +1,7 @@ +namespace Codelyzer.Analysis.Analyzers +{ + public abstract class LanguageAnalyzerFactory + { + public abstract LanguageAnalyzer GetLanguageAnalyzer(); + } +} diff --git a/src/Analysis/Codelyzer.Analysis/Analyzer/VBAnalyzer.cs b/src/Analysis/Codelyzer.Analysis.Analyzer/VbAnalyzer.cs similarity index 56% rename from src/Analysis/Codelyzer.Analysis/Analyzer/VBAnalyzer.cs rename to src/Analysis/Codelyzer.Analysis.Analyzer/VbAnalyzer.cs index 702aa017..4933de21 100644 --- a/src/Analysis/Codelyzer.Analysis/Analyzer/VBAnalyzer.cs +++ b/src/Analysis/Codelyzer.Analysis.Analyzer/VbAnalyzer.cs @@ -1,18 +1,17 @@ - -using Codelyzer.Analysis.Build; +using System.Linq; using Codelyzer.Analysis.Common; -using Codelyzer.Analysis.VisualBasic; using Codelyzer.Analysis.Model; -using Microsoft.Extensions.Logging; -using System.Linq; +using Codelyzer.Analysis.Model.Build; +using Codelyzer.Analysis.VisualBasic; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.VisualBasic; +using Microsoft.Extensions.Logging; -namespace Codelyzer.Analysis.Analyzer +namespace Codelyzer.Analysis.Analyzers { - class VBAnalyzer : LanguageAnalyzer + class VbAnalyzer : LanguageAnalyzer { - public VBAnalyzer(AnalyzerConfiguration configuration, ILogger logger) + public VbAnalyzer(AnalyzerConfiguration configuration, ILogger logger) : base(configuration, logger) { } @@ -21,7 +20,7 @@ public VBAnalyzer(AnalyzerConfiguration configuration, ILogger logger) public override RootUstNode AnalyzeFile(SourceFileBuildResult sourceFileBuildResult, string projectRootPath) { - CodeContext codeContext = new CodeContext(sourceFileBuildResult.PrePortSemanticModel, + var codeContext = new CodeContext(sourceFileBuildResult.PrePortSemanticModel, sourceFileBuildResult.SemanticModel, sourceFileBuildResult.SyntaxTree, projectRootPath, @@ -31,14 +30,16 @@ public override RootUstNode AnalyzeFile(SourceFileBuildResult sourceFileBuildRes Logger.LogDebug("Analyzing: " + sourceFileBuildResult.SourceFileFullPath); - using VisualBasicRoslynProcessor processor = new VisualBasicRoslynProcessor(codeContext); - - var result = (RootUstNode) processor.Visit(codeContext.SyntaxTree.GetRoot()); + using (var processor = new VisualBasicRoslynProcessor(codeContext)) + { + var result = (RootUstNode)processor.Visit(codeContext.SyntaxTree.GetRoot()); - result.LinesOfCode = sourceFileBuildResult.SyntaxTree.GetRoot().DescendantTrivia() - .Where(t => t.IsKind(SyntaxKind.EndOfLineTrivia)).Count(); + result.LinesOfCode = sourceFileBuildResult.SyntaxTree.GetRoot() + .DescendantTrivia().Count(t => t.IsKind(SyntaxKind.EndOfLineTrivia)); + + return result; + } - return result as RootUstNode; } } } diff --git a/src/Analysis/Codelyzer.Analysis.Analyzer/VbAnalyzerFactory.cs b/src/Analysis/Codelyzer.Analysis.Analyzer/VbAnalyzerFactory.cs new file mode 100644 index 00000000..f9602acc --- /dev/null +++ b/src/Analysis/Codelyzer.Analysis.Analyzer/VbAnalyzerFactory.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.Logging; + +namespace Codelyzer.Analysis.Analyzers +{ + public class VbAnalyzerFactory:LanguageAnalyzerFactory + { + protected readonly AnalyzerConfiguration AnalyzerConfiguration; + protected readonly ILogger Logger; + public VbAnalyzerFactory(AnalyzerConfiguration analyzerConfiguration, ILogger logger) + { + AnalyzerConfiguration = analyzerConfiguration; + Logger = logger; + } + public override LanguageAnalyzer GetLanguageAnalyzer() + { + return new VbAnalyzer(AnalyzerConfiguration, Logger); + } + } +} diff --git a/src/Analysis/Codelyzer.Analysis.Common/MSBuildDetector.cs b/src/Analysis/Codelyzer.Analysis.Build/MSBuildDetector.cs similarity index 90% rename from src/Analysis/Codelyzer.Analysis.Common/MSBuildDetector.cs rename to src/Analysis/Codelyzer.Analysis.Build/MSBuildDetector.cs index e5afad3d..6fb949c3 100644 --- a/src/Analysis/Codelyzer.Analysis.Common/MSBuildDetector.cs +++ b/src/Analysis/Codelyzer.Analysis.Build/MSBuildDetector.cs @@ -7,7 +7,7 @@ using System.IO; using System.Linq; -namespace Codelyzer.Analysis.Common +namespace Codelyzer.Analysis.Build { /// /// Detects the MSBuild Path that best matches the solution @@ -201,6 +201,36 @@ public List GetFileSystemMsBuildExePath(string programFilesPath = null, return result; } + /// + /// Returns MSBuild Path based on visual studio version given + /// + /// VS2019 or VS2022 + /// MSBuild path from visual studio version or blank if not found + public static string GetMsBuildPathFromVisualStudioVersion(VisualStudioVersion visualStudioVersion) + { + List editions = new List { "Enterprise", "Professional", "Community", "BuildTools" }; + var targets = new string[] + { + "Microsoft.CSharp.targets", "Microsoft.CSharp.CurrentVersion.targets", "Microsoft.Common.targets" + }; + var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); + string programFilesX86 = System.Environment.GetFolderPath(System.Environment.SpecialFolder.ProgramFilesX86); + DirectoryInfo vsDirectory = null; + switch (visualStudioVersion) + { + case VisualStudioVersion.VS2022: + vsDirectory = new DirectoryInfo(Path.Combine(programFiles, "Microsoft Visual Studio")); + break; + case VisualStudioVersion.VS2019: + vsDirectory = new DirectoryInfo(Path.Combine(programFilesX86, "Microsoft Visual Studio")); + + break; + } + + return vsDirectory != null ? + GetMsBuildPathFromVSDirectory(vsDirectory, editions, targets, null) : + ""; + } public static string GetMsBuildPathFromVSDirectory(DirectoryInfo vsDirectory, List editions, string[] targets, string projectToolsVersion) { diff --git a/src/Analysis/Codelyzer.Analysis.Build/ProjectBuildHandler.cs b/src/Analysis/Codelyzer.Analysis.Build/ProjectBuildHandler.cs index 13648353..6cfebfaf 100644 --- a/src/Analysis/Codelyzer.Analysis.Build/ProjectBuildHandler.cs +++ b/src/Analysis/Codelyzer.Analysis.Build/ProjectBuildHandler.cs @@ -1,5 +1,6 @@ using Buildalyzer; using Codelyzer.Analysis.Model; +using Codelyzer.Analysis.Model.Build; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Editing; @@ -15,6 +16,7 @@ using System.Threading.Tasks; using System.Xml; using System.Xml.Linq; +using Codelyzer.Analysis.Common; using Microsoft.CodeAnalysis.VisualBasic; using Constants = Codelyzer.Analysis.Common.Constants; using LanguageVersion = Microsoft.CodeAnalysis.CSharp.LanguageVersion; @@ -39,87 +41,14 @@ public class ProjectBuildHandler : IDisposable private string _projectPath; private const string syntaxAnalysisError = "Build Errors: Encountered an unknown build issue. Falling back to syntax analysis"; - - private XDocument LoadProjectFile(string projectFilePath) - { - if (!File.Exists(projectFilePath)) - { - return null; - } - try - { - return XDocument.Load(projectFilePath); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error loading project file {}", projectFilePath); - return null; - } - - } - private List LoadMetadataReferences(XDocument projectFile) - { - var references = new List(); - - if (projectFile == null) { - return references; - } - - var fileReferences = ExtractFileReferencesFromProject(projectFile); - fileReferences?.ForEach(fileRef => - { - if(!File.Exists(fileRef)) { - MissingMetaReferences.Add(fileRef); - Logger.LogWarning("Assembly {} referenced does not exist.", fileRef); - return; - } - try - { - references.Add(MetadataReference.CreateFromFile(fileRef)); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error while parsing metadata reference {}.", fileRef); - } - - }); - - return references; - } - - private List ExtractFileReferencesFromProject(XDocument projectFileContents) - { - if (projectFileContents == null) - { - return null; - } - - var portingNode = projectFileContents.Descendants() - .FirstOrDefault(d => - d.Name.LocalName == "ItemGroup" - && d.FirstAttribute?.Name == "Label" - && d.FirstAttribute?.Value == "PortingInfo"); - - var fileReferences = portingNode?.FirstNode?.ToString() - .Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries)? - .Where(s => !(s.Contains(""))) - .Select(s => s.Trim()) - .ToList(); - - return fileReferences; - } - + private async Task SetPrePortCompilation() { - var preportReferences = LoadMetadataReferences(LoadProjectFile(Project.FilePath)); - if (preportReferences.Count > 0) - { - var preportProject = Project.WithMetadataReferences(preportReferences); - PrePortMetaReferences = preportReferences.Select(m => m.Display).ToList(); - return await preportProject.GetCompilationAsync(); - } - - return null; + var (prePortCompilation, preportReferences, missingReferences) = + await new ProjectBuildHelper(Logger).GetPrePortCompilation(Project); + MissingMetaReferences = missingReferences; + PrePortMetaReferences = preportReferences; + return prePortCompilation; } private bool CanSkipErrorsForVisualBasic() @@ -243,9 +172,18 @@ Project.CompilationOptions is VisualBasicCompilationOptions if (trees.Count != 0) { - Compilation = (vbOptions != null)? - VisualBasicCompilation.Create(Project.AssemblyName,trees, meta, vbOptions): - (options!= null)? CSharpCompilation.Create(Project.AssemblyName, trees, meta, options) : null; + if (vbOptions != null) + { + Compilation = VisualBasicCompilation.Create(Project.AssemblyName, trees, meta, vbOptions); + } + else if (options != null) + { + Compilation = CSharpCompilation.Create(Project.AssemblyName, trees, meta, options); + } + else + { + Compilation = null; + } } } private void SetSyntaxCompilation(List metadataReferences) diff --git a/src/Analysis/Codelyzer.Analysis.Build/WorkspaceBuilder.cs b/src/Analysis/Codelyzer.Analysis.Build/WorkspaceBuilder.cs index b8cffd31..c795c4fa 100644 --- a/src/Analysis/Codelyzer.Analysis.Build/WorkspaceBuilder.cs +++ b/src/Analysis/Codelyzer.Analysis.Build/WorkspaceBuilder.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Codelyzer.Analysis.Model.Build; namespace Codelyzer.Analysis.Build { diff --git a/src/Analysis/Codelyzer.Analysis.Build/WorkspaceBuilderHelper.cs b/src/Analysis/Codelyzer.Analysis.Build/WorkspaceBuilderHelper.cs index ebf0d2f5..d7ddc484 100644 --- a/src/Analysis/Codelyzer.Analysis.Build/WorkspaceBuilderHelper.cs +++ b/src/Analysis/Codelyzer.Analysis.Build/WorkspaceBuilderHelper.cs @@ -632,10 +632,6 @@ private OSPlatform DetermineOSPlatform() { return OSPlatform.Linux; } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD)) - { - return OSPlatform.FreeBSD; - } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { return OSPlatform.OSX; diff --git a/src/Analysis/Codelyzer.Analysis.CSharp/CSharpRoslynProcessor.cs b/src/Analysis/Codelyzer.Analysis.CSharp/CSharpRoslynProcessor.cs index 159a09d2..af5110d6 100644 --- a/src/Analysis/Codelyzer.Analysis.CSharp/CSharpRoslynProcessor.cs +++ b/src/Analysis/Codelyzer.Analysis.CSharp/CSharpRoslynProcessor.cs @@ -34,7 +34,6 @@ public CSharpRoslynProcessor(CodeContext context) /// /// The node to start the traversal from /// - [return: MaybeNull] public override UstNode Visit(SyntaxNode node) { if (node == null) @@ -65,7 +64,6 @@ public override UstNode Visit(SyntaxNode node) return RootNode; } - [return: MaybeNull] public override UstNode DefaultVisit(SyntaxNode node) { return null; diff --git a/src/Analysis/Codelyzer.Analysis.CSharp/Codelyzer.Analysis.CSharp.csproj b/src/Analysis/Codelyzer.Analysis.CSharp/Codelyzer.Analysis.CSharp.csproj index b76a2cfe..1274b5c9 100644 --- a/src/Analysis/Codelyzer.Analysis.CSharp/Codelyzer.Analysis.CSharp.csproj +++ b/src/Analysis/Codelyzer.Analysis.CSharp/Codelyzer.Analysis.CSharp.csproj @@ -1,7 +1,7 @@  - net6.0 + netstandard2.0 false Codelyzer.Analysis.CSharp diff --git a/src/Analysis/Codelyzer.Analysis.CSharp/Handlers/ClassDeclarationHandler.cs b/src/Analysis/Codelyzer.Analysis.CSharp/Handlers/ClassDeclarationHandler.cs index a1065e8d..5d78ad26 100644 --- a/src/Analysis/Codelyzer.Analysis.CSharp/Handlers/ClassDeclarationHandler.cs +++ b/src/Analysis/Codelyzer.Analysis.CSharp/Handlers/ClassDeclarationHandler.cs @@ -27,7 +27,7 @@ public ClassDeclarationHandler(CodeContext context, ClassDeclaration.Reference.Version = GetAssemblyVersion(classSymbol); ClassDeclaration.Reference.AssemblySymbol = classSymbol.ContainingAssembly; - ClassDeclaration.BaseList = new(); + ClassDeclaration.BaseList = new System.Collections.Generic.List(); if (classSymbol.BaseType != null) { var baseTypeSymbol = classSymbol.BaseType; diff --git a/src/Analysis/Codelyzer.Analysis.CSharp/Handlers/RecordDeclarationHandler.cs b/src/Analysis/Codelyzer.Analysis.CSharp/Handlers/RecordDeclarationHandler.cs index 3b1f18a8..d94d99be 100644 --- a/src/Analysis/Codelyzer.Analysis.CSharp/Handlers/RecordDeclarationHandler.cs +++ b/src/Analysis/Codelyzer.Analysis.CSharp/Handlers/RecordDeclarationHandler.cs @@ -27,7 +27,7 @@ public RecordDeclarationHandler(CodeContext context, RecordDeclaration.Reference.Version = GetAssemblyVersion(recordSymbol); RecordDeclaration.Reference.AssemblySymbol = recordSymbol.ContainingAssembly; - RecordDeclaration.BaseList = new(); + RecordDeclaration.BaseList = new System.Collections.Generic.List(); if (recordSymbol.BaseType != null) { var baseTypeSymbol = recordSymbol.BaseType; diff --git a/src/Analysis/Codelyzer.Analysis.CSharp/Handlers/StructDeclarationHandler.cs b/src/Analysis/Codelyzer.Analysis.CSharp/Handlers/StructDeclarationHandler.cs index 642bcd1e..a6e8f99b 100644 --- a/src/Analysis/Codelyzer.Analysis.CSharp/Handlers/StructDeclarationHandler.cs +++ b/src/Analysis/Codelyzer.Analysis.CSharp/Handlers/StructDeclarationHandler.cs @@ -25,7 +25,7 @@ public StructDeclarationHandler(CodeContext context, StructDeclaration.Reference.AssemblySymbol = structSymbol.ContainingAssembly; StructDeclaration.FullIdentifier = structSymbol.OriginalDefinition.ToString(); - StructDeclaration.BaseList = new(); + StructDeclaration.BaseList = new System.Collections.Generic.List(); if (structSymbol.BaseType != null) { var baseTypeSymbol = structSymbol.BaseType; diff --git a/src/Analysis/Codelyzer.Analysis.Common/AnalyzerConfiguration.cs b/src/Analysis/Codelyzer.Analysis.Common/AnalyzerConfiguration.cs index fc4a92fa..ca472a55 100644 --- a/src/Analysis/Codelyzer.Analysis.Common/AnalyzerConfiguration.cs +++ b/src/Analysis/Codelyzer.Analysis.Common/AnalyzerConfiguration.cs @@ -10,7 +10,7 @@ public class AnalyzerConfiguration public string Language; public int ConcurrentThreads = Constants.DefaultConcurrentThreads; public bool AnalyzeFailedProjects = false; - public static List DefaultBuildArguments = new() + public static List DefaultBuildArguments = new List() { Constants.RestorePackagesConfigArgument, Constants.RestoreArgument @@ -46,31 +46,11 @@ public static class LanguageOptions public class BuildSettings { - public BuildSettings(VisualStudioVersion? visualStudioVersion = null) + public BuildSettings(VisualStudioVersion? visualStudioVersion = null, string msBuildPath = "") { BuildArguments = AnalyzerConfiguration.DefaultBuildArguments; VisualStudioVersion = visualStudioVersion; - if (visualStudioVersion!= null && VisualStudioVersion.HasValue) - { - List editions = new List { "Enterprise", "Professional", "Community", "BuildTools" }; - var targets = new string[] { "Microsoft.CSharp.targets", "Microsoft.CSharp.CurrentVersion.targets", "Microsoft.Common.targets" }; - var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); - string programFilesX86 = System.Environment.GetFolderPath(System.Environment.SpecialFolder.ProgramFilesX86); - DirectoryInfo vsDirectory = null; - switch (VisualStudioVersion.Value) { - case Analysis.VisualStudioVersion.VS2022: - vsDirectory = new DirectoryInfo(Path.Combine(programFiles, "Microsoft Visual Studio")); - break; - case Analysis.VisualStudioVersion.VS2019: - vsDirectory = new DirectoryInfo(Path.Combine(programFilesX86, "Microsoft Visual Studio")); - - break; - } - if (vsDirectory != null) - { - MSBuildPath = MSBuildDetector.GetMsBuildPathFromVSDirectory(vsDirectory, editions, targets, null); - } - } + MSBuildPath = msBuildPath; } public string MSBuildPath; public List BuildArguments; diff --git a/src/Analysis/Codelyzer.Analysis.Common/Codelyzer.Analysis.Common.csproj b/src/Analysis/Codelyzer.Analysis.Common/Codelyzer.Analysis.Common.csproj index c4b3d4a8..80d0254d 100644 --- a/src/Analysis/Codelyzer.Analysis.Common/Codelyzer.Analysis.Common.csproj +++ b/src/Analysis/Codelyzer.Analysis.Common/Codelyzer.Analysis.Common.csproj @@ -1,15 +1,18 @@  - net6.0 + netstandard2.0 false Codelyzer.Analysis.Common + true + 10 + - + diff --git a/src/Analysis/Codelyzer.Analysis.Common/CommonUtils.cs b/src/Analysis/Codelyzer.Analysis.Common/CommonUtils.cs index 8f661130..4d095f85 100644 --- a/src/Analysis/Codelyzer.Analysis.Common/CommonUtils.cs +++ b/src/Analysis/Codelyzer.Analysis.Common/CommonUtils.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Http; diff --git a/src/Analysis/Codelyzer.Analysis.Build/ExternalReferenceLoader.cs b/src/Analysis/Codelyzer.Analysis.Common/ExternalReferenceLoader.cs similarity index 87% rename from src/Analysis/Codelyzer.Analysis.Build/ExternalReferenceLoader.cs rename to src/Analysis/Codelyzer.Analysis.Common/ExternalReferenceLoader.cs index 69d22663..e7952063 100644 --- a/src/Analysis/Codelyzer.Analysis.Build/ExternalReferenceLoader.cs +++ b/src/Analysis/Codelyzer.Analysis.Common/ExternalReferenceLoader.cs @@ -1,4 +1,5 @@ using Codelyzer.Analysis.Model; +using Codelyzer.Analysis.Model.Extensions; using Microsoft.CodeAnalysis; using Microsoft.Extensions.Logging; using NuGet.Packaging; @@ -8,9 +9,9 @@ using System.Linq; using System.Reflection; using System.Text.RegularExpressions; -using Constants = Codelyzer.Analysis.Common.Constants; -namespace Codelyzer.Analysis.Build +namespace Codelyzer.Analysis.Common + { public class ExternalReferenceLoader { @@ -49,7 +50,7 @@ private HashSet LoadProjectReferences() { var projectReferencesIds = _project.ProjectReferences != null ? _project.ProjectReferences.Select(pr => pr.ProjectId).ToList() : null; var projectReferences = projectReferencesIds != null ? _project.Solution.Projects.Where(p => projectReferencesIds.Contains(p.Id)) : null; - projectReferenceNames = projectReferences != null ? projectReferences.Select(p => p.Name).ToHashSet() : null; + projectReferenceNames = projectReferences != null ? projectReferences.Select(p => p.Name).ToHashSet() : null; _externalReferences.ProjectReferences.AddRange(projectReferences.Select(p => new ExternalReference() { @@ -65,10 +66,11 @@ private void LoadFromBuildPackageReferences() { _packageReferences?.ToList().ForEach(packageReference => { + bool getVersionSuccess = packageReference.Value.TryGetValue(Constants.Version, out var version); var reference = new ExternalReference() { Identity = packageReference.Key, - Version = packageReference.Value.GetValueOrDefault(Constants.Version) + Version = getVersionSuccess ? version : "", }; if (!_externalReferences.NugetReferences.Contains(reference)) { @@ -86,17 +88,19 @@ private void LoadFromPackagesConfig() { try { - using var fileStream = new FileStream(packageConfig, FileMode.Open); - var configReader = new PackagesConfigReader(fileStream); - var packages = configReader.GetPackages(true); + using (var fileStream = new FileStream(packageConfig, FileMode.Open)) + { + var configReader = new PackagesConfigReader(fileStream); + var packages = configReader.GetPackages(true); - packages?.ToList().ForEach(package => { - var reference = new ExternalReference() { Identity = package.PackageIdentity.Id, Version = package.PackageIdentity.Version.OriginalVersion }; - if (!_externalReferences.NugetReferences.Contains(reference)) - { - _externalReferences.NugetReferences.Add(reference); - } - }); + packages?.ToList().ForEach(package => { + var reference = new ExternalReference() { Identity = package.PackageIdentity.Id, Version = package.PackageIdentity.Version.OriginalVersion }; + if (!_externalReferences.NugetReferences.Contains(reference)) + { + _externalReferences.NugetReferences.Add(reference); + } + }); + } } catch (Exception ex) { @@ -160,7 +164,7 @@ private void LoadFromCompilation(HashSet projectReferenceNames) nugetRef.Version = externalReference.Version; } } - else if (filePath.Contains(Common.Constants.PackagesDirectoryIdentifier, System.StringComparison.CurrentCultureIgnoreCase)) + else if (filePath.IndexOf(Common.Constants.PackagesDirectoryIdentifier, System.StringComparison.CurrentCultureIgnoreCase) >= 0) { _externalReferences.NugetDependencies.Add(externalReference); } diff --git a/src/Analysis/Codelyzer.Analysis.Build/FileBuildHandler.cs b/src/Analysis/Codelyzer.Analysis.Common/FileBuildHandler.cs similarity index 96% rename from src/Analysis/Codelyzer.Analysis.Build/FileBuildHandler.cs rename to src/Analysis/Codelyzer.Analysis.Common/FileBuildHandler.cs index 684d7058..b988779b 100644 --- a/src/Analysis/Codelyzer.Analysis.Build/FileBuildHandler.cs +++ b/src/Analysis/Codelyzer.Analysis.Common/FileBuildHandler.cs @@ -1,4 +1,3 @@ -using Buildalyzer; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.Extensions.Logging; @@ -8,8 +7,10 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.CodeAnalysis.VisualBasic; +using Codelyzer.Analysis.Model.Build; +using Codelyzer.Analysis.Model; -namespace Codelyzer.Analysis.Build +namespace Codelyzer.Analysis.Common { public class FileBuildHandler : IDisposable { @@ -62,7 +63,7 @@ await Task.Run(() => } _fileInfo.Keys.ToList().ForEach(file => { - var sourceFilePath = Path.GetRelativePath(_projectPath, file); + var sourceFilePath = PathNetCore.GetRelativePath(_projectPath, file); var fileTree = trees.FirstOrDefault(t => t.FilePath == file); if (fileTree != null) diff --git a/src/Analysis/Codelyzer.Analysis.Common/FileUtils.cs b/src/Analysis/Codelyzer.Analysis.Common/FileUtils.cs index a659293d..9dffd40a 100644 --- a/src/Analysis/Codelyzer.Analysis.Common/FileUtils.cs +++ b/src/Analysis/Codelyzer.Analysis.Common/FileUtils.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading.Tasks; using System.Xml.Linq; +using Codelyzer.Analysis.Model.Extensions; namespace Codelyzer.Analysis.Common { @@ -15,30 +16,21 @@ public static void WriteFile(string path, string data) { System.IO.File.WriteAllText(path, data); } - + public static async Task WriteFileAsync(string dir, string file, string content) { - using (StreamWriter outputFile = new StreamWriter(Path.Combine(dir, file)) ) + using (StreamWriter outputFile = new StreamWriter(Path.Combine(dir, file))) { await outputFile.WriteAsync(content); } return Path.Combine(dir, file); } - + public static string ReadFile(string pathFile) { return File.ReadAllText(pathFile); } - - public static string GetRelativePath(string filePath, string dirPath) - { - var dirPathSeparator = Path.EndsInDirectorySeparator(dirPath) ? dirPath : - Path.Combine(dirPath, Path.DirectorySeparatorChar.ToString()); - - var path = filePath.Replace(dirPathSeparator, ""); - return path; - } public static void DirectoryCopy(string sourceDirPath, string destDirPath, bool copySubDirs = true) { @@ -122,7 +114,7 @@ public static Dictionary> GetProjectsWithReferences(stri .Root .Descendants() .Where(x => x.Name?.LocalName == "ProjectReference") - .Select(p => Path.GetFullPath(p.Attribute("Include").Value, Path.GetDirectoryName(projectFile)).ToLower()) + .Select(p => GetFullPath(p.Attribute("Include").Value, Path.GetDirectoryName(projectFile)).ToLower()) .Distinct() .ToHashSet(); result.Add(projectFile.ToLower(), projectReferenceNodes); @@ -135,5 +127,15 @@ public static Dictionary> GetProjectsWithReferences(stri return result; } + + private static string GetFullPath(string path, string basePath) + { + string currentDirectory = Environment.CurrentDirectory; + Environment.CurrentDirectory = basePath; + string fullPath = Path.GetFullPath(path); + Environment.CurrentDirectory = currentDirectory; + return fullPath; + } } + } diff --git a/src/Analysis/Codelyzer.Analysis.Common/ProjectBuildHelper.cs b/src/Analysis/Codelyzer.Analysis.Common/ProjectBuildHelper.cs new file mode 100644 index 00000000..1c1b5dd4 --- /dev/null +++ b/src/Analysis/Codelyzer.Analysis.Common/ProjectBuildHelper.cs @@ -0,0 +1,146 @@ +using Microsoft.CodeAnalysis; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.Build.Construction; +using Codelyzer.Analysis.Model; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Codelyzer.Analysis.Common +{ + public class ProjectBuildHelper + { + private readonly ILogger _logger; + + public ProjectBuildHelper(ILogger logger) + { + _logger = logger; + } + + /// + /// Gets pre-port compilation, pre-port meta references, and missing references from given project + /// + /// (Pre-port Compilation, Pre-port meta references, MissingMetaReferences) + public async Task<(Compilation?, List, List)> GetPrePortCompilation(Project project) + { + var projectBuildHelper = new ProjectBuildHelper(_logger); + var projectFile = projectBuildHelper.LoadProjectFile(project.FilePath); + if (projectFile == null) + { + return (null, new List(), new List()); + } + var (prePortReferences, missingMetaReferences) = + projectBuildHelper.LoadMetadataReferences(projectFile); + if (prePortReferences.Count > 0) + { + var prePortProject = project.WithMetadataReferences(prePortReferences); + var prePortMetaReferences = prePortReferences + .Select(m => m.Display ?? "") + .ToList(); + var prePortCompilation = await prePortProject.GetCompilationAsync(); + return (prePortCompilation, prePortMetaReferences, missingMetaReferences); + } + return (null, new List(), missingMetaReferences); + } + + private XDocument LoadProjectFile(string projectFilePath) + { + if (!File.Exists(projectFilePath)) + { + return null; + } + try + { + return XDocument.Load(projectFilePath); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error loading project file {}", projectFilePath); + return null; + } + } + + public (List, List) LoadMetadataReferences( + XDocument projectFile) + { + var references = new List(); + var missingMetaReferences = new List(); + + if (projectFile == null) + { + return (references, missingMetaReferences); + } + + var fileReferences = ExtractFileReferencesFromProject(projectFile); + foreach (var fileRef in fileReferences) + { + if (!File.Exists(fileRef)) + { + missingMetaReferences.Add(fileRef); + _logger.LogWarning("Assembly {} referenced does not exist.", fileRef); + } + try + { + references.Add(MetadataReference.CreateFromFile(fileRef)); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while parsing metadata reference {}.", fileRef); + } + } + return (references, missingMetaReferences); + } + + private List ExtractFileReferencesFromProject(XDocument projectFileContents) + { + if (projectFileContents == null) + { + return null; + } + + var portingNode = projectFileContents.Descendants() + .FirstOrDefault(d => + d.Name.LocalName == "ItemGroup" + && d.FirstAttribute?.Name == "Label" + && d.FirstAttribute?.Value == "PortingInfo"); + + var fileReferences = portingNode?.FirstNode?.ToString() + .Split(Environment.NewLine.ToCharArray(), StringSplitOptions.RemoveEmptyEntries)? + .Where(s => !(s.Contains(""))) + .Select(s => s.Trim()) + .ToList() ?? new List(); + + return fileReferences; + } + + public Dictionary GetProjectInSolutionObjects(string solutionPath) + { + var map = new Dictionary(); + var solution = SolutionFile.Parse(solutionPath); + return solution.ProjectsInOrder.ToDictionary(project => project.ProjectName); + } + + public ExternalReferences GetExternalReferences( + Compilation compilation, + Project project) + { + if (project.FilePath == null) + { + // todo: error metric candidate + throw new Exception("Project file path is invalid"); + } + ExternalReferenceLoader externalReferenceLoader = new ExternalReferenceLoader( + Directory.GetParent(project.FilePath)?.FullName, + compilation, + project, + new Dictionary>(), + NullLogger.Instance); + + return externalReferenceLoader.Load(); + } + } +} diff --git a/src/Analysis/Codelyzer.Analysis/AnalyzerResult.cs b/src/Analysis/Codelyzer.Analysis.Model/AnalyzerResult.cs similarity index 83% rename from src/Analysis/Codelyzer.Analysis/AnalyzerResult.cs rename to src/Analysis/Codelyzer.Analysis.Model/AnalyzerResult.cs index 8a1f3340..06754075 100644 --- a/src/Analysis/Codelyzer.Analysis/AnalyzerResult.cs +++ b/src/Analysis/Codelyzer.Analysis.Model/AnalyzerResult.cs @@ -1,8 +1,7 @@ -using Codelyzer.Analysis.Build; -using Codelyzer.Analysis.Model; +using Codelyzer.Analysis.Model.Build; using System; -namespace Codelyzer.Analysis +namespace Codelyzer.Analysis.Model { /// /// The result of a Project Analysis diff --git a/src/Analysis/Codelyzer.Analysis.Build/Models/ProjectBuildResult.cs b/src/Analysis/Codelyzer.Analysis.Model/Build/ProjectBuildResult.cs similarity index 93% rename from src/Analysis/Codelyzer.Analysis.Build/Models/ProjectBuildResult.cs rename to src/Analysis/Codelyzer.Analysis.Model/Build/ProjectBuildResult.cs index 5a832593..905ff2f1 100644 --- a/src/Analysis/Codelyzer.Analysis.Build/Models/ProjectBuildResult.cs +++ b/src/Analysis/Codelyzer.Analysis.Model/Build/ProjectBuildResult.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using System.IO; -namespace Codelyzer.Analysis.Build +namespace Codelyzer.Analysis.Model.Build { public class ProjectBuildResult : IDisposable { @@ -42,7 +42,7 @@ public bool IsBuildSuccess() internal void AddSourceFile(string filePath) { - var wsPath = Path.GetRelativePath(ProjectRootPath, filePath); + var wsPath = PathNetCore.GetRelativePath(ProjectRootPath, filePath); SourceFiles.Add(wsPath); } diff --git a/src/Analysis/Codelyzer.Analysis.Build/Models/SourceFileBuildResult.cs b/src/Analysis/Codelyzer.Analysis.Model/Build/SourceFileBuildResult.cs similarity index 82% rename from src/Analysis/Codelyzer.Analysis.Build/Models/SourceFileBuildResult.cs rename to src/Analysis/Codelyzer.Analysis.Model/Build/SourceFileBuildResult.cs index ead72374..ce8e867f 100644 --- a/src/Analysis/Codelyzer.Analysis.Build/Models/SourceFileBuildResult.cs +++ b/src/Analysis/Codelyzer.Analysis.Model/Build/SourceFileBuildResult.cs @@ -1,10 +1,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Editing; -using System; -using System.Collections.Generic; -using System.Text; -namespace Codelyzer.Analysis.Build +namespace Codelyzer.Analysis.Model.Build { public class SourceFileBuildResult { diff --git a/src/Analysis/Codelyzer.Analysis.Model/Codelyzer.Analysis.Model.csproj b/src/Analysis/Codelyzer.Analysis.Model/Codelyzer.Analysis.Model.csproj index 75d1e9f3..3f100c39 100644 --- a/src/Analysis/Codelyzer.Analysis.Model/Codelyzer.Analysis.Model.csproj +++ b/src/Analysis/Codelyzer.Analysis.Model/Codelyzer.Analysis.Model.csproj @@ -1,17 +1,20 @@  - net6.0 + netstandard2.0 false Codelyzer.Analysis.Model + true - - - + + + + + @@ -19,4 +22,8 @@ + + + + diff --git a/src/Analysis/Codelyzer.Analysis.Model/Extensions/Extensions.cs b/src/Analysis/Codelyzer.Analysis.Model/Extensions/Extensions.cs new file mode 100644 index 00000000..ed836b8b --- /dev/null +++ b/src/Analysis/Codelyzer.Analysis.Model/Extensions/Extensions.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Codelyzer.Analysis.Model.Extensions +{ + public static class EnumerableExtensions + { + public static HashSet ToHashSet(this IEnumerable enumerable) + { + var set = new HashSet(); + foreach (var item in enumerable) + { + set.Add(item); + } + return set; + } + } +} diff --git a/src/Analysis/Codelyzer.Analysis.Build/Models/IDEProjectResult.cs b/src/Analysis/Codelyzer.Analysis.Model/IDEProjectResult.cs similarity index 93% rename from src/Analysis/Codelyzer.Analysis.Build/Models/IDEProjectResult.cs rename to src/Analysis/Codelyzer.Analysis.Model/IDEProjectResult.cs index 8f2323f7..c01054f5 100644 --- a/src/Analysis/Codelyzer.Analysis.Build/Models/IDEProjectResult.cs +++ b/src/Analysis/Codelyzer.Analysis.Model/IDEProjectResult.cs @@ -1,4 +1,5 @@ using Codelyzer.Analysis.Model; +using Codelyzer.Analysis.Model.Build; using System; using System.Collections.Generic; using System.Text; diff --git a/src/Analysis/Codelyzer.Analysis.Model/PathNetCore.cs b/src/Analysis/Codelyzer.Analysis.Model/PathNetCore.cs new file mode 100644 index 00000000..dc71cf77 --- /dev/null +++ b/src/Analysis/Codelyzer.Analysis.Model/PathNetCore.cs @@ -0,0 +1,290 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +//Adapted from https://github.com/dotnet/corefx/blob/master/src/Common/src/CoreLib/System/IO/Path.cs#L697 for Windows +// by Anton Krouglov + +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text; + +namespace Codelyzer.Analysis.Model +{ + // Provides methods for processing file system strings in a cross-platform manner. + // Most of the methods don't do a complete parsing (such as examining a UNC hostname), + // but they will handle most string operations. + public static class PathNetCore + { + + /// + /// Create a relative path from one path to another. Paths will be resolved before calculating the difference. + /// Default path comparison for the active platform will be used (OrdinalIgnoreCase for Windows or Mac, Ordinal for Unix). + /// + /// The source path the output should be relative to. This path is always considered to be a directory. + /// The destination path. + /// The relative path or if the paths don't share the same root. + /// Thrown if or is null or an empty string. + public static string GetRelativePath(string relativeTo, string path) + { + return GetRelativePath(relativeTo, path, StringComparison); + } + + private static string GetRelativePath(string relativeTo, string path, StringComparison comparisonType) + { + if (string.IsNullOrEmpty(relativeTo)) throw new ArgumentNullException(nameof(relativeTo)); + if (string.IsNullOrEmpty(path)) throw new ArgumentNullException(nameof(path)); + + relativeTo = Path.GetFullPath(relativeTo); + path = Path.GetFullPath(path); + + // Need to check if the roots are different- if they are we need to return the "to" path. + if (!PathInternalNetCore.AreRootsEqual(relativeTo, path, comparisonType)) + return path; + + int commonLength = PathInternalNetCore.GetCommonPathLength(relativeTo, path, + ignoreCase: comparisonType == StringComparison.OrdinalIgnoreCase); + + // If there is nothing in common they can't share the same root, return the "to" path as is. + if (commonLength == 0) + return path; + + // Trailing separators aren't significant for comparison + int relativeToLength = relativeTo.Length; + if (PathInternalNetCore.EndsInDirectorySeparator(relativeTo)) + relativeToLength--; + + bool pathEndsInSeparator = PathInternalNetCore.EndsInDirectorySeparator(path); + int pathLength = path.Length; + if (pathEndsInSeparator) + pathLength--; + + // If we have effectively the same path, return "." + if (relativeToLength == pathLength && commonLength >= relativeToLength) return "."; + + // We have the same root, we need to calculate the difference now using the + // common Length and Segment count past the length. + // + // Some examples: + // + // C:\Foo C:\Bar L3, S1 -> ..\Bar + // C:\Foo C:\Foo\Bar L6, S0 -> Bar + // C:\Foo\Bar C:\Bar\Bar L3, S2 -> ..\..\Bar\Bar + // C:\Foo\Foo C:\Foo\Bar L7, S1 -> ..\Bar + + StringBuilder + sb = new StringBuilder(); //StringBuilderCache.Acquire(Math.Max(relativeTo.Length, path.Length)); + + // Add parent segments for segments past the common on the "from" path + if (commonLength < relativeToLength) + { + sb.Append(".."); + + for (int i = commonLength + 1; i < relativeToLength; i++) + { + if (PathInternalNetCore.IsDirectorySeparator(relativeTo[i])) + { + sb.Append(DirectorySeparatorChar); + sb.Append(".."); + } + } + } + else if (PathInternalNetCore.IsDirectorySeparator(path[commonLength])) + { + // No parent segments and we need to eat the initial separator + // (C:\Foo C:\Foo\Bar case) + commonLength++; + } + + // Now add the rest of the "to" path, adding back the trailing separator + int differenceLength = pathLength - commonLength; + if (pathEndsInSeparator) + differenceLength++; + + if (differenceLength > 0) + { + if (sb.Length > 0) + { + sb.Append(DirectorySeparatorChar); + } + + sb.Append(path, commonLength, differenceLength); + } + + return sb.ToString(); //StringBuilderCache.GetStringAndRelease(sb); + } + + // Public static readonly variant of the separators. The Path implementation itself is using + // internal const variant of the separators for better performance. + public static readonly char DirectorySeparatorChar = PathInternalNetCore.DirectorySeparatorChar; + public static readonly char AltDirectorySeparatorChar = PathInternalNetCore.AltDirectorySeparatorChar; + public static readonly char VolumeSeparatorChar = PathInternalNetCore.VolumeSeparatorChar; + public static readonly char PathSeparator = PathInternalNetCore.PathSeparator; + + /// Returns a comparison that can be used to compare file and directory names for equality. + internal static StringComparison StringComparison => StringComparison.OrdinalIgnoreCase; + } + + /// Contains internal path helpers that are shared between many projects. + internal static class PathInternalNetCore + { + internal static char DirectorySeparatorChar = Path.DirectorySeparatorChar; + internal static char AltDirectorySeparatorChar = Path.AltDirectorySeparatorChar; + internal const char VolumeSeparatorChar = ':'; + internal const char PathSeparator = ';'; + + internal const string ExtendedDevicePathPrefix = @"\\?\"; + internal const string UncPathPrefix = @"\\"; + internal const string UncDevicePrefixToInsert = @"?\UNC\"; + internal const string UncExtendedPathPrefix = @"\\?\UNC\"; + internal const string DevicePathPrefix = @"\\.\"; + + //internal const int MaxShortPath = 260; + + // \\?\, \\.\, \??\ + internal const int DevicePrefixLength = 4; + + /// + /// Returns true if the two paths have the same root + /// + internal static bool AreRootsEqual(string first, string second, StringComparison comparisonType) + { + int firstRootLength = GetRootLength(first); + int secondRootLength = GetRootLength(second); + + return firstRootLength == secondRootLength + && string.Compare( + strA: first, + indexA: 0, + strB: second, + indexB: 0, + length: firstRootLength, + comparisonType: comparisonType) == 0; + } + + /// + /// Gets the length of the root of the path (drive, share, etc.). + /// + internal static int GetRootLength(string path) + { + int i = 0; + int volumeSeparatorLength = 2; // Length to the colon "C:" + int uncRootLength = 2; // Length to the start of the server name "\\" + + bool extendedSyntax = path.StartsWith(ExtendedDevicePathPrefix); + bool extendedUncSyntax = path.StartsWith(UncExtendedPathPrefix); + if (extendedSyntax) + { + // Shift the position we look for the root from to account for the extended prefix + if (extendedUncSyntax) + { + // "\\" -> "\\?\UNC\" + uncRootLength = UncExtendedPathPrefix.Length; + } + else + { + // "C:" -> "\\?\C:" + volumeSeparatorLength += ExtendedDevicePathPrefix.Length; + } + } + + if ((!extendedSyntax || extendedUncSyntax) && path.Length > 0 && IsDirectorySeparator(path[0])) + { + // UNC or simple rooted path (e.g. "\foo", NOT "\\?\C:\foo") + + i = 1; // Drive rooted (\foo) is one character + if (extendedUncSyntax || (path.Length > 1 && IsDirectorySeparator(path[1]))) + { + // UNC (\\?\UNC\ or \\), scan past the next two directory separators at most + // (e.g. to \\?\UNC\Server\Share or \\Server\Share\) + i = uncRootLength; + int n = 2; // Maximum separators to skip + while (i < path.Length && (!IsDirectorySeparator(path[i]) || --n > 0)) i++; + } + } + else if (path.Length >= volumeSeparatorLength && + path[volumeSeparatorLength - 1] == PathNetCore.VolumeSeparatorChar) + { + // Path is at least longer than where we expect a colon, and has a colon (\\?\A:, A:) + // If the colon is followed by a directory separator, move past it + i = volumeSeparatorLength; + if (path.Length >= volumeSeparatorLength + 1 && IsDirectorySeparator(path[volumeSeparatorLength])) i++; + } + + return i; + } + + /// + /// True if the given character is a directory separator. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool IsDirectorySeparator(char c) + { + return c == PathNetCore.DirectorySeparatorChar || c == PathNetCore.AltDirectorySeparatorChar; + } + + /// + /// Get the common path length from the start of the string. + /// + internal static int GetCommonPathLength(string first, string second, bool ignoreCase) + { + int commonChars = EqualStartingCharacterCount(first, second, ignoreCase: ignoreCase); + + // If nothing matches + if (commonChars == 0) + return commonChars; + + // Or we're a full string and equal length or match to a separator + if (commonChars == first.Length + && (commonChars == second.Length || IsDirectorySeparator(second[commonChars]))) + return commonChars; + + if (commonChars == second.Length && IsDirectorySeparator(first[commonChars])) + return commonChars; + + // It's possible we matched somewhere in the middle of a segment e.g. C:\Foodie and C:\Foobar. + while (commonChars > 0 && !IsDirectorySeparator(first[commonChars - 1])) + commonChars--; + + return commonChars; + } + + /// + /// Gets the count of common characters from the left optionally ignoring case + /// + internal static unsafe int EqualStartingCharacterCount(string first, string second, bool ignoreCase) + { + if (string.IsNullOrEmpty(first) || string.IsNullOrEmpty(second)) return 0; + + int commonChars = 0; + + fixed (char* f = first) + fixed (char* s = second) + { + char* l = f; + char* r = s; + char* leftEnd = l + first.Length; + char* rightEnd = r + second.Length; + + while (l != leftEnd && r != rightEnd + && (*l == *r || (ignoreCase && + char.ToUpperInvariant((*l)) == char.ToUpperInvariant((*r))))) + { + commonChars++; + l++; + r++; + } + } + + return commonChars; + } + + /// + /// Returns true if the path ends in a directory separator. + /// + internal static bool EndsInDirectorySeparator(string path) + => path.Length > 0 && IsDirectorySeparator(path[path.Length - 1]); + } +} diff --git a/src/Analysis/Codelyzer.Analysis.VisualBasic/Codelyzer.Analysis.VisualBasic.csproj b/src/Analysis/Codelyzer.Analysis.VisualBasic/Codelyzer.Analysis.VisualBasic.csproj index cddd809b..2af56312 100644 --- a/src/Analysis/Codelyzer.Analysis.VisualBasic/Codelyzer.Analysis.VisualBasic.csproj +++ b/src/Analysis/Codelyzer.Analysis.VisualBasic/Codelyzer.Analysis.VisualBasic.csproj @@ -1,6 +1,6 @@  - net6.0 + netstandard2.0 false Codelyzer.Analysis.VisualBasic diff --git a/src/Analysis/Codelyzer.Analysis.VisualBasic/Handlers/ArgumentListHandler.cs b/src/Analysis/Codelyzer.Analysis.VisualBasic/Handlers/ArgumentListHandler.cs index 76c5ae90..31e24a89 100644 --- a/src/Analysis/Codelyzer.Analysis.VisualBasic/Handlers/ArgumentListHandler.cs +++ b/src/Analysis/Codelyzer.Analysis.VisualBasic/Handlers/ArgumentListHandler.cs @@ -26,7 +26,7 @@ private void SetMetaData(ArgumentListSyntax syntaxNode) var identifier = ""; var semanticType = ""; - if (argumentSyntax is not OmittedArgumentSyntax) + if (argumentSyntax.GetType() != typeof(OmittedArgumentSyntax)) { identifier = argumentSyntax.GetExpression().ToString(); semanticType = SemanticHelper.GetSemanticType(argumentSyntax.GetExpression(), SemanticModel); diff --git a/src/Analysis/Codelyzer.Analysis.VisualBasic/Handlers/InvocationExpressionHandler.cs b/src/Analysis/Codelyzer.Analysis.VisualBasic/Handlers/InvocationExpressionHandler.cs index 3a89bc85..3b862e24 100644 --- a/src/Analysis/Codelyzer.Analysis.VisualBasic/Handlers/InvocationExpressionHandler.cs +++ b/src/Analysis/Codelyzer.Analysis.VisualBasic/Handlers/InvocationExpressionHandler.cs @@ -46,7 +46,7 @@ private void SetMetaData(InvocationExpressionSyntax syntaxNode) var identifier = ""; var semanticType = ""; - if (argumentSyntax is not OmittedArgumentSyntax) + if (argumentSyntax.GetType() != typeof(OmittedArgumentSyntax)) { identifier = argumentSyntax.GetExpression().ToString(); semanticType = SemanticHelper.GetSemanticType(argumentSyntax.GetExpression(), SemanticModel); diff --git a/src/Analysis/Codelyzer.Analysis.VisualBasic/VisualBasicRoslynProcessor.cs b/src/Analysis/Codelyzer.Analysis.VisualBasic/VisualBasicRoslynProcessor.cs index c8c838d4..2ccf404f 100644 --- a/src/Analysis/Codelyzer.Analysis.VisualBasic/VisualBasicRoslynProcessor.cs +++ b/src/Analysis/Codelyzer.Analysis.VisualBasic/VisualBasicRoslynProcessor.cs @@ -34,7 +34,6 @@ public VisualBasicRoslynProcessor(CodeContext context) /// /// The node to start the traversal from /// - [return: MaybeNull] public override UstNode Visit(SyntaxNode node) { if (node == null) @@ -65,7 +64,6 @@ public override UstNode Visit(SyntaxNode node) return RootNode; } - [return: MaybeNull] public override UstNode DefaultVisit(SyntaxNode node) { return null; diff --git a/src/Analysis/Codelyzer.Analysis.Workspace/Codelyzer.Analysis.Workspace.csproj b/src/Analysis/Codelyzer.Analysis.Workspace/Codelyzer.Analysis.Workspace.csproj new file mode 100644 index 00000000..cb4cacc5 --- /dev/null +++ b/src/Analysis/Codelyzer.Analysis.Workspace/Codelyzer.Analysis.Workspace.csproj @@ -0,0 +1,22 @@ + + + + netstandard2.0 + 10 + enable + + + + + + + + + + + + + + + + diff --git a/src/Analysis/Codelyzer.Analysis.Workspace/IWorkspaceHelper.cs b/src/Analysis/Codelyzer.Analysis.Workspace/IWorkspaceHelper.cs new file mode 100644 index 00000000..40ab461b --- /dev/null +++ b/src/Analysis/Codelyzer.Analysis.Workspace/IWorkspaceHelper.cs @@ -0,0 +1,14 @@ +using Codelyzer.Analysis.Model.Build; +using Microsoft.CodeAnalysis; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Codelyzer.Analysis.Workspace +{ + public interface IWorkspaceHelper + { + public Task> GetProjectBuildResults(Solution solution); + + public IAsyncEnumerable GetProjectBuildResultsGeneratorAsync(Solution solution); + } +} diff --git a/src/Analysis/Codelyzer.Analysis.Workspace/WorkspaceHelper.cs b/src/Analysis/Codelyzer.Analysis.Workspace/WorkspaceHelper.cs new file mode 100644 index 00000000..b7e0e2f4 --- /dev/null +++ b/src/Analysis/Codelyzer.Analysis.Workspace/WorkspaceHelper.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using Codelyzer.Analysis.Model.Build; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Editing; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Codelyzer.Analysis.Common; +using Microsoft.Build.Construction; +using Codelyzer.Analysis.Model; +using Microsoft.Extensions.Logging; + +namespace Codelyzer.Analysis.Workspace +{ + public class WorkspaceHelper : IWorkspaceHelper + { + private ILogger _logger; + private ProjectBuildHelper _projectBuildHelper; + + public WorkspaceHelper(ILogger logger) + { + _logger = logger; + _projectBuildHelper = new ProjectBuildHelper(_logger); + } + + public async Task> GetProjectBuildResults(Solution solution) + { + var buildResults = new List(); + + var projectMap = _projectBuildHelper.GetProjectInSolutionObjects(solution.FilePath); + + foreach (var project in solution.Projects) + { + buildResults.Add(await GetProjectBuildResult(project, projectMap)); + } + + return buildResults; + } + + public async IAsyncEnumerable GetProjectBuildResultsGeneratorAsync(Solution solution) + { + var projectMap = _projectBuildHelper + .GetProjectInSolutionObjects(solution.FilePath); + + foreach (var project in solution.Projects) + { + yield return await GetProjectBuildResult(project, projectMap); + } + } + + private async Task GetProjectBuildResult(Project project, Dictionary projectMap) + { + var compilation = await project.GetCompilationAsync() ?? throw new Exception("Get compilation failed"); + + // await SetCompilation(); maybe we should refactor the fallback compilation? but with vs workspace, we shouldn't need this faillback + + var (prePortCompilation, prePortMetaReferences, missingMetaReferences) = + await _projectBuildHelper.GetPrePortCompilation(project); + var projectBuildResult = new ProjectBuildResult + { + BuildErrors = compilation.GetDiagnostics() + .Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) + .Select(error => error.ToString()) + .ToList(), + ProjectPath = project.FilePath, + ProjectRootPath = Path.GetDirectoryName(project.FilePath), + Project = project, + Compilation = compilation, + PrePortCompilation = prePortCompilation, + IsSyntaxAnalysis = false, // I don't think we should ever hit this. + PreportReferences = prePortMetaReferences, + MissingReferences = missingMetaReferences, + + //GetTargetFrameworks(projectBuildResult, AnalyzerResult); todo: get this? + + ProjectGuid = ( + projectMap.TryGetValue(project.Name, out var projectInSolution) + ? Guid.Parse(projectInSolution.ProjectGuid) + : Guid.NewGuid()) + .ToString(), + ProjectType = projectInSolution != null ? projectInSolution.ProjectType.ToString() : string.Empty + }; + + foreach (var syntaxTree in compilation.SyntaxTrees) + { + var sourceFilePath = PathNetCore.GetRelativePath(projectBuildResult.ProjectRootPath, syntaxTree.FilePath); + var prePortTree = prePortCompilation?.SyntaxTrees?.FirstOrDefault(s => s.FilePath == syntaxTree.FilePath); + var fileResult = new SourceFileBuildResult + { + SyntaxTree = syntaxTree, + PrePortSemanticModel = prePortTree != null ? prePortCompilation?.GetSemanticModel(prePortTree) : null, + SemanticModel = compilation.GetSemanticModel(syntaxTree), + SourceFileFullPath = syntaxTree.FilePath, + SyntaxGenerator = SyntaxGenerator.GetGenerator(project), + SourceFilePath = sourceFilePath + }; + projectBuildResult.SourceFileBuildResults.Add(fileResult); + projectBuildResult.SourceFiles.Add(sourceFilePath); + } + + projectBuildResult.ExternalReferences = _projectBuildHelper.GetExternalReferences( + projectBuildResult?.Compilation, + projectBuildResult?.Project); + + return projectBuildResult ?? new ProjectBuildResult(); + } + } +} diff --git a/src/Analysis/Codelyzer.Analysis/Analyzer/CodeAnalyzerByLanguage.cs b/src/Analysis/Codelyzer.Analysis/Analyzer/CodeAnalyzerByLanguage.cs index a568b29e..8aa8f26a 100644 --- a/src/Analysis/Codelyzer.Analysis/Analyzer/CodeAnalyzerByLanguage.cs +++ b/src/Analysis/Codelyzer.Analysis/Analyzer/CodeAnalyzerByLanguage.cs @@ -4,10 +4,9 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using Codelyzer.Analysis.Analyzers; using Codelyzer.Analysis.Build; -using Codelyzer.Analysis.Common; using Codelyzer.Analysis.Model; -using Microsoft.CodeAnalysis; using Microsoft.Extensions.Logging; namespace Codelyzer.Analysis.Analyzer @@ -17,10 +16,13 @@ public class CodeAnalyzerByLanguage protected readonly AnalyzerConfiguration AnalyzerConfiguration; protected readonly ILogger Logger; + private readonly Analyzers.CodeAnalyzer _codeAnalyzer; + public CodeAnalyzerByLanguage(AnalyzerConfiguration configuration, ILogger logger) { AnalyzerConfiguration = configuration; Logger = logger; + _codeAnalyzer = new Analyzers.CodeAnalyzer(configuration, logger); } public async Task AnalyzeProject(string projectPath) { @@ -33,16 +35,14 @@ public async Task> AnalyzeSolution(string solutionPath) return await Analyze(solutionPath); } - public async Task> AnalyzeSolutionGenerator(string solutionPath) { var analyzerResults = await AnalyzeSolutionGeneratorAsync(solutionPath).ToListAsync(); - await GenerateOptionalOutput(analyzerResults); + await _codeAnalyzer.GenerateOptionalOutput(analyzerResults); return analyzerResults; } - /// public async IAsyncEnumerable AnalyzeSolutionGeneratorAsync(string solutionPath) { @@ -66,7 +66,6 @@ private async IAsyncEnumerable AnalyzeGeneratorAsync(string path { throw new FileNotFoundException(path); } - WorkspaceBuilder builder = new WorkspaceBuilder(Logger, path, AnalyzerConfiguration); var projectBuildResultEnumerator = builder.BuildProject().GetAsyncEnumerator(); try @@ -74,18 +73,7 @@ private async IAsyncEnumerable AnalyzeGeneratorAsync(string path while (await projectBuildResultEnumerator.MoveNextAsync().ConfigureAwait(false)) { var projectBuildResult = projectBuildResultEnumerator.Current; - var workspaceResult = AnalyzeProject(projectBuildResult); - workspaceResult.ProjectGuid = projectBuildResult.ProjectGuid; - workspaceResult.ProjectType = projectBuildResult.ProjectType; - - if (AnalyzerConfiguration.MetaDataSettings.LoadBuildData) - { - yield return new AnalyzerResult() { ProjectResult = workspaceResult, ProjectBuildResult = projectBuildResult }; - } - else - { - yield return new AnalyzerResult() { ProjectResult = workspaceResult }; - } + yield return await _codeAnalyzer.AnalyzeProjectBuildResult(projectBuildResult); } } finally @@ -94,8 +82,6 @@ private async IAsyncEnumerable AnalyzeGeneratorAsync(string path } } - - public async Task> Analyze(string path) { if (!File.Exists(path)) @@ -104,114 +90,34 @@ public async Task> Analyze(string path) } List workspaceResults = new List(); - var analyzerResults = new List(); WorkspaceBuilder builder = new WorkspaceBuilder(Logger, path, AnalyzerConfiguration); var projectBuildResults = await builder.Build(); - foreach (var projectBuildResult in projectBuildResults) - { - var workspaceResult = await Task.Run(() => AnalyzeProject(projectBuildResult)); - workspaceResult.ProjectGuid = projectBuildResult.ProjectGuid; - workspaceResult.ProjectType = projectBuildResult.ProjectType; - workspaceResults.Add(workspaceResult); - - //Generate Output result - if (AnalyzerConfiguration.MetaDataSettings.LoadBuildData) - { - analyzerResults.Add(new AnalyzerResult() { ProjectResult = workspaceResult, ProjectBuildResult = projectBuildResult }); - } - else - { - analyzerResults.Add(new AnalyzerResult() { ProjectResult = workspaceResult }); - } - } - - await GenerateOptionalOutput(analyzerResults); + var analyzerResults = await _codeAnalyzer.Analyze(projectBuildResults); return analyzerResults; } - private async Task GenerateOptionalOutput(List analyzerResults) - { - if (AnalyzerConfiguration.ExportSettings.GenerateJsonOutput) - { - Directory.CreateDirectory(AnalyzerConfiguration.ExportSettings.OutputPath); - foreach (var analyzerResult in analyzerResults) - { - Logger.LogDebug("Generating Json file for " + analyzerResult.ProjectResult.ProjectName); - var jsonOutput = SerializeUtils.ToJson(analyzerResult.ProjectResult); - var jsonFilePath = await FileUtils.WriteFileAsync(AnalyzerConfiguration.ExportSettings.OutputPath, - analyzerResult.ProjectResult.ProjectName + ".json", jsonOutput); - analyzerResult.OutputJsonFilePath = jsonFilePath; - Logger.LogDebug("Generated Json file " + jsonFilePath); - } - } - } - public ProjectWorkspace AnalyzeProject(ProjectBuildResult projectResult) - { - Logger.LogDebug("Analyzing the project: " + projectResult.ProjectPath); - var projType = Path.GetExtension(projectResult.ProjectPath)?.ToLower(); - LanguageAnalyzer languageAnalyzer = GetLanguageAnalyzerByProjectType(projType); - ProjectWorkspace workspace = new ProjectWorkspace(projectResult.ProjectPath) - { - SourceFiles = new UstList(projectResult.SourceFiles), - BuildErrors = projectResult.BuildErrors, - BuildErrorsCount = projectResult.BuildErrors.Count - }; - - if (AnalyzerConfiguration.MetaDataSettings.ReferenceData) - { - workspace.ExternalReferences = projectResult.ExternalReferences; - } - workspace.TargetFramework = projectResult.TargetFramework; - workspace.TargetFrameworks = projectResult.TargetFrameworks; - workspace.LinesOfCode = 0; - foreach (var fileBuildResult in projectResult.SourceFileBuildResults) - { - var fileAnalysis = languageAnalyzer.AnalyzeFile(fileBuildResult, workspace.ProjectRootPath); - workspace.LinesOfCode += fileAnalysis.LinesOfCode; - workspace.SourceFileResults.Add(fileAnalysis); - } - - return workspace; - } - - public LanguageAnalyzer GetLanguageAnalyzerByProjectType(string projType) + public async Task AnalyzeFile(string filePath, AnalyzerResult analyzerResult) { - LanguageAnalyzerFactory languageAnalyzerFactory; - switch (projType.ToLower()) + if (!File.Exists(filePath)) { - case ".vbproj": - languageAnalyzerFactory = new VBAnalyerFactory(AnalyzerConfiguration, Logger); - break; - case ".csproj": - languageAnalyzerFactory = new CSharpAnalyzerFactory(AnalyzerConfiguration, Logger); - break; - - default: - throw new Exception($"invalid project type {projType}"); + throw new FileNotFoundException(filePath); } - return languageAnalyzerFactory.GetLanguageAnalyzer(); - } + var projectBuildResult = analyzerResult.ProjectBuildResult; + var oldSourceFileResult = analyzerResult.ProjectResult.SourceFileResults.FirstOrDefault(sourceFile => sourceFile.FileFullPath == filePath); - public LanguageAnalyzer GetLanguageAnalyzerByFileType(string fileType) - { - LanguageAnalyzerFactory languageAnalyzerFactory; - switch (fileType.ToLower()) - { - case ".vb": - languageAnalyzerFactory = new VBAnalyerFactory(AnalyzerConfiguration, Logger); - break; - case ".cs": - languageAnalyzerFactory = new CSharpAnalyzerFactory(AnalyzerConfiguration, Logger); - break; + analyzerResult.ProjectResult.SourceFileResults.Remove(oldSourceFileResult); - default: - throw new Exception($"invalid project type {fileType}"); - } - return languageAnalyzerFactory.GetLanguageAnalyzer(); + ProjectBuildHandler projectBuildHandler = new ProjectBuildHandler(Logger, + analyzerResult.ProjectBuildResult.Project, + analyzerResult.ProjectBuildResult.Compilation, + analyzerResult.ProjectBuildResult.PrePortCompilation, + AnalyzerConfiguration); + analyzerResult.ProjectBuildResult = await projectBuildHandler.IncrementalBuild(filePath, analyzerResult.ProjectBuildResult); + return await _codeAnalyzer.AnalyzeFile(filePath, analyzerResult.ProjectBuildResult, analyzerResult); } /// @@ -255,56 +161,59 @@ public CodeGraph GenerateGraph(List analyzerResults) return codeGraph; } - /// - public async Task> AnalyzeSolution(string solutionPath, Dictionary> oldReferences, Dictionary> references) + public async Task> AnalyzeSolution( + string solutionPath, + Dictionary> oldReferences, + Dictionary> references) { var analyzerResults = await AnalyzeWithReferences(solutionPath, oldReferences, references); return analyzerResults; } - private async Task> AnalyzeWithReferences(string path, Dictionary> oldReferences, Dictionary> references) + private async Task> AnalyzeWithReferences( + string path, + Dictionary> oldReferences, + Dictionary> references) { if (!File.Exists(path)) { throw new FileNotFoundException(path); } - - List workspaceResults = new List(); - var analyzerResults = new List(); - WorkspaceBuilder builder = new WorkspaceBuilder(Logger, path, AnalyzerConfiguration); - var projectBuildResults = builder.GenerateNoBuildAnalysis(oldReferences, references); - - foreach (var projectBuildResult in projectBuildResults) - { - var workspaceResult = await Task.Run(() => AnalyzeProject(projectBuildResult)); - workspaceResult.ProjectGuid = projectBuildResult.ProjectGuid; - workspaceResult.ProjectType = projectBuildResult.ProjectType; - workspaceResults.Add(workspaceResult); - - //Generate Output result - if (AnalyzerConfiguration.MetaDataSettings.LoadBuildData) - { - analyzerResults.Add(new AnalyzerResult() { ProjectResult = workspaceResult, ProjectBuildResult = projectBuildResult }); - } - else - { - analyzerResults.Add(new AnalyzerResult() { ProjectResult = workspaceResult }); - } - } - - await GenerateOptionalOutput(analyzerResults); - + var analyzerResults = await _codeAnalyzer.Analyze(projectBuildResults); + await _codeAnalyzer.GenerateOptionalOutput(analyzerResults); return analyzerResults; } - public async Task AnalyzeProject(string projectPath, List oldReferences, List references) + public async Task AnalyzeProject( + string projectPath, + List oldReferences, + List references) { - var analyzerResult = await AnalyzeWithReferences(projectPath, oldReferences?.ToDictionary(r => projectPath, r => oldReferences), references?.ToDictionary(r => projectPath, r => references)); + var analyzerResult = await AnalyzeWithReferences( + projectPath, + oldReferences?.ToDictionary(r => projectPath, r => oldReferences), + references?.ToDictionary(r => projectPath, r => references)); return analyzerResult.FirstOrDefault(); } + + //maintained for backwards compatibility + public Analyzers.LanguageAnalyzer GetLanguageAnalyzerByProjectType(string projType) + { + return _codeAnalyzer.GetLanguageAnalyzerByProjectType(projType); + } + + /// + /// Deprecated method. Call directly into AnalyzeFile instead. Returns language analyzer based on file extension. + /// + /// File extension, either .cs or .vb + /// Language analyzer object for the corresponding language + public LanguageAnalyzer GetLanguageAnalyzerByFileType(string fileType) + { + return _codeAnalyzer.GetLanguageAnalyzerByFileType(fileType); + } } } diff --git a/src/Analysis/Codelyzer.Analysis/Analyzer/LanguageAnalyzerFactory.cs b/src/Analysis/Codelyzer.Analysis/Analyzer/LanguageAnalyzerFactory.cs deleted file mode 100644 index 31a80f8a..00000000 --- a/src/Analysis/Codelyzer.Analysis/Analyzer/LanguageAnalyzerFactory.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Codelyzer.Analysis.Analyzer -{ - abstract class LanguageAnalyzerFactory - { - public abstract LanguageAnalyzer GetLanguageAnalyzer(); - } -} diff --git a/src/Analysis/Codelyzer.Analysis/Analyzer/VBAnalyerFactory.cs b/src/Analysis/Codelyzer.Analysis/Analyzer/VBAnalyerFactory.cs deleted file mode 100644 index cb190385..00000000 --- a/src/Analysis/Codelyzer.Analysis/Analyzer/VBAnalyerFactory.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Codelyzer.Analysis.Analyzer -{ - class VBAnalyerFactory:LanguageAnalyzerFactory - { - protected readonly AnalyzerConfiguration _analyzerConfiguration; - protected readonly ILogger _logger; - public VBAnalyerFactory(AnalyzerConfiguration analyzerConfiguration, ILogger logger) - { - _analyzerConfiguration = analyzerConfiguration; - _logger = logger; - } - public override LanguageAnalyzer GetLanguageAnalyzer() - { - return new VBAnalyzer(_analyzerConfiguration, _logger); - } - } -} diff --git a/src/Analysis/Codelyzer.Analysis/CSharpCodeAnalyzer.cs b/src/Analysis/Codelyzer.Analysis/CSharpCodeAnalyzer.cs index 89199023..6b2f7f4c 100644 --- a/src/Analysis/Codelyzer.Analysis/CSharpCodeAnalyzer.cs +++ b/src/Analysis/Codelyzer.Analysis/CSharpCodeAnalyzer.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CSharp; +using Codelyzer.Analysis.Model.Build; namespace Codelyzer.Analysis { diff --git a/src/Analysis/Codelyzer.Analysis/CodeAnalyzer.cs b/src/Analysis/Codelyzer.Analysis/CodeAnalyzer.cs index e045d1f6..55289a3d 100644 --- a/src/Analysis/Codelyzer.Analysis/CodeAnalyzer.cs +++ b/src/Analysis/Codelyzer.Analysis/CodeAnalyzer.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Codelyzer.Analysis.Build; +using Codelyzer.Analysis.Model; using Microsoft.CodeAnalysis; using Microsoft.Extensions.Logging; diff --git a/src/Analysis/Codelyzer.Analysis/CodeAnalyzerFactory.cs b/src/Analysis/Codelyzer.Analysis/CodeAnalyzerFactory.cs index cd845108..e779a005 100644 --- a/src/Analysis/Codelyzer.Analysis/CodeAnalyzerFactory.cs +++ b/src/Analysis/Codelyzer.Analysis/CodeAnalyzerFactory.cs @@ -14,6 +14,7 @@ public static class CodeAnalyzerFactory /// Configuration of the analyzer /// Logger object /// + [Obsolete("Using the CodeAnalyzerFactory is deprecated, use the CodeAnalyzerByLanguage class instead")] public static CodeAnalyzer GetAnalyzer(AnalyzerConfiguration configuration, ILogger logger, string projectFile = "") { if (configuration.Language == LanguageOptions.Vb ||projectFile.EndsWith(".vbproj", diff --git a/src/Analysis/Codelyzer.Analysis/CodeGraph.cs b/src/Analysis/Codelyzer.Analysis/CodeGraph.cs index 8775c14a..0e9a3076 100644 --- a/src/Analysis/Codelyzer.Analysis/CodeGraph.cs +++ b/src/Analysis/Codelyzer.Analysis/CodeGraph.cs @@ -1,133 +1,140 @@ using Codelyzer.Analysis.Model; using Microsoft.Extensions.Logging; -using NuGet.Packaging; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; namespace Codelyzer.Analysis { public class CodeGraph { - HashSet _projectNodes; - HashSet _namespaceNodes; - HashSet _classNodes; - HashSet _interfaceNodes; - HashSet _structNodes; - HashSet _enumNodes; - HashSet _recordNodes; - HashSet _methodNodes; + ConcurrentDictionary _projectNodes; + ConcurrentDictionary _namespaceNodes; + ConcurrentDictionary _classNodes; + ConcurrentDictionary _interfaceNodes; + ConcurrentDictionary _typeNodes; + ConcurrentDictionary _structNodes; + ConcurrentDictionary _enumNodes; + ConcurrentDictionary _recordNodes; + ConcurrentDictionary _methodNodes; protected readonly ILogger Logger; - public HashSet Graph { get; set; } - public HashSet ProjectNodes + public ConcurrentDictionary Graph { get; set; } + public ConcurrentDictionary ProjectNodes { get { if (_projectNodes == null) { - _projectNodes = Graph.Where(n => n.NodeType == NodeType.Project).ToHashSet(); + _projectNodes = new ConcurrentDictionary(Graph.Where(n => n.Key.NodeType == NodeType.Project)); } return _projectNodes; } } - public HashSet NamespaceNodes + public ConcurrentDictionary NamespaceNodes { get { if (_namespaceNodes == null) { - _namespaceNodes = Graph.Where(n => n.NodeType == NodeType.Namespace).ToHashSet(); + _namespaceNodes = new ConcurrentDictionary(Graph.Where(n => n.Key.NodeType == NodeType.Namespace)); } return _namespaceNodes; } } - public HashSet ClassNodes + public ConcurrentDictionary ClassNodes { get { if (_classNodes == null) { - _classNodes = Graph.Where(n => n.NodeType == NodeType.Class).ToHashSet(); + _classNodes = new ConcurrentDictionary(Graph.Where(n => n.Key.NodeType == NodeType.Class)); } return _classNodes; } } - public HashSet InterfaceNodes + public ConcurrentDictionary InterfaceNodes { get { if (_interfaceNodes == null) { - _interfaceNodes = Graph.Where(n => n.NodeType == NodeType.Interface).ToHashSet(); + _interfaceNodes = new ConcurrentDictionary(Graph.Where(n => n.Key.NodeType == NodeType.Interface)); } return _interfaceNodes; } } - public HashSet StructNodes + public ConcurrentDictionary TypeNodes { get { - if (_structNodes == null) + if (_typeNodes == null) { - _structNodes = Graph.Where(n => n.NodeType == NodeType.Struct).ToHashSet(); + _typeNodes = new ConcurrentDictionary(ClassNodes + .Union(InterfaceNodes).Union(StructNodes).Union(RecordNodes).Union(EnumNodes)); } - return _structNodes; + return _typeNodes; } } - public HashSet TypeNodes + public ConcurrentDictionary StructNodes { get { - return ClassNodes.Union(InterfaceNodes).Union(StructNodes).Union(EnumNodes).Union(RecordNodes).ToHashSet(); + if (_structNodes == null) + { + _structNodes = new ConcurrentDictionary(Graph.Where(n => n.Key.NodeType == NodeType.Struct)); + } + return _structNodes; } } - public HashSet EnumNodes + public ConcurrentDictionary EnumNodes { get { if (_enumNodes == null) { - _enumNodes = Graph.Where(n => n.NodeType == NodeType.Enum).ToHashSet(); + _enumNodes = new ConcurrentDictionary(Graph.Where(n => n.Key.NodeType == NodeType.Enum)); } return _enumNodes; } } - public HashSet RecordNodes + public ConcurrentDictionary RecordNodes { get { if (_recordNodes == null) { - _recordNodes = Graph.Where(n => n.NodeType == NodeType.Record).ToHashSet(); + _recordNodes = new ConcurrentDictionary(Graph.Where(n => n.Key.NodeType == NodeType.Record)); } return _recordNodes; } } - public HashSet MethodNodes + public ConcurrentDictionary MethodNodes { get { if (_methodNodes == null) { - _methodNodes = Graph.Where(n => n.NodeType == NodeType.Method).ToHashSet(); + _methodNodes = new ConcurrentDictionary(Graph.Where(n => n.Key.NodeType == NodeType.Method)); } return _methodNodes; } } - HashSet projectWorkspaces; + ConcurrentDictionary projectWorkspaces; // Edges can have duplicates - Dictionary> ustNodeEdgeCandidates; - Dictionary> filteredUstNodeEdgeCandidates; + public ConcurrentDictionary> ustNodeEdgeCandidates; + public ConcurrentDictionary> filteredUstNodeEdgeCandidates; public CodeGraph(ILogger logger) { Logger = logger; - projectWorkspaces = new HashSet(); - ustNodeEdgeCandidates = new Dictionary>(); - filteredUstNodeEdgeCandidates = new Dictionary>(); - Graph = new HashSet(); - } + projectWorkspaces = new ConcurrentDictionary(); + ustNodeEdgeCandidates = new ConcurrentDictionary>(); + filteredUstNodeEdgeCandidates = new ConcurrentDictionary>(); + Graph = new ConcurrentDictionary(); + } public void Initialize(List analyzerResults) { PopulateGraphs(analyzerResults); @@ -135,31 +142,63 @@ public void Initialize(List analyzerResults) public void MergeGraphs(List codeGraphs) { //Clear previous variable to re-initiate: - _projectNodes=null; - _namespaceNodes=null; - _classNodes=null; - _interfaceNodes=null; - _structNodes=null; - _enumNodes=null; - _recordNodes=null; - _methodNodes=null; + _projectNodes = null; + _namespaceNodes = null; + _classNodes = null; + _interfaceNodes = null; + _structNodes = null; + _enumNodes = null; + _recordNodes = null; + _methodNodes = null; + _typeNodes = null; - foreach (CodeGraph codeGraph in codeGraphs) + Parallel.ForEach(codeGraphs, codeGraph => { - projectWorkspaces.AddRange(codeGraph.projectWorkspaces); - Graph.AddRange(codeGraph.Graph); + foreach (var projectWorkspace in codeGraph.projectWorkspaces) + { + projectWorkspaces.TryAdd(projectWorkspace.Key, projectWorkspace.Value); + } + foreach (var g in codeGraph.Graph) + { + // Should we limit this to namespace only? + if (!Graph.ContainsKey(g.Key)) + { + Graph.TryAdd(g.Key, g.Value); + } + else + { + // Merge the nodes + var existingKey = Graph.FirstOrDefault(g1 => g1.Key.Equals(g.Key)); + g.Key.ChildNodes?.ToList().ForEach(childNode => + { + existingKey.Key.ChildNodes.TryAdd(childNode.Key, childNode.Value); + }); + g.Key.IncomingEdges?.ToList().ForEach(incomingEdge => + { + existingKey.Key.IncomingEdges.Add(incomingEdge); + }); + g.Key.OutgoingEdges?.ToList().ForEach(outgoingEdge => + { + existingKey.Key.OutgoingEdges.Add(outgoingEdge); + }); + } + } foreach (var kvp in codeGraph.ustNodeEdgeCandidates) { if (ustNodeEdgeCandidates.ContainsKey(kvp.Key)) { - ustNodeEdgeCandidates[kvp.Key].AddRange(kvp.Value); + foreach (var v in kvp.Value) + { + ustNodeEdgeCandidates[kvp.Key].Add(v); + } } else { - ustNodeEdgeCandidates.Add(kvp.Key, kvp.Value); + ustNodeEdgeCandidates.TryAdd(kvp.Key, kvp.Value); } } - } + + }); // Remove edges that are external to the projects RemoveExternalEdges(); @@ -168,7 +207,7 @@ public void MergeGraphs(List codeGraphs) } private void PopulateGraphs(List analyzerResults) - { + { try { AddNodes(analyzerResults); @@ -193,8 +232,8 @@ private void AddNodes(List analyzerResults) Identifier = analyzerResult.ProjectResult.ProjectFilePath.ToLower() }; - Graph.Add(projectNode); - projectWorkspaces.Add(analyzerResult.ProjectResult); + Graph.TryAdd(projectNode, projectNode.Identifier); + projectWorkspaces.TryAdd(analyzerResult.ProjectResult, string.Empty); // Add Relevant Children from source files analyzerResult.ProjectResult.SourceFileResults.ForEach(sourceFileResult => @@ -202,9 +241,9 @@ private void AddNodes(List analyzerResults) var children = InitializeNodesHelper(sourceFileResult, projectNode); foreach (var child in children) { - if (!projectNode.ChildNodes.Contains(child)) + if (!projectNode.ChildNodes.ContainsKey(child)) { - projectNode.ChildNodes.Add(child); + projectNode.ChildNodes.TryAdd(child, child.Identifier); } } }); @@ -218,11 +257,27 @@ private void AddNodes(List analyzerResults) private void AddEdges() { AddProjectEdges(); - ClassNodes.ToList().ForEach(classNode => { CreateClassHierarchyEdges(classNode); }); - InterfaceNodes.ToList().ForEach(interfaceNode => { CreateClassHierarchyEdges(interfaceNode); }); - StructNodes.ToList().ForEach(structNode => { CreateClassHierarchyEdges(structNode); }); - RecordNodes.ToList().ForEach(recordNode => { CreateClassHierarchyEdges(recordNode); }); - filteredUstNodeEdgeCandidates.Keys.ToList().ForEach(key => CreateEdges(key)); + Parallel.ForEach(ClassNodes, classNode => + { + var ustNode = classNode.Key.UstNode as ClassDeclaration; + CreateClassHierarchyEdges(classNode.Key, ustNode.BaseList, ustNode.BaseTypeOriginalDefinition); + }); + Parallel.ForEach(InterfaceNodes, interfaceNode => + { + var ustNode = interfaceNode.Key.UstNode as InterfaceDeclaration; + CreateClassHierarchyEdges(interfaceNode.Key, ustNode.BaseList, ustNode.BaseTypeOriginalDefinition); + }); + Parallel.ForEach(StructNodes, structNode => + { + var ustNode = structNode.Key.UstNode as StructDeclaration; + CreateClassHierarchyEdges(structNode.Key, ustNode.BaseList, ustNode.BaseTypeOriginalDefinition); + }); + Parallel.ForEach(RecordNodes, recordNode => + { + var ustNode = recordNode.Key.UstNode as RecordDeclaration; + CreateClassHierarchyEdges(recordNode.Key, ustNode.BaseList, ustNode.BaseTypeOriginalDefinition); + }); + Parallel.ForEach(filteredUstNodeEdgeCandidates.Keys, key => { CreateEdges(key); }); } private void AddProjectEdges() { @@ -230,12 +285,12 @@ private void AddProjectEdges() { try { - var projectReferences = projectResult?.ExternalReferences?.ProjectReferences; - var sourceNode = ProjectNodes.FirstOrDefault(p => p.Identifier.Equals(projectResult.ProjectFilePath, StringComparison.InvariantCultureIgnoreCase)); + var projectReferences = projectResult.Key?.ExternalReferences?.ProjectReferences; + var sourceNode = ProjectNodes.FirstOrDefault(p => p.Key.Identifier.Equals(projectResult.Key.ProjectFilePath, StringComparison.InvariantCultureIgnoreCase)).Key; projectReferences?.ForEach(projectReference => { - var targetNode = ProjectNodes.FirstOrDefault(p => p.Identifier.Equals(projectReference.AssemblyLocation, StringComparison.InvariantCultureIgnoreCase)); + var targetNode = ProjectNodes.FirstOrDefault(p => p.Key.Identifier.Equals(projectReference.AssemblyLocation, StringComparison.InvariantCultureIgnoreCase)).Key; if (targetNode != null) { var edge = new Edge() { EdgeType = EdgeType.ProjectReference, TargetNode = targetNode, SourceNode = sourceNode }; @@ -249,20 +304,18 @@ private void AddProjectEdges() } catch (Exception ex) { - Logger.LogError(ex, $"Error while adding project edges for {projectResult.ProjectFilePath}"); + Logger.LogError(ex, $"Error while adding project edges for {projectResult.Key.ProjectFilePath}"); } }); } private void RemoveExternalEdges() { - var uniqueNamespaces = Graph.Where(n => n.NodeType == NodeType.Namespace).Select(n=>n.Identifier).Distinct().ToHashSet(); - - ustNodeEdgeCandidates.ToList().ForEach(nodeAndChildren => - { + var uniqueNamespaces = Graph.Where(n => n.Key.NodeType == NodeType.Namespace).Select(n => n.Key.Identifier).Distinct().ToHashSet(); + Parallel.ForEach(ustNodeEdgeCandidates, nodeAndChildren => { try { var key = nodeAndChildren.Key; - var value = nodeAndChildren.Value.Where(child => + var value = new ConcurrentBag(nodeAndChildren.Value.Where(child => { if (child is InvocationExpression invocation) { @@ -277,10 +330,10 @@ private void RemoveExternalEdges() return uniqueNamespaces.Contains(memberAccess?.Reference?.Namespace); } return false; - }).ToList(); + })); if (value.Count > 0) { - filteredUstNodeEdgeCandidates.Add(key, value); + filteredUstNodeEdgeCandidates.TryAdd(key, value); } } catch (Exception ex) @@ -288,7 +341,6 @@ private void RemoveExternalEdges() Logger.LogError(ex, "Error while removing external edges"); } }); - } private NodeType GetNodeType(T ustNode) { @@ -340,17 +392,17 @@ private HashSet InitializeNodesHelper(UstNode node, Node parentNode) currentNode.NodeType = GetNodeType(child); currentNode.Name = child.Identifier; currentNode.Identifier = child.FullIdentifier; - if (!Graph.Contains(currentNode)) + if (!Graph.Keys.Contains(currentNode)) { childNodes.Add(currentNode); - Graph.Add(currentNode); + Graph.TryAdd(currentNode, currentNode.Identifier); } else { - currentNode = Graph.FirstOrDefault(n => n.Equals(currentNode)); + currentNode = Graph.Keys.FirstOrDefault(n => n.Equals(currentNode)); } var children = InitializeNodesHelper(child, currentNode); - children.ToList().ForEach(child => currentNode.ChildNodes.Add(child)); + children.ToList().ForEach(child => currentNode.ChildNodes.TryAdd(child, child.Identifier)); } else { @@ -371,71 +423,37 @@ private HashSet InitializeNodesHelper(UstNode node, Node parentNode) } return childNodes; } - private void CreateClassHierarchyEdges(Node sourceNode) + private void CreateClassHierarchyEdges(Node sourceNode, List baseTypes, string baseTypeOriginalDefinition) { - // TODO - Create a common Declaration class for classes and interfaces: - - var targetNodes = ClassNodes.Union(InterfaceNodes).ToList(); - - var baseTypes = new List(); - var baseTypeOriginalDefinition = string.Empty; - - if (sourceNode.UstNode is ClassDeclaration classDeclaration) - { - // Check base types list for interfaces - baseTypes = classDeclaration.BaseList; - baseTypeOriginalDefinition = classDeclaration.BaseTypeOriginalDefinition; - } - else if (sourceNode.UstNode is InterfaceDeclaration interfaceDeclaration) - { - // Check base types list for interfaces - baseTypes = interfaceDeclaration.BaseList; - baseTypeOriginalDefinition = interfaceDeclaration.BaseTypeOriginalDefinition; - } - else if (sourceNode.UstNode is StructDeclaration structDeclaration) - { - // Check base types list for interfaces - baseTypes = structDeclaration.BaseList; - baseTypeOriginalDefinition = structDeclaration.BaseTypeOriginalDefinition; - } - else if (sourceNode.UstNode is RecordDeclaration recordDeclaration) - { - // Check base types list for interfaces - baseTypes = recordDeclaration.BaseList; - baseTypeOriginalDefinition = recordDeclaration.BaseTypeOriginalDefinition; - } - else - { - // If it's neither, no need to continue - return; - } - if (!string.IsNullOrEmpty(baseTypeOriginalDefinition) && baseTypeOriginalDefinition != "object") { baseTypes.Add(baseTypeOriginalDefinition); } baseTypes.ForEach(baseType => { - var targetNode = targetNodes.FirstOrDefault(n => n.Identifier == baseType); - if (targetNode != null) + //If edge is already added, we dont need to proceed + var existingEdge = sourceNode.OutgoingEdges.FirstOrDefault(e => e.TargetNode.Identifier == baseType); + if (existingEdge == null) { - var edge = new Edge() + var targetNode = TypeNodes.Keys.FirstOrDefault(n => n.Identifier == baseType); + if (targetNode != null) { - EdgeType = EdgeType.Inheritance, - TargetNode = targetNode, - SourceNode = sourceNode - }; - sourceNode.OutgoingEdges.Add(edge); - targetNode.IncomingEdges.Add(edge); + var edge = new Edge() + { + EdgeType = EdgeType.Inheritance, + TargetNode = targetNode, + SourceNode = sourceNode + }; + sourceNode.OutgoingEdges.Add(edge); + targetNode.IncomingEdges.Add(edge); + } } }); } private void CreateEdges(Node sourceNode) { - var edgeCandidates = filteredUstNodeEdgeCandidates[sourceNode]; - - edgeCandidates.ForEach(edgeCandidate => - { + var edgeCandidates = filteredUstNodeEdgeCandidates[sourceNode].ToList(); + Parallel.ForEach(edgeCandidates, edgeCandidate => { //If edge is already added, we dont need to proceed var existingEdge = sourceNode.OutgoingEdges.FirstOrDefault(e => e.TargetNode.Identifier == edgeCandidate.FullIdentifier); @@ -443,7 +461,7 @@ private void CreateEdges(Node sourceNode) { if (existingEdge?.EdgeType != EdgeType.Declaration) { - var targetNode = TypeNodes.FirstOrDefault(c => c.Identifier == edgeCandidate.FullIdentifier); + var targetNode = TypeNodes.Keys.FirstOrDefault(c => c.Identifier == edgeCandidate.FullIdentifier); if (targetNode?.Equals(sourceNode) == false) { var edge = new Edge() @@ -461,7 +479,7 @@ private void CreateEdges(Node sourceNode) { if (existingEdge?.EdgeType != EdgeType.MemberAccess) { - var targetNode = TypeNodes.FirstOrDefault(c => c.Identifier == memberAccess.SemanticFullClassTypeName); + var targetNode = TypeNodes.Keys.FirstOrDefault(c => c.Identifier == memberAccess.SemanticFullClassTypeName); //Skip methods in same class if (targetNode?.Equals(sourceNode) == false) @@ -485,12 +503,12 @@ private void CreateEdges(Node sourceNode) if (existingEdge?.EdgeType != EdgeType.ObjectCreation) { // Find any constructors with the same signature - var targetNode = MethodNodes.FirstOrDefault(n => n.Identifier == edgeCandidate.FullIdentifier); + var targetNode = MethodNodes.Keys.FirstOrDefault(n => n.Identifier == edgeCandidate.FullIdentifier); // No constructors found, find the class type if (targetNode is null) { - targetNode = TypeNodes.FirstOrDefault(n => n.Identifier == objectCreationExpression.SemanticFullClassTypeName); + targetNode = TypeNodes.Keys.FirstOrDefault(n => n.Identifier == objectCreationExpression.SemanticFullClassTypeName); } //Skip methods in same class @@ -512,7 +530,7 @@ private void CreateEdges(Node sourceNode) { if (existingEdge?.EdgeType != EdgeType.Invocation) { - var targetNode = MethodNodes.FirstOrDefault(n => n.Identifier == edgeCandidate.FullIdentifier); + var targetNode = MethodNodes.Keys.FirstOrDefault(n => n.Identifier == edgeCandidate.FullIdentifier); //Skip methods in same class if (targetNode?.Equals(sourceNode) == false) { @@ -531,11 +549,11 @@ private void CreateEdges(Node sourceNode) } }); } - private List GetOrAddEdgeCandidates(Node parentNode) + private ConcurrentBag GetOrAddEdgeCandidates(Node parentNode) { if (!ustNodeEdgeCandidates.ContainsKey(parentNode)) { - ustNodeEdgeCandidates.Add(parentNode, new List()); + ustNodeEdgeCandidates.TryAdd(parentNode, new ConcurrentBag()); } return ustNodeEdgeCandidates[parentNode]; } @@ -548,40 +566,40 @@ public class Node { public Node() { - Properties = new Dictionary(); - OutgoingEdges = new List(); - IncomingEdges = new List(); - ChildNodes = new HashSet(); + Properties = new ConcurrentDictionary(); + OutgoingEdges = new ConcurrentBag(); + IncomingEdges = new ConcurrentBag(); + ChildNodes = new ConcurrentDictionary(); } public Node ParentNode { get; set; } - public HashSet ChildNodes { get; set; } + public ConcurrentDictionary ChildNodes { get; set; } public string Name { get; set; } public string Identifier { get; set; } public NodeType NodeType { get; set; } - public List Edges + public ConcurrentBag Edges { get { - return IncomingEdges.Union(OutgoingEdges).ToList(); + return new ConcurrentBag(IncomingEdges.Union(OutgoingEdges)); } } - public List AllEdges + public ConcurrentBag AllEdges { get { - return AllIncomingEdges.Union(AllOutgoingEdges).ToList(); + return new ConcurrentBag(AllIncomingEdges.Union(AllOutgoingEdges)); } } - public List OutgoingEdges { get; set; } - public List IncomingEdges { get; set; } - public List AllOutgoingEdges { get => OutgoingEdges.Union(ChildNodes.SelectMany(c => c.AllOutgoingEdges)).ToList(); } - public List AllIncomingEdges { get => IncomingEdges.Union(ChildNodes.SelectMany(c => c.AllIncomingEdges)).ToList(); } + public ConcurrentBag OutgoingEdges { get; set; } + public ConcurrentBag IncomingEdges { get; set; } + public ConcurrentBag AllOutgoingEdges { get => new ConcurrentBag(OutgoingEdges.Union(ChildNodes.SelectMany(c => c.Key.AllOutgoingEdges))); } + public ConcurrentBag AllIncomingEdges { get => new ConcurrentBag(IncomingEdges.Union(ChildNodes.SelectMany(c => c.Key.AllIncomingEdges))); } public UstNode UstNode { get; set; } - public Dictionary Properties { get; set; } + public ConcurrentDictionary Properties { get; set; } public override bool Equals(object obj) { var node = obj as Node; - if(node != null) + if (node != null) { return node.Identifier == this.Identifier && node.NodeType == this.NodeType; @@ -607,7 +625,7 @@ public Edge() public override bool Equals(object obj) { var edge = obj as Edge; - if(edge != null) + if (edge != null) { return edge.Identifier == this.Identifier && edge.EdgeType == this.EdgeType diff --git a/src/Analysis/Codelyzer.Analysis/Codelyzer.Analysis.csproj b/src/Analysis/Codelyzer.Analysis/Codelyzer.Analysis.csproj index 86bc88c2..442a0f95 100644 --- a/src/Analysis/Codelyzer.Analysis/Codelyzer.Analysis.csproj +++ b/src/Analysis/Codelyzer.Analysis/Codelyzer.Analysis.csproj @@ -16,6 +16,7 @@ + diff --git a/src/Analysis/Codelyzer.Analysis/SolutionAnalyzerResult.cs b/src/Analysis/Codelyzer.Analysis/SolutionAnalyzerResult.cs index f9d06e0d..026812d6 100644 --- a/src/Analysis/Codelyzer.Analysis/SolutionAnalyzerResult.cs +++ b/src/Analysis/Codelyzer.Analysis/SolutionAnalyzerResult.cs @@ -1,8 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Collections.Generic; +using Codelyzer.Analysis.Model; namespace Codelyzer.Analysis { diff --git a/src/Analysis/Codelyzer.Analysis/VisualBasicCodeAnalyzer.cs b/src/Analysis/Codelyzer.Analysis/VisualBasicCodeAnalyzer.cs index 8f655286..790ca5d4 100644 --- a/src/Analysis/Codelyzer.Analysis/VisualBasicCodeAnalyzer.cs +++ b/src/Analysis/Codelyzer.Analysis/VisualBasicCodeAnalyzer.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using Codelyzer.Analysis.VisualBasic; using Microsoft.CodeAnalysis.VisualBasic; +using Codelyzer.Analysis.Model.Build; namespace Codelyzer.Analysis { diff --git a/src/Codelyzer.sln b/src/Codelyzer.sln index 8656b51b..4502b2d9 100644 --- a/src/Codelyzer.sln +++ b/src/Codelyzer.sln @@ -19,6 +19,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Codelyzer.Analysis.VisualBa EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Codelyzer.Analysis.Languages.UnitTests", "..\tst\Codelyzer.Analysis.Languages.UnitTests\Codelyzer.Analysis.Languages.UnitTests.csproj", "{F27A5219-8DA1-4C39-A7A0-940530DC53FD}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Codelyzer.Analysis.Workspace", "Analysis\Codelyzer.Analysis.Workspace\Codelyzer.Analysis.Workspace.csproj", "{266F793B-44AF-4338-A820-E19A932F0C6B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Codelyzer.Analysis.Analyzers", "Analysis\Codelyzer.Analysis.Analyzer\Codelyzer.Analysis.Analyzers.csproj", "{5028A8B3-641D-4210-9FB8-D4C3589D5D82}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Codelyzer.Analysis.Workspace.Tests", "..\tst\Codelyzer.Analysis.Workspace.Tests\Codelyzer.Analysis.Workspace.Tests.csproj", "{E4FEA3EC-9F89-466B-A31C-A0E9E914DC14}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -57,18 +63,18 @@ Global {F27A5219-8DA1-4C39-A7A0-940530DC53FD}.Debug|Any CPU.Build.0 = Debug|Any CPU {F27A5219-8DA1-4C39-A7A0-940530DC53FD}.Release|Any CPU.ActiveCfg = Release|Any CPU {F27A5219-8DA1-4C39-A7A0-940530DC53FD}.Release|Any CPU.Build.0 = Release|Any CPU - {8516D00F-D387-4E2A-8455-596660192A02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8516D00F-D387-4E2A-8455-596660192A02}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8516D00F-D387-4E2A-8455-596660192A02}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8516D00F-D387-4E2A-8455-596660192A02}.Release|Any CPU.Build.0 = Release|Any CPU - {0E336692-DC5C-4D30-92BC-E89EBC06C5F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0E336692-DC5C-4D30-92BC-E89EBC06C5F7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0E336692-DC5C-4D30-92BC-E89EBC06C5F7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0E336692-DC5C-4D30-92BC-E89EBC06C5F7}.Release|Any CPU.Build.0 = Release|Any CPU - {ADA4B662-5481-4356-8DB4-BBE860C09BBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {ADA4B662-5481-4356-8DB4-BBE860C09BBB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {ADA4B662-5481-4356-8DB4-BBE860C09BBB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {ADA4B662-5481-4356-8DB4-BBE860C09BBB}.Release|Any CPU.Build.0 = Release|Any CPU + {266F793B-44AF-4338-A820-E19A932F0C6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {266F793B-44AF-4338-A820-E19A932F0C6B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {266F793B-44AF-4338-A820-E19A932F0C6B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {266F793B-44AF-4338-A820-E19A932F0C6B}.Release|Any CPU.Build.0 = Release|Any CPU + {5028A8B3-641D-4210-9FB8-D4C3589D5D82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5028A8B3-641D-4210-9FB8-D4C3589D5D82}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5028A8B3-641D-4210-9FB8-D4C3589D5D82}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5028A8B3-641D-4210-9FB8-D4C3589D5D82}.Release|Any CPU.Build.0 = Release|Any CPU + {E4FEA3EC-9F89-466B-A31C-A0E9E914DC14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E4FEA3EC-9F89-466B-A31C-A0E9E914DC14}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E4FEA3EC-9F89-466B-A31C-A0E9E914DC14}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E4FEA3EC-9F89-466B-A31C-A0E9E914DC14}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/tst/Codelyzer.Analysis.Tests/AnalyzerRefacTests.cs b/tst/Codelyzer.Analysis.Tests/AnalyzerRefacTests.cs index c8a9dc44..ed711818 100644 --- a/tst/Codelyzer.Analysis.Tests/AnalyzerRefacTests.cs +++ b/tst/Codelyzer.Analysis.Tests/AnalyzerRefacTests.cs @@ -12,7 +12,10 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Castle.Components.DictionaryAdapter; +using Microsoft.CodeAnalysis; using Assert = NUnit.Framework.Assert; +using System.Collections.Concurrent; namespace Codelyzer.Analysis.Tests { @@ -47,22 +50,10 @@ public void OneTimeTearDown() private void DownloadTestProjects() { - DownloadFromGitHub(@"https://github.com/FabianGosebrink/ASPNET-WebAPI-Sample/archive/671a629cab0382ecd6dec4833b3868f96f89da50.zip", "ASPNET-WebAPI-Sample-671a629cab0382ecd6dec4833b3868f96f89da50"); - DownloadFromGitHub(@"https://github.com/Duikmeester/MvcMusicStore/archive/e274968f2827c04cfefbe6493f0a784473f83f80.zip", "MvcMusicStore-e274968f2827c04cfefbe6493f0a784473f83f80"); - DownloadFromGitHub(@"https://github.com/nopSolutions/nopCommerce/archive/73567858b3e3ef281d1433d7ac79295ebed47ee6.zip", "nopCommerce-73567858b3e3ef281d1433d7ac79295ebed47ee6"); - DownloadFromGitHub(@"https://github.com/marknfawaz/TestProjects/zipball/master/", "TestProjects-latest"); - } - - private void DownloadFromGitHub(string link, string name) - { - using (var client = new HttpClient()) - { - var content = client.GetByteArrayAsync(link).Result; - var fileName = Path.Combine(downloadsDir, string.Concat(name, @".zip")); - File.WriteAllBytes(fileName, content); - ZipFile.ExtractToDirectory(fileName, downloadsDir, true); - File.Delete(fileName); - } + DownloadFromGitHub(@"https://github.com/FabianGosebrink/ASPNET-WebAPI-Sample/archive/671a629cab0382ecd6dec4833b3868f96f89da50.zip", "ASPNET-WebAPI-Sample-671a629cab0382ecd6dec4833b3868f96f89da50", downloadsDir); + DownloadFromGitHub(@"https://github.com/Duikmeester/MvcMusicStore/archive/e274968f2827c04cfefbe6493f0a784473f83f80.zip", "MvcMusicStore-e274968f2827c04cfefbe6493f0a784473f83f80", downloadsDir); + DownloadFromGitHub(@"https://github.com/nopSolutions/nopCommerce/archive/73567858b3e3ef281d1433d7ac79295ebed47ee6.zip", "nopCommerce-73567858b3e3ef281d1433d7ac79295ebed47ee6", downloadsDir); + DownloadFromGitHub(@"https://github.com/marknfawaz/TestProjects/zipball/master/", "TestProjects-latest", downloadsDir); } [Test] @@ -819,8 +810,7 @@ public ActionResult ChangePassword(ChangePasswordModel model) } } }"); - var analyzer = analyzerByLanguage.GetLanguageAnalyzerByFileType(".cs"); - result = await analyzer.AnalyzeFile(accountController.FileFullPath, result); + result = await analyzerByLanguage.AnalyzeFile(accountController.FileFullPath, result); var references = result.ProjectBuildResult.Project.MetadataReferences.Select(m => m.Display).ToList(); var updatedSourcefile = result.ProjectResult.SourceFileResults.FirstOrDefault(s => s.FileFullPath.Contains("AccountController.cs")); } @@ -902,6 +892,14 @@ public ActionResult ChangePassword(ChangePasswordModel model) var oneFileResult = await analyzer.AnalyzeFile(projectPath, filePath, null, references); var listOfFilesResult = await analyzer.AnalyzeFile(projectPath, new List { filePath }, null, references); var fileInfoResult = await analyzer.AnalyzeFile(projectPath, fileInfo, null, references); + + var fileInfoDictionary = new Dictionary() + { + { filePath, fileContent } + }; + var fileInfoResult2 = await analyzer.AnalyzeFile(projectPath, fileInfoDictionary, + new List(), new List()); + Assert.AreEqual(fileInfoResult.SourceFileBuildResults.Count, fileInfoResult2.SourceFileBuildResults.Count); var oneFileWithContentResult = await analyzer.AnalyzeFile(projectPath, filePath, fileContent, null, references); var oneFileResultPre = await analyzer.AnalyzeFile(projectPath, filePath, references, null); @@ -1501,7 +1499,7 @@ public async Task VBTestReferenceBuilds() [Test] public async Task TestModernizeGraph() { - string solutionPath = CopySolutionFolderToTemp("Modernize.Web.sln"); + string solutionPath = CopySolutionFolderToTemp("Modernize.Web.sln"); FileAssert.Exists(solutionPath); AnalyzerConfiguration configurationWithoutBuild = new AnalyzerConfiguration(LanguageOptions.CSharp) @@ -1560,17 +1558,17 @@ public async Task TestModernizeGraph() } }; - + CodeAnalyzerByLanguage analyzerWithoutBuild = new CodeAnalyzerByLanguage(configurationWithoutBuild, NullLogger.Instance); CodeAnalyzerByLanguage analyzerWithBuild = new CodeAnalyzerByLanguage(configurationWithBuild, NullLogger.Instance); var resultWithoutBuild = await analyzerWithoutBuild.AnalyzeSolutionWithGraph(solutionPath); var resultWithBuild = await analyzerWithBuild.AnalyzeSolutionWithGraph(solutionPath); - var projectGraphWithoutBuild = resultWithoutBuild.CodeGraph.ProjectNodes; - var projectGraphWithBuild = resultWithBuild.CodeGraph.ProjectNodes; - var classGraphWithoutBuild = resultWithoutBuild.CodeGraph?.ClassNodes; - var classGraphWithBuild = resultWithBuild.CodeGraph?.ClassNodes; + var projectGraphWithoutBuild = resultWithoutBuild.CodeGraph.ProjectNodes.Keys; + var projectGraphWithBuild = resultWithBuild.CodeGraph.ProjectNodes.Keys; + var classGraphWithoutBuild = resultWithoutBuild.CodeGraph?.ClassNodes.Keys; + var classGraphWithBuild = resultWithBuild.CodeGraph?.ClassNodes.Keys; // There are 5 projects in the solution Assert.AreEqual(5, projectGraphWithoutBuild.Count); @@ -1626,7 +1624,7 @@ public async Task TestModernizeGraph() [Test] public async Task TestModernizeParallelizeGraph() { - string solutionPath = CopySolutionFolderToTemp("Modernize.Web.sln"); + string solutionPath = CopySolutionFolderToTemp("Modernize.Web.sln"); FileAssert.Exists(solutionPath); AnalyzerConfiguration configurationParallel = new AnalyzerConfiguration(LanguageOptions.CSharp) @@ -1711,10 +1709,10 @@ public async Task TestModernizeParallelizeGraph() var resultNoParallel = await analyzerNoParallel.AnalyzeSolutionWithGraph(solutionPath); - var projectGraphParallel = resultParallel.CodeGraph.ProjectNodes; - var projectGraphNoParallel = resultNoParallel.CodeGraph.ProjectNodes; - var classGraphParallel = resultParallel.CodeGraph?.ClassNodes; - var classGraphNoParallel = resultNoParallel.CodeGraph?.ClassNodes; + var projectGraphParallel = resultParallel.CodeGraph.ProjectNodes.Keys; + var projectGraphNoParallel = resultNoParallel.CodeGraph.ProjectNodes.Keys; + var classGraphParallel = resultParallel.CodeGraph?.ClassNodes.Keys; + var classGraphNoParallel = resultNoParallel.CodeGraph?.ClassNodes.Keys; // There are 5 projects in the solution Assert.AreEqual(5, projectGraphParallel.Count); @@ -1766,8 +1764,9 @@ public async Task TestModernizeParallelizeGraph() ValidateRecordEdges(resultNoParallel.CodeGraph.RecordNodes); } - private void ValidateClassEdges(HashSet nodes) + private void ValidateClassEdges(ConcurrentDictionary nodesDictionary) { + var nodes = nodesDictionary.Keys.ToHashSet(); Assert.AreEqual(0, nodes.FirstOrDefault(c => c.Identifier.Equals("Modernize.Web.Common.Constants")).AllOutgoingEdges.Count); Assert.AreEqual(0, nodes.FirstOrDefault(c => c.Identifier.Equals("Modernize.Web.Models.Customer")).AllOutgoingEdges.Count); Assert.AreEqual(0, nodes.FirstOrDefault(c => c.Identifier.Equals("Modernize.Web.Models.Product")).AllOutgoingEdges.Count); @@ -1797,27 +1796,32 @@ private void ValidateClassEdges(HashSet nodes) Assert.AreEqual(3, nodes.FirstOrDefault(c => c.Identifier.Equals("Modernize.Web.Mvc.Data.ModernizeWebMvcContext")).AllOutgoingEdges.Count); Assert.AreEqual(8, nodes.FirstOrDefault(c => c.Identifier.Equals("Modernize.Web.Mvc.UtilityClass")).AllOutgoingEdges.Count); } - private void ValidateInterfaceEdges(HashSet nodes) + private void ValidateInterfaceEdges(ConcurrentDictionary nodesDictionary) { + var nodes = nodesDictionary.Keys.ToHashSet(); Assert.AreEqual(0, nodes.FirstOrDefault(c => c.Identifier.Equals("Modernize.Web.Models.Generics.IObjectType")).AllOutgoingEdges.Count); Assert.AreEqual(5, nodes.FirstOrDefault(c => c.Identifier.Equals("Modernize.Web.Facade.ICustomerFacade")).AllOutgoingEdges.Count); Assert.AreEqual(0, nodes.FirstOrDefault(c => c.Identifier.Equals("Modernize.Web.Facade.IProductFacade")).AllOutgoingEdges.Count); Assert.AreEqual(0, nodes.FirstOrDefault(c => c.Identifier.Equals("Modernize.Web.Facade.IPurchaseFacade")).AllOutgoingEdges.Count); } - private void ValidateStructEdges(HashSet nodes) + private void ValidateStructEdges(ConcurrentDictionary nodesDictionary) { + var nodes = nodesDictionary.Keys.ToHashSet(); Assert.AreEqual(3, nodes.FirstOrDefault(c => c.Identifier.Equals("Modernize.Web.Models.ValuesStruct")).AllIncomingEdges.Count); } - private void ValidateEnumEdges(HashSet nodes) + private void ValidateEnumEdges(ConcurrentDictionary nodesDictionary) { + var nodes = nodesDictionary.Keys.ToHashSet(); Assert.AreEqual(2, nodes.FirstOrDefault(c => c.Identifier.Equals("Modernize.Web.Models.ValuesEnum")).AllIncomingEdges.Count); } - private void ValidateRecordEdges(HashSet nodes) + private void ValidateRecordEdges(ConcurrentDictionary nodesDictionary) { + var nodes = nodesDictionary.Keys.ToHashSet(); Assert.AreEqual(3, nodes.FirstOrDefault(c => c.Identifier.Equals("Modernize.Web.Models.ValuesRecord")).AllIncomingEdges.Count); } - private void ValidateMethodEdges(HashSet nodes) + private void ValidateMethodEdges(ConcurrentDictionary nodesDictionary) { + var nodes = nodesDictionary.Keys.ToHashSet(); Assert.AreEqual(0, nodes.FirstOrDefault(c => c.Identifier.Equals("Modernize.Web.Data.SqlProvider.GetSampleData()")).AllOutgoingEdges.Count); Assert.AreEqual(1, nodes.FirstOrDefault(c => c.Identifier.Equals("Modernize.Web.Data.SqlProvider.GetProducts()")).AllOutgoingEdges.Count); Assert.AreEqual(2, nodes.FirstOrDefault(c => c.Identifier.Equals("Modernize.Web.Data.SqlProvider.GetProduct()")).AllOutgoingEdges.Count); @@ -2037,9 +2041,8 @@ End Sub End Class End Class End Namespace"); - - var analyzer = analyzerByLanguage.GetLanguageAnalyzerByFileType(".vb"); - result = await analyzer.AnalyzeFile(signalRNode.FileFullPath, result); + + result = await analyzerByLanguage.AnalyzeFile(signalRNode.FileFullPath, result); var references = result.ProjectBuildResult.Project.MetadataReferences.Select(m => m.Display).ToList(); var updatedSourcefile = result.ProjectResult.SourceFileResults.FirstOrDefault(s => s.FileFullPath.Contains("SignalR.vb")); Assert.IsNotNull(updatedSourcefile); @@ -2186,24 +2189,6 @@ public async Task TestLineOfCodeVB() Assert.AreEqual(232, results[0].ProjectResult.LinesOfCode); } #region private methods - private void DeleteDir(string path, int retries = 0) - { - if (retries <= 10) - { - try - { - if (Directory.Exists(path)) - { - Directory.Delete(path, true); - } - } - catch (Exception) - { - Thread.Sleep(10000); - DeleteDir(path, retries + 1); - } - } - } private static IEnumerable TestCliMetaDataSource { diff --git a/tst/Codelyzer.Analysis.Tests/AnalyzerTest.cs b/tst/Codelyzer.Analysis.Tests/AnalyzerTest.cs index 21191df0..f6a67d11 100644 --- a/tst/Codelyzer.Analysis.Tests/AnalyzerTest.cs +++ b/tst/Codelyzer.Analysis.Tests/AnalyzerTest.cs @@ -1149,24 +1149,6 @@ public async Task TestLineOfCodeVB() Assert.AreEqual(232, results[0].ProjectResult.LinesOfCode); } #region private methods - private void DeleteDir(string path, int retries = 0) - { - if(retries <= 10) - { - try - { - if (Directory.Exists(path)) - { - Directory.Delete(path, true); - } - } - catch (Exception) - { - Thread.Sleep(10000); - DeleteDir(path, retries + 1); - } - } - } private static IEnumerable TestCliMetaDataSource { diff --git a/tst/Codelyzer.Analysis.Tests/AnalyzerWithGeneratorTests.cs b/tst/Codelyzer.Analysis.Tests/AnalyzerWithGeneratorTests.cs index f5607e77..bc4293b5 100644 --- a/tst/Codelyzer.Analysis.Tests/AnalyzerWithGeneratorTests.cs +++ b/tst/Codelyzer.Analysis.Tests/AnalyzerWithGeneratorTests.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging.Abstractions; using NUnit.Framework; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -819,8 +820,7 @@ public ActionResult ChangePassword(ChangePasswordModel model) } } }"); - var analyzer = analyzerByLanguage.GetLanguageAnalyzerByFileType(".cs"); - result = await analyzer.AnalyzeFile(accountController.FileFullPath, result); + result = await analyzerByLanguage.AnalyzeFile(accountController.FileFullPath, result); var references = result.ProjectBuildResult.Project.MetadataReferences.Select(m => m.Display).ToList(); var updatedSourcefile = result.ProjectResult.SourceFileResults.FirstOrDefault(s => s.FileFullPath.Contains("AccountController.cs")); } @@ -1596,17 +1596,17 @@ public async Task TestModernizeGraph() } }; - + CodeAnalyzerByLanguage analyzerWithoutBuild = new CodeAnalyzerByLanguage(configurationWithoutBuild, NullLogger.Instance); CodeAnalyzerByLanguage analyzerWithBuild = new CodeAnalyzerByLanguage(configurationWithBuild, NullLogger.Instance); var resultWithoutBuild = await analyzerWithoutBuild.AnalyzeSolutionGeneratorWithGraph(solutionPath); var resultWithBuild = await analyzerWithBuild.AnalyzeSolutionGeneratorWithGraph(solutionPath); - var projectGraphWithoutBuild = resultWithoutBuild.CodeGraph.ProjectNodes; - var projectGraphWithBuild = resultWithBuild.CodeGraph.ProjectNodes; - var classGraphWithoutBuild = resultWithoutBuild.CodeGraph?.ClassNodes; - var classGraphWithBuild = resultWithBuild.CodeGraph?.ClassNodes; + var projectGraphWithoutBuild = resultWithoutBuild.CodeGraph.ProjectNodes.Keys; + var projectGraphWithBuild = resultWithBuild.CodeGraph.ProjectNodes.Keys; + var classGraphWithoutBuild = resultWithoutBuild.CodeGraph?.ClassNodes.Keys; + var classGraphWithBuild = resultWithBuild.CodeGraph?.ClassNodes.Keys; // There are 5 projects in the solution Assert.AreEqual(5, projectGraphWithoutBuild.Count); @@ -1658,8 +1658,9 @@ public async Task TestModernizeGraph() ValidateRecordEdges(resultWithBuild.CodeGraph.RecordNodes); } - private void ValidateClassEdges(HashSet nodes) + private void ValidateClassEdges(ConcurrentDictionary nodesDictionary) { + var nodes = nodesDictionary.Keys.ToHashSet(); Assert.AreEqual(0, nodes.FirstOrDefault(c => c.Identifier.Equals("Modernize.Web.Common.Constants")).AllOutgoingEdges.Count); Assert.AreEqual(0, nodes.FirstOrDefault(c => c.Identifier.Equals("Modernize.Web.Models.Customer")).AllOutgoingEdges.Count); Assert.AreEqual(0, nodes.FirstOrDefault(c => c.Identifier.Equals("Modernize.Web.Models.Product")).AllOutgoingEdges.Count); @@ -1689,27 +1690,32 @@ private void ValidateClassEdges(HashSet nodes) Assert.AreEqual(3, nodes.FirstOrDefault(c => c.Identifier.Equals("Modernize.Web.Mvc.Data.ModernizeWebMvcContext")).AllOutgoingEdges.Count); Assert.AreEqual(8, nodes.FirstOrDefault(c => c.Identifier.Equals("Modernize.Web.Mvc.UtilityClass")).AllOutgoingEdges.Count); } - private void ValidateInterfaceEdges(HashSet nodes) + private void ValidateInterfaceEdges(ConcurrentDictionary nodesDictionary) { + var nodes = nodesDictionary.Keys.ToHashSet(); Assert.AreEqual(0, nodes.FirstOrDefault(c => c.Identifier.Equals("Modernize.Web.Models.Generics.IObjectType")).AllOutgoingEdges.Count); Assert.AreEqual(5, nodes.FirstOrDefault(c => c.Identifier.Equals("Modernize.Web.Facade.ICustomerFacade")).AllOutgoingEdges.Count); Assert.AreEqual(0, nodes.FirstOrDefault(c => c.Identifier.Equals("Modernize.Web.Facade.IProductFacade")).AllOutgoingEdges.Count); Assert.AreEqual(0, nodes.FirstOrDefault(c => c.Identifier.Equals("Modernize.Web.Facade.IPurchaseFacade")).AllOutgoingEdges.Count); } - private void ValidateStructEdges(HashSet nodes) + private void ValidateStructEdges(ConcurrentDictionary nodesDictionary) { + var nodes = nodesDictionary.Keys.ToHashSet(); Assert.AreEqual(3, nodes.FirstOrDefault(c => c.Identifier.Equals("Modernize.Web.Models.ValuesStruct")).AllIncomingEdges.Count); } - private void ValidateEnumEdges(HashSet nodes) + private void ValidateEnumEdges(ConcurrentDictionary nodesDictionary) { + var nodes = nodesDictionary.Keys.ToHashSet(); Assert.AreEqual(2, nodes.FirstOrDefault(c => c.Identifier.Equals("Modernize.Web.Models.ValuesEnum")).AllIncomingEdges.Count); } - private void ValidateRecordEdges(HashSet nodes) + private void ValidateRecordEdges(ConcurrentDictionary nodesDictionary) { + var nodes = nodesDictionary.Keys.ToHashSet(); Assert.AreEqual(3, nodes.FirstOrDefault(c => c.Identifier.Equals("Modernize.Web.Models.ValuesRecord")).AllIncomingEdges.Count); } - private void ValidateMethodEdges(HashSet nodes) + private void ValidateMethodEdges(ConcurrentDictionary nodesDictionary) { + var nodes = nodesDictionary.Keys.ToHashSet(); Assert.AreEqual(0, nodes.FirstOrDefault(c => c.Identifier.Equals("Modernize.Web.Data.SqlProvider.GetSampleData()")).AllOutgoingEdges.Count); Assert.AreEqual(1, nodes.FirstOrDefault(c => c.Identifier.Equals("Modernize.Web.Data.SqlProvider.GetProducts()")).AllOutgoingEdges.Count); Assert.AreEqual(2, nodes.FirstOrDefault(c => c.Identifier.Equals("Modernize.Web.Data.SqlProvider.GetProduct()")).AllOutgoingEdges.Count); @@ -1807,6 +1813,8 @@ private void ValidateMethodEdges(HashSet nodes) Assert.AreEqual(1, nodes.FirstOrDefault(c => c.Identifier.StartsWith("Modernize.Web.Mvc.UtilityClass.MethodReferencingEnumAsParameter()")).AllOutgoingEdges.Count); } + + [Test] public async Task VBLibraryClassAnalyze() { @@ -1929,9 +1937,8 @@ End Sub End Class End Class End Namespace"); - - var analyzer = analyzerByLanguage.GetLanguageAnalyzerByFileType(".vb"); - result = await analyzer.AnalyzeFile(signalRNode.FileFullPath, result); + + result = await analyzerByLanguage.AnalyzeFile(signalRNode.FileFullPath, result); var references = result.ProjectBuildResult.Project.MetadataReferences.Select(m => m.Display).ToList(); var updatedSourcefile = result.ProjectResult.SourceFileResults.FirstOrDefault(s => s.FileFullPath.Contains("SignalR.vb")); Assert.IsNotNull(updatedSourcefile); @@ -1976,24 +1983,6 @@ public async Task TestAnalyzeSolutionNoProjects() #region private methods - private void DeleteDir(string path, int retries = 0) - { - if (retries <= 10) - { - try - { - if (Directory.Exists(path)) - { - Directory.Delete(path, true); - } - } - catch (Exception) - { - Thread.Sleep(10000); - DeleteDir(path, retries + 1); - } - } - } private static IEnumerable TestCliMetaDataSource { diff --git a/tst/Codelyzer.Analysis.Tests/AwsBaseTest.cs b/tst/Codelyzer.Analysis.Tests/AwsBaseTest.cs index 38717e71..7eb9e3e6 100644 --- a/tst/Codelyzer.Analysis.Tests/AwsBaseTest.cs +++ b/tst/Codelyzer.Analysis.Tests/AwsBaseTest.cs @@ -1,4 +1,13 @@ +using Codelyzer.Analysis.Common; +using Microsoft.CodeAnalysis.MSBuild; +using Microsoft.CodeAnalysis; +using System; using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; namespace Codelyzer.Analysis.Tests { @@ -43,5 +52,83 @@ public string GetSrcPath(string path) { return Path.Combine(srcPath, path); } + + protected void DownloadFromGitHub(string link, string name, string downloadsDir) + { + using (var client = new HttpClient()) + { + var content = client.GetByteArrayAsync(link).Result; + var fileName = Path.Combine(downloadsDir, string.Concat(name, @".zip")); + File.WriteAllBytes(fileName, content); + ZipFile.ExtractToDirectory(fileName, downloadsDir, true); + File.Delete(fileName); + } + } + + protected void DeleteDir(string path, int retries = 0) + { + if (retries <= 10) + { + try + { + if (Directory.Exists(path)) + { + Directory.Delete(path, true); + } + } + catch (Exception) + { + Thread.Sleep(10000); + DeleteDir(path, retries + 1); + } + } + } + + protected string CopySolutionFolderToTemp(string solutionName, string downloadsDir, string tempDir) + { + string solutionPath = Directory.EnumerateFiles(downloadsDir, solutionName, SearchOption.AllDirectories) + .FirstOrDefault(); + string solutionDir = Directory.GetParent(solutionPath).FullName; + var newTempDir = Path.Combine(tempDir, Guid.NewGuid().ToString()); + FileUtils.DirectoryCopy(solutionDir, newTempDir); + + solutionPath = Directory.EnumerateFiles(newTempDir, solutionName, SearchOption.AllDirectories) + .FirstOrDefault(); + return solutionPath; + } + + protected async Task GetWorkspaceSolution(string solutionPath) + { + try + { + var workspace = MSBuildWorkspace.Create(); + var solution = await workspace.OpenSolutionAsync(solutionPath); + return solution; + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + return null; + } + } + + protected void SetupDefaultAnalyzerConfiguration(AnalyzerConfiguration configuration) + { + configuration.ExportSettings.GenerateJsonOutput = true; + configuration.ExportSettings.OutputPath = Path.Combine("/", "tmp", "UnitTests"); + configuration.MetaDataSettings.LiteralExpressions = true; + configuration.MetaDataSettings.MethodInvocations = true; + configuration.MetaDataSettings.Annotations = true; + configuration.MetaDataSettings.DeclarationNodes = true; + configuration.MetaDataSettings.LocationData = false; + configuration.MetaDataSettings.ReferenceData = true; + configuration.MetaDataSettings.InterfaceDeclarations = true; + configuration.MetaDataSettings.GenerateBinFiles = true; + configuration.MetaDataSettings.LoadBuildData = true; + configuration.MetaDataSettings.ReturnStatements = true; + configuration.MetaDataSettings.InvocationArguments = true; + configuration.MetaDataSettings.ElementAccess = true; + configuration.MetaDataSettings.MemberAccess = true; + } } } diff --git a/tst/Codelyzer.Analysis.Tests/Codelyzer.Analysis.Tests.csproj b/tst/Codelyzer.Analysis.Tests/Codelyzer.Analysis.Tests.csproj index 87f56c62..4912d26d 100644 --- a/tst/Codelyzer.Analysis.Tests/Codelyzer.Analysis.Tests.csproj +++ b/tst/Codelyzer.Analysis.Tests/Codelyzer.Analysis.Tests.csproj @@ -12,6 +12,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/tst/Codelyzer.Analysis.Tests/FileBuildHandlerTests.cs b/tst/Codelyzer.Analysis.Tests/FileBuildHandlerTests.cs index eb9f230d..945c5ca3 100644 --- a/tst/Codelyzer.Analysis.Tests/FileBuildHandlerTests.cs +++ b/tst/Codelyzer.Analysis.Tests/FileBuildHandlerTests.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.Linq; -using Codelyzer.Analysis.Build; +using Codelyzer.Analysis.Common; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.VisualBasic; using Microsoft.Extensions.Logging.Abstractions; diff --git a/tst/Codelyzer.Analysis.Tests/FileUtilsTests.cs b/tst/Codelyzer.Analysis.Tests/FileUtilsTests.cs index 37c4677c..1ce950a1 100644 --- a/tst/Codelyzer.Analysis.Tests/FileUtilsTests.cs +++ b/tst/Codelyzer.Analysis.Tests/FileUtilsTests.cs @@ -1,8 +1,10 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using Codelyzer.Analysis.Common; +using Codelyzer.Analysis.Model; using NUnit.Framework; namespace Codelyzer.Analysis.Tests @@ -165,5 +167,77 @@ public void GetProjectPathsFromSolutionFile_NoSLNFile() Assert.IsTrue(results.FirstOrDefault() == projectPath); Assert.IsTrue(File.Exists(projectPath)); } + + [Test] + public void TestGetRelativePath() + { + // FileUtils.GetRelativePath is needed because .NET Standard 2.0 does not have Path.GetRelativePath. + // Testing against actual method for correctness check. + + const string testFileName = "testRelativePathFile.txt"; + Directory.CreateDirectory(SourceDirPath); + var filePath = Path.Combine(Directory.GetCurrentDirectory(), SourceDirPath, testFileName); + File.Create(filePath).Dispose(); + + var actual = PathNetCore.GetRelativePath(Directory.GetCurrentDirectory(), filePath); + var expected = Path.GetRelativePath(Directory.GetCurrentDirectory(), filePath); + Assert.AreEqual(expected, actual); + + var actual2 = PathNetCore.GetRelativePath("Q:\\", filePath); + var expected2 = Path.GetRelativePath("Q:\\", filePath); + + Assert.AreEqual(expected2, actual2); + } + + [Test] + [TestCase(@"C:\", @"C:\", @".")] + [TestCase(@"C:\a", @"C:\a\", @".")] + [TestCase(@"C:\A", @"C:\a\", @".")] + [TestCase(@"C:\a\", @"C:\a", @".")] + [TestCase(@"C:\", @"C:\b", @"b")] + [TestCase(@"C:\a", @"C:\b", @"..\b")] + [TestCase(@"C:\a", @"C:\b\", @"..\b\")] + [TestCase(@"C:\a\b", @"C:\a", @"..")] + [TestCase(@"C:\a\b", @"C:\a\", @"..")] + [TestCase(@"C:\a\b\", @"C:\a", @"..")] + [TestCase(@"C:\a\b\", @"C:\a\", @"..")] + [TestCase(@"C:\a\b\c", @"C:\a\b", @"..")] + [TestCase(@"C:\a\b\c", @"C:\a\b\", @"..")] + [TestCase(@"C:\a\b\c", @"C:\a", @"..\..")] + [TestCase(@"C:\a\b\c", @"C:\a\", @"..\..")] + [TestCase(@"C:\a\b\c\", @"C:\a\b", @"..")] + [TestCase(@"C:\a\b\c\", @"C:\a\b\", @"..")] + [TestCase(@"C:\a\b\c\", @"C:\a", @"..\..")] + [TestCase(@"C:\a\b\c\", @"C:\a\", @"..\..")] + [TestCase(@"C:\a\", @"C:\b", @"..\b")] + [TestCase(@"C:\a", @"C:\a\b", @"b")] + [TestCase(@"C:\a", @"C:\A\b", @"b")] + [TestCase(@"C:\a", @"C:\b\c", @"..\b\c")] + [TestCase(@"C:\a\", @"C:\a\b", @"b")] + [TestCase(@"C:\", @"D:\", @"D:\")] + [TestCase(@"C:\", @"D:\b", @"D:\b")] + [TestCase(@"C:\", @"D:\b\", @"D:\b\")] + [TestCase(@"C:\a", @"D:\b", @"D:\b")] + [TestCase(@"C:\a\", @"D:\b", @"D:\b")] + [TestCase(@"C:\ab", @"C:\a", @"..\a")] + [TestCase(@"C:\a", @"C:\ab", @"..\ab")] + [TestCase(@"C:\", @"\\LOCALHOST\Share\b", @"\\LOCALHOST\Share\b")] + [TestCase(@"\\LOCALHOST\Share\a", @"\\LOCALHOST\Share\b", @"..\b")] + //[PlatformSpecific(TestPlatforms.Windows)] // Tests Windows-specific paths + public static void GetRelativePath_Windows(string relativeTo, string path, string expected) + { + string result = PathNetCore.GetRelativePath(relativeTo, path); + Assert.AreEqual(Path.GetRelativePath(relativeTo, path), result); + + // Check that we get the equivalent path when the result is combined with the sources + Assert.IsTrue( + Path.GetFullPath(path) + .TrimEnd(Path.DirectorySeparatorChar) + .Equals( + Path.GetFullPath(Path.Combine(Path.GetFullPath(relativeTo), + result)) + .TrimEnd(Path.DirectorySeparatorChar), + StringComparison.OrdinalIgnoreCase)); + } } } diff --git a/tst/Codelyzer.Analysis.Tests/ProjectBuildHandlerTests.cs b/tst/Codelyzer.Analysis.Tests/ProjectBuildHandlerTests.cs index 14627d97..2b1d2768 100644 --- a/tst/Codelyzer.Analysis.Tests/ProjectBuildHandlerTests.cs +++ b/tst/Codelyzer.Analysis.Tests/ProjectBuildHandlerTests.cs @@ -3,9 +3,11 @@ using System.IO; using System.Xml.Linq; using Codelyzer.Analysis.Build; +using Codelyzer.Analysis.Common; using NUnit.Framework; using Microsoft.CodeAnalysis; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Moq; namespace Codelyzer.Analysis.Tests @@ -58,12 +60,12 @@ public void ExtractFileReferencesFromProject_Retrieves_Expected_ReferencePaths() { using var stringReader = new StringReader(projectFileContent); var projectFileDoc = XDocument.Load(stringReader); - var projectBuildHandlerInstance = new ProjectBuildHandler(null); + var projectBuildHelperInstance = new ProjectBuildHelper(NullLogger.Instance); var extractFileReferencesFromProjectMethod = - TestUtils.GetPrivateMethod(projectBuildHandlerInstance.GetType(), "ExtractFileReferencesFromProject"); + TestUtils.GetPrivateMethod(projectBuildHelperInstance.GetType(), "ExtractFileReferencesFromProject"); // Invoke method and read contents of method output - var fileReferences = (List)extractFileReferencesFromProjectMethod.Invoke(projectBuildHandlerInstance, new object[] { projectFileDoc }); + var fileReferences = (List)extractFileReferencesFromProjectMethod.Invoke(projectBuildHelperInstance, new object[] { projectFileDoc }); var expectedFileReferences = new List { @"C:\\RandomFile.dll", @@ -81,12 +83,12 @@ public void LoadProjectFile_Returns_Expected_XDocument() "ProjectFileWithNonExistingMetaReferences.xml" )); File.WriteAllText(testProjectFilePath, projectFileContent); - var projectBuildHandlerInstance = new ProjectBuildHandler(null); + var projectBuildHelperInstance = new ProjectBuildHelper(null); var loadProjectFileMethod = - TestUtils.GetPrivateMethod(projectBuildHandlerInstance.GetType(), "LoadProjectFile"); + TestUtils.GetPrivateMethod(projectBuildHelperInstance.GetType(), "LoadProjectFile"); // Invoke method and read contents of method output - var projectFile = (XDocument)loadProjectFileMethod.Invoke(projectBuildHandlerInstance, new object[] { testProjectFilePath }); + var projectFile = (XDocument)loadProjectFileMethod.Invoke(projectBuildHelperInstance, new object[] { testProjectFilePath }); Assert.AreEqual(projectFileContent, projectFile.ToString()); } @@ -95,12 +97,12 @@ public void LoadProjectFile_Returns_Expected_XDocument() [Test] public void LoadProjectFile_Returns_Null_On_Invalid_ProjectFilePath() { - var projectBuildHandlerInstance = new ProjectBuildHandler(null); + var projectBuildHelperInstance = new ProjectBuildHelper(null); var loadProjectFileMethod = - TestUtils.GetPrivateMethod(projectBuildHandlerInstance.GetType(), "LoadProjectFile"); + TestUtils.GetPrivateMethod(projectBuildHelperInstance.GetType(), "LoadProjectFile"); // Invoke method and read contents of method output - var projectFile = (XDocument)loadProjectFileMethod.Invoke(projectBuildHandlerInstance, new object[] { @"C:\\Invalid\\ProjectFilePath.csproj" }); + var projectFile = (XDocument)loadProjectFileMethod.Invoke(projectBuildHelperInstance, new object[] { @"C:\\Invalid\\ProjectFilePath.csproj" }); Assert.AreEqual(null, projectFile); } @@ -114,12 +116,12 @@ public void LoadProjectFile_Returns_Null_On_Invalid_ProjectFileContent() File.WriteAllText(testProjectFilePath, "Invalid Project File Content!!!"); var mockedLogger = new Mock(); - var projectBuildHandlerInstance = new ProjectBuildHandler(mockedLogger.Object, null, new List()); + var projectBuildHelperInstance = new ProjectBuildHelper(mockedLogger.Object); var loadProjectFileMethod = - TestUtils.GetPrivateMethod(projectBuildHandlerInstance.GetType(), "LoadProjectFile"); + TestUtils.GetPrivateMethod(projectBuildHelperInstance.GetType(), "LoadProjectFile"); // Invoke method and read contents of method output - var projectFile = (XDocument)loadProjectFileMethod.Invoke(projectBuildHandlerInstance, new object[] { testProjectFilePath }); + var projectFile = (XDocument)loadProjectFileMethod.Invoke(projectBuildHelperInstance, new object[] { testProjectFilePath }); Assert.AreEqual(null, projectFile); } @@ -127,15 +129,10 @@ public void LoadProjectFile_Returns_Null_On_Invalid_ProjectFileContent() [Test] public void LoadMetadataReferences_Returns_Empty_On_Invalid_ProjectFile() { - var projectBuildHandlerInstance = new ProjectBuildHandler(null); - var loadMetadataReferencesMethod = - TestUtils.GetPrivateMethod(projectBuildHandlerInstance.GetType(), "LoadMetadataReferences"); - - // Invoke method and read contents of method output - var metadataReferences = (List)loadMetadataReferencesMethod.Invoke(projectBuildHandlerInstance, new object[] { null }); + var projectBuildHelperInstance = new ProjectBuildHelper(null); + var (metadataReferences, _) = projectBuildHelperInstance.LoadMetadataReferences(null); var expectedMetadataReferences = new List(); - - CollectionAssert.AreEquivalent(expectedMetadataReferences, metadataReferences); + CollectionAssert.AreEquivalent(expectedMetadataReferences, (List)metadataReferences); } [Test] @@ -144,18 +141,14 @@ public void LoadMetadataReferences_Returns_Empty_On_Invalid_ReferencePath() var projectFileDoc = XDocument.Load(new StringReader(projectFileContent)); var mockedLogger = new Mock(); - var projectBuildHandlerInstance = new ProjectBuildHandler(mockedLogger.Object, null, new List()); - var loadMetadataReferencesMethod = - TestUtils.GetPrivateMethod(projectBuildHandlerInstance.GetType(), "LoadMetadataReferences"); - + var projectBuildHelperInstance = new ProjectBuildHelper(mockedLogger.Object); + // Invoke method and read contents of method output - var metadataReferences = (List)loadMetadataReferencesMethod.Invoke(projectBuildHandlerInstance, new object[] { projectFileDoc }); + var (metadataReferences, missingMetaReferences) = projectBuildHelperInstance.LoadMetadataReferences(projectFileDoc); var expectedMetadataReferences = new List(); CollectionAssert.AreEquivalent(expectedMetadataReferences, metadataReferences); // Validate MissingMetaReferences - var prop = TestUtils.GetPrivateProperty(projectBuildHandlerInstance.GetType(), "MissingMetaReferences"); - List missingMetaReferences = (List)prop.GetValue(projectBuildHandlerInstance); List expectedMissingMetaReferences = new List { @"C:\\RandomFile.dll", @"C:\\this\\is\\some\\path\\to\\Some.dll" }; CollectionAssert.AreEquivalent(expectedMissingMetaReferences, missingMetaReferences); } @@ -192,17 +185,14 @@ public void LoadMetadataReferences_Returns_Expected_ReferencePath() var projectFileDoc = XDocument.Load(new StringReader(projectFileContent)); var mockedLogger = new Mock(); - var projectBuildHandlerInstance = new ProjectBuildHandler(mockedLogger.Object, null, new List()); - var loadMetadataReferencesMethod = - TestUtils.GetPrivateMethod(projectBuildHandlerInstance.GetType(), "LoadMetadataReferences"); + var projectBuildHelperInstance = new ProjectBuildHelper(mockedLogger.Object); // Invoke method and read contents of method output - var metadataReferences = (List)loadMetadataReferencesMethod.Invoke(projectBuildHandlerInstance, new object[] { projectFileDoc }); + var (metadataReferences, missingMetaReferences) = + projectBuildHelperInstance.LoadMetadataReferences(projectFileDoc); Assert.AreEqual(1, metadataReferences.Count); // Validate MissingMetaReferences - var prop = TestUtils.GetPrivateProperty(projectBuildHandlerInstance.GetType(), "MissingMetaReferences"); - List missingMetaReferences = (List)prop.GetValue(projectBuildHandlerInstance); List expectedMissingMetaReferences = new List { @"C:\\RandomFile.dll" }; CollectionAssert.AreEquivalent(expectedMissingMetaReferences, missingMetaReferences); } diff --git a/tst/Codelyzer.Analysis.Workspace.Tests/AnalyzerWithWorkspaceTests.cs b/tst/Codelyzer.Analysis.Workspace.Tests/AnalyzerWithWorkspaceTests.cs new file mode 100644 index 00000000..05c2f402 --- /dev/null +++ b/tst/Codelyzer.Analysis.Workspace.Tests/AnalyzerWithWorkspaceTests.cs @@ -0,0 +1,194 @@ +using NUnit.Framework; +using System; +using System.IO; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Codelyzer.Analysis.Analyzer; +using Microsoft.Extensions.Logging.Abstractions; +using Codelyzer.Analysis.Model; +using Microsoft.Build.Locator; +using AnalyzerResult = Codelyzer.Analysis.Model.AnalyzerResult; + +namespace Codelyzer.Analysis.Workspace.Tests +{ + internal class AnalyzerWithWorkspaceTests : WorkspaceBaseTest + { + private string _downloadsDir = ""; // A place to download example solutions one time for all tests + private string _tempDir = ""; // A place to copy example solutions for each test (as needed) + + [OneTimeSetUp] + public void OneTimeSetup() + { + Setup(GetType()); + _tempDir = GetTstPath(Path.Combine(Constants.TempProjectDirectories)); + _downloadsDir = GetTstPath(Path.Combine(Constants.TempProjectDownloadDirectories)); + DeleteDir(_tempDir); + DeleteDir(_downloadsDir); + Directory.CreateDirectory(_tempDir); + Directory.CreateDirectory(_downloadsDir); + DownloadTestProjects(); + SetupMsBuildLocator(); + } + + [OneTimeTearDown] + public void OneTimeTearDown() + { + DeleteDir(_tempDir); + DeleteDir(_downloadsDir); + } + + private void DownloadTestProjects() + { + DownloadFromGitHub(@"https://github.com/marknfawaz/TestProjects/zipball/master/", "TestProjects-latest", _downloadsDir); + } + + [Test] + [TestCase("OwinParadise.sln", "OwinExtraApi.cs")] + // TODO Team to check after release + //[TestCase("CoreWebApi.sln", "WeatherForecastController.cs")] + public async Task TestAnalyze(string solutionName, string fileName) + { + var (solutionPath, configuration, expectedResults) = CommonTestSetup(solutionName); + + var codeAnalyzerByLanguage = new CodeAnalyzerByLanguage(configuration, NullLogger.Instance); + var codeAnalyzer = new Analyzers.CodeAnalyzer(configuration, NullLogger.Instance); + var solution = await GetWorkspaceSolution(solutionPath); + var results = await codeAnalyzer.Analyze(solution); + var getCodeAnalyzerByLanguageResults = codeAnalyzerByLanguage.Analyze(solutionPath); + var result = results.FirstOrDefault(); + Assert.IsNotNull(result); + Assert.False(result.ProjectBuildResult.IsSyntaxAnalysis); + Assert.That(result.ProjectResult.ExternalReferences.NugetReferences, + Has.Count.EqualTo(expectedResults["NugetReferencesCount"])); + + Assert.That(result.ProjectResult.SourceFiles, Has.Count.EqualTo(expectedResults["SourceFilesCount"])); + + var fileUstNode = result.ProjectResult.SourceFileResults.FirstOrDefault( + f => f.FilePath.EndsWith(fileName)); + Assert.NotNull(fileUstNode); + + var classDeclarations = fileUstNode.AllClasses(); + var methodDeclarations = fileUstNode.AllMethods(); + + VerifyFileUstNode(fileUstNode, expectedResults); + + var semanticMethodSignatures = methodDeclarations.Select(m => m.SemanticSignature); + Assert.That(semanticMethodSignatures.Any(methodSignature => string.Compare( + expectedResults["MethodSignature"].ToString(), + methodSignature, + StringComparison.InvariantCulture) == 0), Is.True); + + var houseControllerClass = classDeclarations.First(c => + c.Identifier == expectedResults["ClassDeclarationIdentifier"].ToString()); + Assert.That(houseControllerClass.Modifiers, Is.EqualTo(expectedResults["ClassDeclarationModifier"])); + + var oldResults = await getCodeAnalyzerByLanguageResults; + var oldResult = oldResults.FirstOrDefault(); + Assert.That(oldResult, Is.Not.Null); + VerifyWorkspaceResults(oldResult, result, fileName); + + var languageAnalyzer = codeAnalyzerByLanguage.GetLanguageAnalyzerByFileType(".cs"); + var fileResult = languageAnalyzer.AnalyzeFile( + fileUstNode.FilePath, + results.First().ProjectBuildResult, + results.First()); + Assert.That(fileResult, Is.Not.Null); + } + + [Test] + [TestCase("OwinParadise.sln", "OwinExtraApi.cs")] + // TODO Team to check after release + //[TestCase("CoreWebApi.sln", "WeatherForecastController.cs")] + public async Task TestAnalyzeGenerator(string solutionName, string fileName) + { + var (solutionPath, configuration, expectedResults) = CommonTestSetup(solutionName); + + var getSolutionTask = GetWorkspaceSolution(solutionPath); + + var codeAnalyzerByLanguage = new CodeAnalyzerByLanguage(configuration, NullLogger.Instance); + var codeAnalyzer = new Analyzers.CodeAnalyzer(configuration, NullLogger.Instance); + + var resultAsyncEnumerable = codeAnalyzer.AnalyzeGeneratorAsync(await getSolutionTask); + var results = new List(); + var resultEnumerator = resultAsyncEnumerable.GetAsyncEnumerator(); + while (await resultEnumerator.MoveNextAsync()) + { + results.Add(resultEnumerator.Current); + } + var codeAnalyzerByLanguageResults = await codeAnalyzerByLanguage.Analyze(solutionPath); + VerifyWorkspaceResults(codeAnalyzerByLanguageResults.First(), results.First(), fileName); + } + + private (string, AnalyzerConfiguration, Dictionary) CommonTestSetup(string solutionName) + { + string solutionPath = CopySolutionFolderToTemp(solutionName, _downloadsDir, _tempDir); + FileAssert.Exists(solutionPath); + + var expectedResults = ExpectedResults.GetExpectedAnalyzerResults(solutionName); + var configuration = new AnalyzerConfiguration(LanguageOptions.CSharp); + SetupDefaultAnalyzerConfiguration(configuration); + + return (solutionPath, configuration, expectedResults); + } + + private void VerifyFileUstNode(RootUstNode fileUstNode, Dictionary expectedResults) + { + Assert.That(fileUstNode.AllBlockStatements(), Has.Count.EqualTo(expectedResults["BlockStatementsCount"])); + Assert.That(fileUstNode.AllClasses(), Has.Count.EqualTo(expectedResults["ClassesCount"])); + Assert.That(fileUstNode.AllExpressions(), Has.Count.EqualTo(expectedResults["ExpressionsCount"])); + Assert.That(fileUstNode.AllInvocationExpressions(), + Has.Count.EqualTo(expectedResults["InvocationExpressionsCount"])); + Assert.That(fileUstNode.AllLiterals(), Has.Count.EqualTo(expectedResults["LiteralExpressionsCount"])); + Assert.That(fileUstNode.AllMethods(), Has.Count.EqualTo(expectedResults["MethodsCount"])); + Assert.That(fileUstNode.AllReturnStatements(), Has.Count.EqualTo(expectedResults["ReturnStatementsCount"])); + Assert.That(fileUstNode.AllAnnotations(), Has.Count.EqualTo(expectedResults["AnnotationsCount"])); + Assert.That(fileUstNode.AllNamespaces(), Has.Count.EqualTo(expectedResults["NamespacesCount"])); + Assert.That(fileUstNode.AllObjectCreationExpressions(), + Has.Count.EqualTo(expectedResults["ObjectCreationCount"])); + Assert.That(fileUstNode.AllUsingDirectives(), Has.Count.EqualTo(expectedResults["UsingDirectivesCount"])); + Assert.That(fileUstNode.AllArguments(), Has.Count.EqualTo(expectedResults["ArgumentsCount"])); + Assert.That(fileUstNode.AllMemberAccessExpressions(), + Has.Count.EqualTo(expectedResults["MemberAccessExpressionsCount"])); + } + + private void VerifyWorkspaceResults( + AnalyzerResult buildalyzerResult, + AnalyzerResult workspaceResult, + string fileName) + { + Assert.That(workspaceResult.ProjectResult.ExternalReferences.NugetReferences.Count, + Is.EqualTo(buildalyzerResult.ProjectResult.ExternalReferences.NugetReferences.Count)); + Assert.That(workspaceResult.ProjectResult.ExternalReferences.SdkReferences.Count, + Is.EqualTo(buildalyzerResult.ProjectResult.ExternalReferences.SdkReferences.Count)); + Assert.That(workspaceResult.ProjectResult.SourceFiles.Count, + Is.EqualTo(buildalyzerResult.ProjectResult.SourceFiles.Count)); + + var buildalyzerSourceFileResult = + buildalyzerResult.ProjectResult.SourceFileResults.FirstOrDefault(f => + f.FilePath.EndsWith(fileName)); + var workspaceSourceFileResult = + workspaceResult.ProjectResult.SourceFileResults.FirstOrDefault(f => + f.FilePath.EndsWith(fileName)); + + Assert.That(buildalyzerSourceFileResult, Is.Not.Null); + Assert.That(workspaceSourceFileResult, Is.Not.Null); + Assert.That(workspaceSourceFileResult.AllBlockStatements(), + Is.EqualTo(buildalyzerSourceFileResult.AllBlockStatements())); + Assert.That(workspaceSourceFileResult.AllClasses(), Is.EqualTo(buildalyzerSourceFileResult.AllClasses())); + Assert.That(workspaceSourceFileResult.AllExpressions(), + Is.EqualTo(buildalyzerSourceFileResult.AllExpressions())); + Assert.That(workspaceSourceFileResult.AllInvocationExpressions(), + Is.EqualTo(buildalyzerSourceFileResult.AllInvocationExpressions())); + Assert.That(workspaceSourceFileResult.AllLiterals(), Is.EqualTo(buildalyzerSourceFileResult.AllLiterals())); + Assert.That(workspaceSourceFileResult.AllMethods(), Is.EqualTo(buildalyzerSourceFileResult.AllMethods())); + Assert.That(workspaceSourceFileResult.AllObjectCreationExpressions(), + Is.EqualTo(buildalyzerSourceFileResult.AllObjectCreationExpressions())); + Assert.That(workspaceSourceFileResult.AllUsingDirectives(), + Is.EqualTo(buildalyzerSourceFileResult.AllUsingDirectives())); + Assert.That(workspaceSourceFileResult.AllMemberAccessExpressions(), + Is.EqualTo(buildalyzerSourceFileResult.AllMemberAccessExpressions())); + } + + } +} diff --git a/tst/Codelyzer.Analysis.Workspace.Tests/Codelyzer.Analysis.Workspace.Tests.csproj b/tst/Codelyzer.Analysis.Workspace.Tests/Codelyzer.Analysis.Workspace.Tests.csproj new file mode 100644 index 00000000..934ff90b --- /dev/null +++ b/tst/Codelyzer.Analysis.Workspace.Tests/Codelyzer.Analysis.Workspace.Tests.csproj @@ -0,0 +1,34 @@ + + + + net6.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tst/Codelyzer.Analysis.Workspace.Tests/Constants.cs b/tst/Codelyzer.Analysis.Workspace.Tests/Constants.cs new file mode 100644 index 00000000..abf16d56 --- /dev/null +++ b/tst/Codelyzer.Analysis.Workspace.Tests/Constants.cs @@ -0,0 +1,12 @@ +namespace Codelyzer.Analysis.Workspace.Tests +{ + internal static class Constants + { + // Do not change these values without updating the corresponding line in .gitignore: + // **/Projects/Temp + // **/Projects/Downloads + // This is to prevent test projects from being picked up in git after failed unit tests. + internal static readonly string[] TempProjectDirectories = { "Projects", "Temp" }; + internal static readonly string[] TempProjectDownloadDirectories = { "Projects", "Downloads" }; + } +} \ No newline at end of file diff --git a/tst/Codelyzer.Analysis.Workspace.Tests/ExpectedResults.cs b/tst/Codelyzer.Analysis.Workspace.Tests/ExpectedResults.cs new file mode 100644 index 00000000..ebf23263 --- /dev/null +++ b/tst/Codelyzer.Analysis.Workspace.Tests/ExpectedResults.cs @@ -0,0 +1,68 @@ + + +using Codelyzer.Analysis.Model; + +namespace Codelyzer.Analysis.Workspace.Tests +{ + public static class ExpectedResults + { + public static Dictionary GetExpectedAnalyzerResults(string solutionName) + { + return solutionName switch + { + "OwinParadise.sln" => GetOwinParadiseResults(), + "CoreWebApi.sln" => GetCoreWebApiResults(), + _ => throw new Exception("Test results for solution name not found") + }; + } + + private static Dictionary GetOwinParadiseResults() + { + return new Dictionary() + { + { "BlockStatementsCount", 2 }, + { "ClassesCount", 1 }, + { "ExpressionsCount", 19 }, + { "InvocationExpressionsCount", 14 }, + { "LiteralExpressionsCount", 5 }, + { "MethodsCount", 2 }, + { "ReturnStatementsCount", 0 }, + { "AnnotationsCount", 0 }, + { "NamespacesCount", 1 }, + { "ObjectCreationCount", 8 }, + { "UsingDirectivesCount", 10 }, + { "ArgumentsCount", 14 }, + { "MemberAccessExpressionsCount", 6 }, + { "NugetReferencesCount", 31 }, + { "SourceFilesCount", 14 }, + { "MethodSignature", "public PortingParadise.OwinExtraApi.OwinAuthorization(IAuthorizationRequirement)" }, + { "ClassDeclarationIdentifier", "OwinExtraApi" }, + { "ClassDeclarationModifier", "public"} + }; + } + private static Dictionary GetCoreWebApiResults() + { + return new Dictionary() + { + { "BlockStatementsCount", 2 }, + { "ClassesCount", 1 }, + { "ExpressionsCount", 23 }, + { "InvocationExpressionsCount", 8 }, + { "LiteralExpressionsCount", 15 }, + { "MethodsCount", 1 }, + { "ReturnStatementsCount", 1 }, + { "AnnotationsCount", 3 }, + { "NamespacesCount", 1 }, + { "ObjectCreationCount", 2 }, + { "UsingDirectivesCount", 6 }, + { "ArgumentsCount", 8 }, + { "MemberAccessExpressionsCount", 8 }, + { "NugetReferencesCount", 0 }, + { "SourceFilesCount", 4 }, + { "MethodSignature", "public CoreWebApi.Controllers.WeatherForecastController.Get()" }, + { "ClassDeclarationIdentifier", "WeatherForecastController" }, + { "ClassDeclarationModifier", "public"} + }; + } + } +} \ No newline at end of file diff --git a/tst/Codelyzer.Analysis.Workspace.Tests/Usings.cs b/tst/Codelyzer.Analysis.Workspace.Tests/Usings.cs new file mode 100644 index 00000000..cefced49 --- /dev/null +++ b/tst/Codelyzer.Analysis.Workspace.Tests/Usings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; \ No newline at end of file diff --git a/tst/Codelyzer.Analysis.Workspace.Tests/WorkspaceBaseTest.cs b/tst/Codelyzer.Analysis.Workspace.Tests/WorkspaceBaseTest.cs new file mode 100644 index 00000000..fc4933dc --- /dev/null +++ b/tst/Codelyzer.Analysis.Workspace.Tests/WorkspaceBaseTest.cs @@ -0,0 +1,131 @@ +using Codelyzer.Analysis.Common; +using Microsoft.CodeAnalysis.MSBuild; +using Microsoft.CodeAnalysis; +using System; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Build.Locator; + +namespace Codelyzer.Analysis.Workspace.Tests +{ + public class WorkspaceBaseTest + { + private System.Type systemType; + private string tstPath; + private string srcPath; + + protected void Setup(System.Type type) + { + this.systemType = type; + this.tstPath = GetTstPath(type); + this.srcPath = GetSrcPath(type); + } + + private string GetTstPath(System.Type type) + { + // The path will get normalized inside the .GetProject() call below + string projectPath = Path.GetFullPath( + Path.Combine( + Path.GetDirectoryName(type.Assembly.Location), + Path.Combine(new string[] { "..", "..", "..", ".." }))); + return projectPath; + } + + private string GetSrcPath(System.Type type) + { + // The path will get normalized inside the .GetProject() call below + string projectPath = Path.GetFullPath( + Path.Combine( + Path.GetDirectoryName(type.Assembly.Location), + Path.Combine(new string[] { "..", "..", "..", "..", "..", "src" }))); + return projectPath; + } + + public string GetTstPath(string path) + { + return Path.Combine(tstPath, path); + } + + protected void DownloadFromGitHub(string link, string name, string downloadsDir) + { + using (var client = new HttpClient()) + { + var content = client.GetByteArrayAsync(link).Result; + var fileName = Path.Combine(downloadsDir, string.Concat(name, @".zip")); + File.WriteAllBytes(fileName, content); + ZipFile.ExtractToDirectory(fileName, downloadsDir, true); + File.Delete(fileName); + } + } + + protected void DeleteDir(string path, int retries = 0) + { + if (retries <= 10) + { + if (Directory.Exists(path)) + { + Directory.Delete(path, true); + } + } + } + + protected string CopySolutionFolderToTemp(string solutionName, string downloadsDir, string tempDir) + { + string solutionPath = Directory.EnumerateFiles(downloadsDir, + solutionName, + SearchOption.AllDirectories) + .FirstOrDefault() ?? string.Empty; + string solutionDir = Directory.GetParent(solutionPath).FullName; + var newTempDir = Path.Combine(tempDir, Guid.NewGuid().ToString()); + FileUtils.DirectoryCopy(solutionDir, newTempDir); + + solutionPath = Directory.EnumerateFiles(newTempDir, + solutionName, + SearchOption.AllDirectories) + .FirstOrDefault() ?? string.Empty; + return solutionPath; + } + + protected async Task GetWorkspaceSolution(string solutionPath) + { + var workspace = MSBuildWorkspace.Create(); + var solution = await workspace.OpenSolutionAsync(solutionPath); + return solution; + } + + protected void SetupDefaultAnalyzerConfiguration(AnalyzerConfiguration configuration) + { + configuration.ExportSettings.GenerateJsonOutput = true; + configuration.ExportSettings.OutputPath = Path.Combine("/", "tmp", "UnitTests"); + configuration.MetaDataSettings.LiteralExpressions = true; + configuration.MetaDataSettings.MethodInvocations = true; + configuration.MetaDataSettings.Annotations = true; + configuration.MetaDataSettings.DeclarationNodes = true; + configuration.MetaDataSettings.LocationData = false; + configuration.MetaDataSettings.ReferenceData = true; + configuration.MetaDataSettings.InterfaceDeclarations = true; + configuration.MetaDataSettings.GenerateBinFiles = true; + configuration.MetaDataSettings.LoadBuildData = true; + configuration.MetaDataSettings.ReturnStatements = true; + configuration.MetaDataSettings.InvocationArguments = true; + configuration.MetaDataSettings.ElementAccess = true; + configuration.MetaDataSettings.MemberAccess = true; + } + + protected void SetupMsBuildLocator() + { + try + { + MSBuildLocator.RegisterDefaults(); + } + catch (Exception e) + { + Console.WriteLine(e.Message); + } + } + } +} diff --git a/tst/Codelyzer.Analysis.Workspace.Tests/WorkspaceHelperTests.cs b/tst/Codelyzer.Analysis.Workspace.Tests/WorkspaceHelperTests.cs new file mode 100644 index 00000000..8c9aea2d --- /dev/null +++ b/tst/Codelyzer.Analysis.Workspace.Tests/WorkspaceHelperTests.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.MSBuild; +using NUnit.Framework; +using Codelyzer.Analysis.Workspace; +using Microsoft.Build.Locator; +using Microsoft.Extensions.Logging.Abstractions; +using Newtonsoft.Json; + +namespace Codelyzer.Analysis.Workspace.Tests +{ + internal class WorkspaceHelperTests : WorkspaceBaseTest + { + private string _downloadsDir = ""; // A place to download example solutions one time for all tests + private string _tempDir = ""; // A place to copy example solutions for each test (as needed) + + [OneTimeSetUp] + public void OneTimeSetup() + { + Setup(GetType()); + _tempDir = GetTstPath(Path.Combine(Constants.TempProjectDirectories)); + _downloadsDir = GetTstPath(Path.Combine(Constants.TempProjectDownloadDirectories)); + DeleteDir(_tempDir); + DeleteDir(_downloadsDir); + Directory.CreateDirectory(_tempDir); + Directory.CreateDirectory(_downloadsDir); + DownloadFromGitHub(@"https://github.com/marknfawaz/TestProjects/zipball/master/", "TestProjects-latest", _downloadsDir); + SetupMsBuildLocator(); + } + + [OneTimeTearDown] + public void OneTimeTearDown() + { + DeleteDir(_tempDir); + DeleteDir(_downloadsDir); + } + + [Test] + [TestCase("MixedClassLibrary.sln", ExpectedResult = true)] + public async Task TestGetProjectBuildResults(string solutionName) + { + string solutionPath = CopySolutionFolderToTemp(solutionName, _downloadsDir, _tempDir); + var solution = await GetWorkspaceSolution(solutionPath); + var results = await new WorkspaceHelper(NullLogger.Instance).GetProjectBuildResults(solution); + + Assert.That(results.Count, Is.EqualTo(2)); + + var vbProject = results[0]; + Assert.That(vbProject.SourceFileBuildResults.Count, Is.EqualTo(6)); + Assert.That(vbProject.ProjectGuid, Is.EqualTo("b72fbf90-e6d1-44a9-8cbd-d50a360f810c")); + Assert.That(vbProject.ExternalReferences.NugetReferences.Count, Is.EqualTo(4)); + + var csharpProject = results[1]; + Assert.That(csharpProject.SourceFileBuildResults.Count, Is.EqualTo(3)); + Assert.That(csharpProject.ProjectGuid, Is.EqualTo("93d5eb47-8ef4-4bd6-a4fc-adf81d92fb69")); + Assert.That(csharpProject.ExternalReferences.NugetReferences.Count, Is.EqualTo(5)); + + return true; + } + + [Test] + [TestCase("MixedClassLibrary.sln", ExpectedResult = true)] + public async Task TestGetProjectBuildResultsGenerator(string solutionName) + { + var solutionPath = CopySolutionFolderToTemp(solutionName, _downloadsDir, _tempDir); + var solution = await GetWorkspaceSolution(solutionPath); + var generator = new WorkspaceHelper(NullLogger.Instance) + .GetProjectBuildResultsGeneratorAsync(solution).GetAsyncEnumerator(); + var count = 0; + while (await generator.MoveNextAsync()) + { + var result = generator.Current; + Assert.IsNotNull(result); + count++; + } + Assert.That(count, Is.EqualTo(2)); + return true; + } + } +}