diff --git a/ChangeLog.md b/ChangeLog.md index 788cb50076..be3ee251fb 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Remove empty namespace declaration ([RCS1072](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1072)) - Remove empty region directive ([RCS1091](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1091)) - Remove empty destructor ([RCS1106](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1106)) +- [CLI] Add glob pattern matching (`--include` or/and `--exclude`) ([#1178](https://github.com/josefpihrt/roslynator/pull/1178)). ### Changed diff --git a/src/CSharp.Workspaces/CSharp/Spelling/CSharpSpellingService.CSharpSpellingAnalyzer.cs b/src/CSharp.Workspaces/CSharp/Spelling/CSharpSpellingService.CSharpSpellingAnalyzer.cs index 66ddd1d282..80f2814604 100644 --- a/src/CSharp.Workspaces/CSharp/Spelling/CSharpSpellingService.CSharpSpellingAnalyzer.cs +++ b/src/CSharp.Workspaces/CSharp/Spelling/CSharpSpellingService.CSharpSpellingAnalyzer.cs @@ -43,6 +43,9 @@ private void AnalyzeSyntaxTree(SyntaxTreeAnalysisContext context) { SyntaxTree tree = context.Tree; + if (_options.FileSystemFilter?.IsMatch(tree.FilePath) == false) + return; + SyntaxNode root = tree.GetRoot(context.CancellationToken); var analysisContext = new SpellingAnalysisContext( diff --git a/src/CommandLine/Commands/AbstractLinesOfCodeCommand`1.cs b/src/CommandLine/Commands/AbstractLinesOfCodeCommand`1.cs index c9b6a3b6d7..d159e58d9b 100644 --- a/src/CommandLine/Commands/AbstractLinesOfCodeCommand`1.cs +++ b/src/CommandLine/Commands/AbstractLinesOfCodeCommand`1.cs @@ -1,10 +1,82 @@ // Copyright (c) Josef Pihrt and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Roslynator.CodeMetrics; +using Roslynator.Host.Mef; +using static Roslynator.Logger; + namespace Roslynator.CommandLine; internal abstract class AbstractLinesOfCodeCommand : MSBuildWorkspaceCommand { - protected AbstractLinesOfCodeCommand(in ProjectFilter projectFilter) : base(projectFilter) + protected AbstractLinesOfCodeCommand(in ProjectFilter projectFilter, FileSystemFilter fileSystemFilter) : base(projectFilter, fileSystemFilter) + { + } + + public async Task> CountLinesAsync( + IEnumerable projects, + LinesOfCodeKind kind, + CodeMetricsOptions options = null, + CancellationToken cancellationToken = default) + { + var codeMetrics = new ConcurrentBag<(ProjectId projectId, CodeMetricsInfo codeMetrics)>(); + +#if NETFRAMEWORK + await Task.CompletedTask; + + Parallel.ForEach( + projects, + project => + { + ICodeMetricsService service = MefWorkspaceServices.Default.GetService(project.Language); + + CodeMetricsInfo projectMetrics = (service is not null) + ? service.CountLinesAsync(project, kind, FileSystemFilter, options, cancellationToken).Result + : CodeMetricsInfo.NotAvailable; + + codeMetrics.Add((project.Id, codeMetrics: projectMetrics)); + }); +#else + await Parallel.ForEachAsync( + projects, + cancellationToken, + async (project, cancellationToken) => + { + ICodeMetricsService service = MefWorkspaceServices.Default.GetService(project.Language); + + CodeMetricsInfo projectMetrics = (service is not null) + ? await service.CountLinesAsync(project, kind, FileSystemFilter, options, cancellationToken) + : CodeMetricsInfo.NotAvailable; + + codeMetrics.Add((project.Id, codeMetrics: projectMetrics)); + }); +#endif + return codeMetrics.ToImmutableDictionary(f => f.projectId, f => f.codeMetrics); + } + + protected static void WriteLinesOfCode(Solution solution, ImmutableDictionary projectsMetrics) { + int maxDigits = projectsMetrics.Max(f => f.Value.CodeLineCount).ToString("n0").Length; + int maxNameLength = projectsMetrics.Max(f => solution.GetProject(f.Key).Name.Length); + + foreach (KeyValuePair kvp in projectsMetrics + .OrderByDescending(f => f.Value.CodeLineCount) + .ThenBy(f => solution.GetProject(f.Key).Name)) + { + Project project = solution.GetProject(kvp.Key); + CodeMetricsInfo codeMetrics = kvp.Value; + + string count = (codeMetrics.CodeLineCount >= 0) + ? codeMetrics.CodeLineCount.ToString("n0").PadLeft(maxDigits) + : "-"; + + WriteLine($"{count} {project.Name.PadRight(maxNameLength)} {project.Language}", Verbosity.Normal); + } } } diff --git a/src/CommandLine/Commands/AnalyzeCommand.cs b/src/CommandLine/Commands/AnalyzeCommand.cs index 6b787d4e5e..ededeed91b 100644 --- a/src/CommandLine/Commands/AnalyzeCommand.cs +++ b/src/CommandLine/Commands/AnalyzeCommand.cs @@ -16,7 +16,7 @@ namespace Roslynator.CommandLine; internal class AnalyzeCommand : MSBuildWorkspaceCommand { - public AnalyzeCommand(AnalyzeCommandLineOptions options, DiagnosticSeverity severityLevel, in ProjectFilter projectFilter) : base(projectFilter) + public AnalyzeCommand(AnalyzeCommandLineOptions options, DiagnosticSeverity severityLevel, in ProjectFilter projectFilter, FileSystemFilter fileSystemFilter) : base(projectFilter, fileSystemFilter) { Options = options; SeverityLevel = severityLevel; @@ -31,6 +31,7 @@ public override async Task ExecuteAsync(ProjectOrSolution AssemblyResolver.Register(); var codeAnalyzerOptions = new CodeAnalyzerOptions( + fileSystemFilter: FileSystemFilter, ignoreAnalyzerReferences: Options.IgnoreAnalyzerReferences, ignoreCompilerDiagnostics: Options.IgnoreCompilerDiagnostics, reportNotConfigurable: Options.ReportNotConfigurable, @@ -80,7 +81,7 @@ public override async Task ExecuteAsync(ProjectOrSolution var projectFilter = new ProjectFilter(Options.Projects, Options.IgnoredProjects, Language); - results = await codeAnalyzer.AnalyzeSolutionAsync(solution, f => projectFilter.IsMatch(f), cancellationToken); + results = await codeAnalyzer.AnalyzeSolutionAsync(solution, f => IsMatch(f), cancellationToken); } return new AnalyzeCommandResult( diff --git a/src/CommandLine/Commands/FindSymbolsCommand.cs b/src/CommandLine/Commands/FindSymbolsCommand.cs index 435fa665f7..118c79177f 100644 --- a/src/CommandLine/Commands/FindSymbolsCommand.cs +++ b/src/CommandLine/Commands/FindSymbolsCommand.cs @@ -28,7 +28,8 @@ internal class FindSymbolsCommand : MSBuildWorkspaceCommand public FindSymbolsCommand( FindSymbolsCommandLineOptions options, SymbolFinderOptions symbolFinderOptions, - in ProjectFilter projectFilter) : base(projectFilter) + in ProjectFilter projectFilter, + FileSystemFilter fileSystemFilter) : base(projectFilter, fileSystemFilter) { Options = options; SymbolFinderOptions = symbolFinderOptions; diff --git a/src/CommandLine/Commands/FixCommand.cs b/src/CommandLine/Commands/FixCommand.cs index 387e5d2d6a..71bee423be 100644 --- a/src/CommandLine/Commands/FixCommand.cs +++ b/src/CommandLine/Commands/FixCommand.cs @@ -23,7 +23,8 @@ public FixCommand( IEnumerable> diagnosticFixMap, IEnumerable> diagnosticFixerMap, FixAllScope fixAllScope, - in ProjectFilter projectFilter) : base(projectFilter) + in ProjectFilter projectFilter, + FileSystemFilter fileSystemFilter) : base(projectFilter, fileSystemFilter) { Options = options; SeverityLevel = severityLevel; @@ -47,6 +48,7 @@ public override async Task ExecuteAsync(ProjectOrSolution proj AssemblyResolver.Register(); var codeFixerOptions = new CodeFixerOptions( + fileSystemFilter: FileSystemFilter, severityLevel: SeverityLevel, ignoreCompilerErrors: Options.IgnoreCompilerErrors, ignoreAnalyzerReferences: Options.IgnoreAnalyzerReferences, @@ -69,14 +71,13 @@ public override async Task ExecuteAsync(ProjectOrSolution proj var projectFilter = new ProjectFilter(Options.Projects, Options.IgnoredProjects, Language); - return await FixAsync(projectOrSolution, analyzerAssemblies, codeFixerOptions, projectFilter, culture, cancellationToken); + return await FixAsync(projectOrSolution, analyzerAssemblies, codeFixerOptions, culture, cancellationToken); } - private static async Task FixAsync( + private async Task FixAsync( ProjectOrSolution projectOrSolution, IEnumerable analyzerAssemblies, CodeFixerOptions codeFixerOptions, - ProjectFilter projectFilter, IFormatProvider formatProvider = null, CancellationToken cancellationToken = default) { @@ -114,7 +115,7 @@ private static async Task FixAsync( CodeFixer codeFixer = GetCodeFixer(solution); - results = await codeFixer.FixSolutionAsync(f => projectFilter.IsMatch(f), cancellationToken); + results = await codeFixer.FixSolutionAsync(f => IsMatch(f), cancellationToken); } WriteProjectFixResults(results, codeFixerOptions, formatProvider); diff --git a/src/CommandLine/Commands/FormatCommand.cs b/src/CommandLine/Commands/FormatCommand.cs index 65aafcc721..4f292a4807 100644 --- a/src/CommandLine/Commands/FormatCommand.cs +++ b/src/CommandLine/Commands/FormatCommand.cs @@ -19,7 +19,7 @@ namespace Roslynator.CommandLine; internal class FormatCommand : MSBuildWorkspaceCommand { - public FormatCommand(FormatCommandLineOptions options, in ProjectFilter projectFilter) : base(projectFilter) + public FormatCommand(FormatCommandLineOptions options, in ProjectFilter projectFilter, FileSystemFilter fileSystemFilter) : base(projectFilter, fileSystemFilter) { Options = options; } @@ -30,20 +30,18 @@ public override async Task ExecuteAsync(ProjectOrSolution p { ImmutableArray formattedDocuments; + var options = new CodeFormatterOptions(fileSystemFilter: FileSystemFilter, includeGeneratedCode: Options.IncludeGeneratedCode); + if (projectOrSolution.IsProject) { Project project = projectOrSolution.AsProject(); - var options = new CodeFormatterOptions(includeGeneratedCode: Options.IncludeGeneratedCode); - formattedDocuments = await FormatProjectAsync(project, options, cancellationToken); } else { Solution solution = projectOrSolution.AsSolution(); - var options = new CodeFormatterOptions(includeGeneratedCode: Options.IncludeGeneratedCode); - formattedDocuments = await FormatSolutionAsync(solution, options, cancellationToken); } @@ -60,6 +58,9 @@ private async Task> FormatSolutionAsync(Solution solu var changedDocuments = new ConcurrentBag>(); +#if NETFRAMEWORK + await Task.CompletedTask; + Parallel.ForEach( FilterProjects(solution), project => @@ -80,6 +81,29 @@ private async Task> FormatSolutionAsync(Solution solu WriteLine($" Done analyzing '{project.Name}'", Verbosity.Normal); }); +#else + await Parallel.ForEachAsync( + FilterProjects(solution), + cancellationToken, + async (project, cancellationToken) => + { + WriteLine($" Analyze '{project.Name}'", Verbosity.Minimal); + + ISyntaxFactsService syntaxFacts = MefWorkspaceServices.Default.GetService(project.Language); + + Project newProject = CodeFormatter.FormatProjectAsync(project, syntaxFacts, options, cancellationToken).Result; + + ImmutableArray formattedDocuments = await CodeFormatter.GetFormattedDocumentsAsync(project, newProject, syntaxFacts); + + if (formattedDocuments.Any()) + { + changedDocuments.Add(formattedDocuments); + LogHelpers.WriteFormattedDocuments(formattedDocuments, project, solutionDirectory); + } + + WriteLine($" Done analyzing '{project.Name}'", Verbosity.Normal); + }); +#endif if (!changedDocuments.IsEmpty) { diff --git a/src/CommandLine/Commands/GenerateDocCommand.cs b/src/CommandLine/Commands/GenerateDocCommand.cs index 656cdad9bf..c53a8bb84b 100644 --- a/src/CommandLine/Commands/GenerateDocCommand.cs +++ b/src/CommandLine/Commands/GenerateDocCommand.cs @@ -37,7 +37,8 @@ public GenerateDocCommand( FilesLayout filesLayout, bool groupByCommonNamespace, InheritanceStyle inheritanceStyle, - in ProjectFilter projectFilter) : base(projectFilter) + in ProjectFilter projectFilter, + FileSystemFilter fileSystemFilter) : base(projectFilter, fileSystemFilter) { Options = options; Depth = depth; @@ -118,7 +119,8 @@ public override async Task ExecuteAsync(ProjectOrSolution project IgnoredTitleParts = IgnoredTitleParts, IncludeContainingNamespaceFilter = IncludeContainingNamespaceFilter, FilesLayout = FilesLayout, - ScrollToContent = (DocumentationHost == DocumentationHost.GitHub) && Options.ScrollToContent + ScrollToContent = (DocumentationHost == DocumentationHost.GitHub) && Options.ScrollToContent, + FileSystemFilter = FileSystemFilter, }; if (Options.IgnoredNames is not null) diff --git a/src/CommandLine/Commands/GenerateDocRootCommand.cs b/src/CommandLine/Commands/GenerateDocRootCommand.cs index bebe368937..ba139427cc 100644 --- a/src/CommandLine/Commands/GenerateDocRootCommand.cs +++ b/src/CommandLine/Commands/GenerateDocRootCommand.cs @@ -29,7 +29,8 @@ public GenerateDocRootCommand( DocumentationHost documentationHost, FilesLayout filesLayout, bool groupByCommonNamespace, - in ProjectFilter projectFilter) : base(projectFilter) + in ProjectFilter projectFilter, + FileSystemFilter fileSystemFilter) : base(projectFilter, fileSystemFilter) { Options = options; Depth = depth; @@ -73,6 +74,7 @@ public override async Task ExecuteAsync(ProjectOrSolution project IncludeContainingNamespaceFilter = IncludeContainingNamespaceFilter, ScrollToContent = (DocumentationHost == DocumentationHost.GitHub) && Options.ScrollToContent, FilesLayout = FilesLayout, + FileSystemFilter = FileSystemFilter, }; if (Options.IgnoredNames is not null) diff --git a/src/CommandLine/Commands/GenerateSourceReferencesCommand.cs b/src/CommandLine/Commands/GenerateSourceReferencesCommand.cs index 78e4721591..7439a46140 100644 --- a/src/CommandLine/Commands/GenerateSourceReferencesCommand.cs +++ b/src/CommandLine/Commands/GenerateSourceReferencesCommand.cs @@ -22,7 +22,8 @@ public GenerateSourceReferencesCommand( GenerateSourceReferencesCommandLineOptions options, DocumentationDepth depth, Visibility visibility, - in ProjectFilter projectFilter) : base(projectFilter) + in ProjectFilter projectFilter, + FileSystemFilter fileSystemFilter) : base(projectFilter, fileSystemFilter) { Options = options; Depth = depth; @@ -39,7 +40,7 @@ public override async Task ExecuteAsync(ProjectOrSolution project { AssemblyResolver.Register(); - var filter = new SymbolFilterOptions(Visibility.ToVisibilityFilter()); + var filter = new SymbolFilterOptions(FileSystemFilter, Visibility.ToVisibilityFilter()); WriteLine($"Save source references to '{Options.Output}'.", Verbosity.Minimal); diff --git a/src/CommandLine/Commands/ListReferencesCommand.cs b/src/CommandLine/Commands/ListReferencesCommand.cs index a1c5d32d10..96b06ca32d 100644 --- a/src/CommandLine/Commands/ListReferencesCommand.cs +++ b/src/CommandLine/Commands/ListReferencesCommand.cs @@ -17,7 +17,8 @@ public ListReferencesCommand( ListReferencesCommandLineOptions options, MetadataReferenceDisplay display, MetadataReferenceFilter filter, - in ProjectFilter projectFilter) : base(projectFilter) + in ProjectFilter projectFilter, + FileSystemFilter fileSystemFilter) : base(projectFilter, fileSystemFilter) { Options = options; Display = display; diff --git a/src/CommandLine/Commands/ListSymbolsCommand.cs b/src/CommandLine/Commands/ListSymbolsCommand.cs index 61386d7090..9d22c87f94 100644 --- a/src/CommandLine/Commands/ListSymbolsCommand.cs +++ b/src/CommandLine/Commands/ListSymbolsCommand.cs @@ -31,7 +31,8 @@ public ListSymbolsCommand( WrapListOptions wrapListOptions, SymbolDefinitionListLayout layout, SymbolDefinitionPartFilter ignoredParts, - in ProjectFilter projectFilter) : base(projectFilter) + in ProjectFilter projectFilter, + FileSystemFilter fileSystemFilter) : base(projectFilter, fileSystemFilter) { Options = options; SymbolFilterOptions = symbolFilterOptions; diff --git a/src/CommandLine/Commands/LogicalLinesOfCodeCommand.cs b/src/CommandLine/Commands/LogicalLinesOfCodeCommand.cs index 145949afc4..f236175cbd 100644 --- a/src/CommandLine/Commands/LogicalLinesOfCodeCommand.cs +++ b/src/CommandLine/Commands/LogicalLinesOfCodeCommand.cs @@ -16,7 +16,7 @@ namespace Roslynator.CommandLine; internal class LogicalLinesOfCodeCommand : AbstractLinesOfCodeCommand { - public LogicalLinesOfCodeCommand(LogicalLinesOfCodeCommandLineOptions options, in ProjectFilter projectFilter) : base(projectFilter) + public LogicalLinesOfCodeCommand(LogicalLinesOfCodeCommandLineOptions options, in ProjectFilter projectFilter, FileSystemFilter fileSystemFilter) : base(projectFilter, fileSystemFilter) { Options = options; } @@ -37,7 +37,7 @@ public override async Task ExecuteAsync(ProjectOrSolut if (service is not null) { - codeMetrics = await CountLogicalLinesAsync(project, service, codeMetricsOptions, cancellationToken); + codeMetrics = await CountLinesAsync(project, service, codeMetricsOptions, cancellationToken); } else { @@ -47,7 +47,7 @@ public override async Task ExecuteAsync(ProjectOrSolut } else { - ImmutableDictionary codeMetricsByProject = CountLines(projectOrSolution.AsSolution(), codeMetricsOptions, cancellationToken); + ImmutableDictionary codeMetricsByProject = await CountLinesAsync(projectOrSolution.AsSolution(), codeMetricsOptions, cancellationToken); codeMetrics = CodeMetricsInfo.Create(codeMetricsByProject.Values); } @@ -55,13 +55,18 @@ public override async Task ExecuteAsync(ProjectOrSolut return new LinesOfCodeCommandResult(CommandStatus.Success, codeMetrics); } - private static async Task CountLogicalLinesAsync(Project project, ICodeMetricsService service, CodeMetricsOptions options, CancellationToken cancellationToken) + private async Task CountLinesAsync(Project project, ICodeMetricsService service, CodeMetricsOptions options, CancellationToken cancellationToken) { WriteLine($"Count logical lines for '{project.Name}'", ConsoleColors.Cyan, Verbosity.Minimal); Stopwatch stopwatch = Stopwatch.StartNew(); - CodeMetricsInfo codeMetrics = await service.CountLinesAsync(project, LinesOfCodeKind.Logical, options, cancellationToken); + CodeMetricsInfo codeMetrics = await service.CountLinesAsync( + project, + LinesOfCodeKind.Logical, + FileSystemFilter, + options, + cancellationToken); stopwatch.Stop(); @@ -78,7 +83,7 @@ private static async Task CountLogicalLinesAsync(Project projec return codeMetrics; } - private ImmutableDictionary CountLines(Solution solution, CodeMetricsOptions options, CancellationToken cancellationToken) + private async Task> CountLinesAsync(Solution solution, CodeMetricsOptions options, CancellationToken cancellationToken) { WriteLine($"Count logical lines for solution '{solution.FilePath}'", ConsoleColors.Cyan, Verbosity.Minimal); @@ -86,7 +91,7 @@ private ImmutableDictionary CountLines(Solution solu Stopwatch stopwatch = Stopwatch.StartNew(); - ImmutableDictionary codeMetrics = LinesOfCodeHelpers.CountLinesInParallel(projects, LinesOfCodeKind.Logical, options, cancellationToken); + ImmutableDictionary codeMetrics = await CountLinesAsync(projects, LinesOfCodeKind.Logical, options, cancellationToken); stopwatch.Stop(); @@ -95,7 +100,7 @@ private ImmutableDictionary CountLines(Solution solu WriteLine(Verbosity.Normal); WriteLine("Logical lines of code by project:", Verbosity.Normal); - LinesOfCodeHelpers.WriteLinesOfCode(solution, codeMetrics); + WriteLinesOfCode(solution, codeMetrics); } WriteMetrics( diff --git a/src/CommandLine/Commands/MSBuildWorkspaceCommand.cs b/src/CommandLine/Commands/MSBuildWorkspaceCommand.cs index 49d222c117..3310eb29fe 100644 --- a/src/CommandLine/Commands/MSBuildWorkspaceCommand.cs +++ b/src/CommandLine/Commands/MSBuildWorkspaceCommand.cs @@ -17,9 +17,10 @@ namespace Roslynator.CommandLine; internal abstract class MSBuildWorkspaceCommand where TCommandResult : CommandResult { - protected MSBuildWorkspaceCommand(in ProjectFilter projectFilter) + protected MSBuildWorkspaceCommand(in ProjectFilter projectFilter, FileSystemFilter fileSystemFilter) { ProjectFilter = projectFilter; + FileSystemFilter = fileSystemFilter; } public string Language @@ -29,6 +30,8 @@ public string Language public ProjectFilter ProjectFilter { get; } + public FileSystemFilter FileSystemFilter { get; } + public abstract Task ExecuteAsync(ProjectOrSolution projectOrSolution, CancellationToken cancellationToken = default); public async Task ExecuteAsync(IEnumerable paths, string msbuildPath = null, IEnumerable properties = null) @@ -337,7 +340,7 @@ private protected IEnumerable FilterProjects( { Project project = workspace.CurrentSolution.GetProject(projectId); - if (ProjectFilter.IsMatch(project)) + if (IsMatch(project)) { yield return project; } @@ -348,6 +351,12 @@ private protected IEnumerable FilterProjects( } } + private protected bool IsMatch(Project project) + { + return ProjectFilter.IsMatch(project) + && FileSystemFilter?.IsMatch(project.FilePath) != false; + } + private protected async Task> GetCompilationsAsync( ProjectOrSolution projectOrSolution, CancellationToken cancellationToken) diff --git a/src/CommandLine/Commands/PhysicalLinesOfCodeCommand.cs b/src/CommandLine/Commands/PhysicalLinesOfCodeCommand.cs index 1034885247..16369e75ea 100644 --- a/src/CommandLine/Commands/PhysicalLinesOfCodeCommand.cs +++ b/src/CommandLine/Commands/PhysicalLinesOfCodeCommand.cs @@ -16,7 +16,7 @@ namespace Roslynator.CommandLine; internal class PhysicalLinesOfCodeCommand : AbstractLinesOfCodeCommand { - public PhysicalLinesOfCodeCommand(PhysicalLinesOfCodeCommandLineOptions options, in ProjectFilter projectFilter) : base(projectFilter) + public PhysicalLinesOfCodeCommand(PhysicalLinesOfCodeCommandLineOptions options, in ProjectFilter projectFilter, FileSystemFilter fileSystemFilter) : base(projectFilter, fileSystemFilter) { Options = options; } @@ -52,7 +52,7 @@ public override async Task ExecuteAsync(ProjectOrSolut } else { - ImmutableDictionary codeMetricsByProject = CountLines(projectOrSolution.AsSolution(), codeMetricsOptions, cancellationToken); + ImmutableDictionary codeMetricsByProject = await CountLinesAsync(projectOrSolution.AsSolution(), codeMetricsOptions, cancellationToken); codeMetrics = CodeMetricsInfo.Create(codeMetricsByProject.Values); } @@ -66,7 +66,12 @@ private async Task CountLinesAsync(Project project, ICodeMetric Stopwatch stopwatch = Stopwatch.StartNew(); - CodeMetricsInfo codeMetrics = await service.CountLinesAsync(project, LinesOfCodeKind.Physical, options, cancellationToken); + CodeMetricsInfo codeMetrics = await service.CountLinesAsync( + project, + LinesOfCodeKind.Physical, + FileSystemFilter, + options, + cancellationToken); stopwatch.Stop(); @@ -84,7 +89,7 @@ private async Task CountLinesAsync(Project project, ICodeMetric return codeMetrics; } - private ImmutableDictionary CountLines(Solution solution, CodeMetricsOptions options, CancellationToken cancellationToken) + private async Task> CountLinesAsync(Solution solution, CodeMetricsOptions options, CancellationToken cancellationToken) { WriteLine($"Count lines for solution '{solution.FilePath}'", ConsoleColors.Cyan, Verbosity.Minimal); @@ -92,7 +97,7 @@ private ImmutableDictionary CountLines(Solution solu Stopwatch stopwatch = Stopwatch.StartNew(); - ImmutableDictionary codeMetrics = LinesOfCodeHelpers.CountLinesInParallel(projects, LinesOfCodeKind.Physical, options, cancellationToken); + ImmutableDictionary codeMetrics = await CountLinesAsync(projects, LinesOfCodeKind.Physical, options, cancellationToken); stopwatch.Stop(); @@ -101,7 +106,7 @@ private ImmutableDictionary CountLines(Solution solu WriteLine(Verbosity.Normal); WriteLine("Lines of code by project:", Verbosity.Normal); - LinesOfCodeHelpers.WriteLinesOfCode(solution, codeMetrics); + WriteLinesOfCode(solution, codeMetrics); } WriteMetrics( diff --git a/src/CommandLine/Commands/RenameSymbolCommand.cs b/src/CommandLine/Commands/RenameSymbolCommand.cs index 4bb1692d87..3f6a0c09b7 100644 --- a/src/CommandLine/Commands/RenameSymbolCommand.cs +++ b/src/CommandLine/Commands/RenameSymbolCommand.cs @@ -18,12 +18,13 @@ internal class RenameSymbolCommand : MSBuildWorkspaceCommand ignoredCompilerDiagnostics, int codeContext, Func predicate, - Func getNewName) : base(projectFilter) + Func getNewName) : base(projectFilter, fileSystemFilter) { Options = options; ScopeFilter = scopeFilter; @@ -80,7 +81,7 @@ public override async Task ExecuteAsync(ProjectOrSolu renamer = GetSymbolRenamer(solution); - await renamer.RenameSymbolsAsync(solution.Projects.Where(p => projectFilter.IsMatch(p)), cancellationToken); + await renamer.RenameSymbolsAsync(solution.Projects.Where(p => IsMatch(p)), cancellationToken); } return new RenameSymbolCommandResult(CommandStatus.Success); @@ -94,6 +95,7 @@ SymbolRenameState GetSymbolRenamer(Solution solution) SkipLocals = (ScopeFilter & RenameScopeFilter.Local) != 0, IncludeGeneratedCode = Options.IncludeGeneratedCode, DryRun = Options.DryRun, + FileSystemMatcher = FileSystemFilter?.Matcher, }; if (IgnoredCompilerDiagnostics is not null) diff --git a/src/CommandLine/Commands/SlnListCommand.cs b/src/CommandLine/Commands/SlnListCommand.cs index 24a6215c34..0a662231c2 100644 --- a/src/CommandLine/Commands/SlnListCommand.cs +++ b/src/CommandLine/Commands/SlnListCommand.cs @@ -15,7 +15,7 @@ namespace Roslynator.CommandLine; internal class SlnListCommand : MSBuildWorkspaceCommand { - public SlnListCommand(SlnListCommandLineOptions options, in ProjectFilter projectFilter) : base(projectFilter) + public SlnListCommand(SlnListCommandLineOptions options, in ProjectFilter projectFilter, FileSystemFilter fileSystemFilter) : base(projectFilter, fileSystemFilter) { Options = options; } diff --git a/src/CommandLine/Commands/SpellcheckCommand.cs b/src/CommandLine/Commands/SpellcheckCommand.cs index a9ff64a62b..1ec31f90ec 100644 --- a/src/CommandLine/Commands/SpellcheckCommand.cs +++ b/src/CommandLine/Commands/SpellcheckCommand.cs @@ -21,9 +21,10 @@ internal class SpellcheckCommand : MSBuildWorkspaceCommand ExecuteAsync(ProjectOrSoluti var options = new SpellcheckOptions() { + FileSystemFilter = FileSystemFilter, ScopeFilter = ScopeFilter, SymbolVisibility = visibilityFilter, MinWordLength = Options.MinWordLength, @@ -71,13 +73,12 @@ public override async Task ExecuteAsync(ProjectOrSoluti var projectFilter = new ProjectFilter(Options.Projects, Options.IgnoredProjects, Language); - return await FixAsync(projectOrSolution, options, projectFilter, culture, cancellationToken); + return await FixAsync(projectOrSolution, options, culture, cancellationToken); } private async Task FixAsync( ProjectOrSolution projectOrSolution, SpellcheckOptions options, - ProjectFilter projectFilter, IFormatProvider formatProvider = null, CancellationToken cancellationToken = default) { @@ -108,7 +109,7 @@ private async Task FixAsync( spellingFixer = GetSpellingFixer(solution); - results = await spellingFixer.FixSolutionAsync(f => projectFilter.IsMatch(f), cancellationToken); + results = await spellingFixer.FixSolutionAsync(f => IsMatch(f), cancellationToken); } SpellingData = spellingFixer.SpellingData; diff --git a/src/CommandLine/LinesOfCodeHelpers.cs b/src/CommandLine/LinesOfCodeHelpers.cs deleted file mode 100644 index f6df9a54a9..0000000000 --- a/src/CommandLine/LinesOfCodeHelpers.cs +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) Josef Pihrt and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis; -using Roslynator.CodeMetrics; -using Roslynator.Host.Mef; -using static Roslynator.Logger; - -namespace Roslynator.CommandLine; - -internal static class LinesOfCodeHelpers -{ - public static ImmutableDictionary CountLinesInParallel( - IEnumerable projects, - LinesOfCodeKind kind, - CodeMetricsOptions options = null, - CancellationToken cancellationToken = default) - { - var codeMetrics = new ConcurrentBag<(ProjectId projectId, CodeMetricsInfo codeMetrics)>(); - - Parallel.ForEach( - projects, - project => - { - ICodeMetricsService service = MefWorkspaceServices.Default.GetService(project.Language); - - CodeMetricsInfo projectMetrics = (service is not null) - ? service.CountLinesAsync(project, kind, options, cancellationToken).Result - : CodeMetricsInfo.NotAvailable; - - codeMetrics.Add((project.Id, codeMetrics: projectMetrics)); - }); - - return codeMetrics.ToImmutableDictionary(f => f.projectId, f => f.codeMetrics); - } - - public static void WriteLinesOfCode(Solution solution, ImmutableDictionary projectsMetrics) - { - int maxDigits = projectsMetrics.Max(f => f.Value.CodeLineCount).ToString("n0").Length; - int maxNameLength = projectsMetrics.Max(f => solution.GetProject(f.Key).Name.Length); - - foreach (KeyValuePair kvp in projectsMetrics - .OrderByDescending(f => f.Value.CodeLineCount) - .ThenBy(f => solution.GetProject(f.Key).Name)) - { - Project project = solution.GetProject(kvp.Key); - CodeMetricsInfo codeMetrics = kvp.Value; - - string count = (codeMetrics.CodeLineCount >= 0) - ? codeMetrics.CodeLineCount.ToString("n0").PadLeft(maxDigits) - : "-"; - - WriteLine($"{count} {project.Name.PadRight(maxNameLength)} {project.Language}", Verbosity.Normal); - } - } -} diff --git a/src/CommandLine/Options/MSBuildCommandLineOptions.cs b/src/CommandLine/Options/MSBuildCommandLineOptions.cs index 1397624cbe..4b2b77cf79 100644 --- a/src/CommandLine/Options/MSBuildCommandLineOptions.cs +++ b/src/CommandLine/Options/MSBuildCommandLineOptions.cs @@ -9,6 +9,18 @@ namespace Roslynator.CommandLine; // Files, IgnoredFiles public abstract class MSBuildCommandLineOptions : BaseCommandLineOptions { + [Option( + longName: "include", + HelpText = "Space separated list of glob patterns to include files/folders.", + MetaValue = "")] + public IEnumerable Include { get; set; } + + [Option( + longName: "exclude", + HelpText = "Space separated list of glob patterns to exclude files/folders.", + MetaValue = "")] + public IEnumerable Exclude { get; set; } + [Option( longName: OptionNames.IgnoredProjects, HelpText = "Defines projects that should not be analyzed.", diff --git a/src/CommandLine/Program.cs b/src/CommandLine/Program.cs index 160b4adc29..a78fce06e4 100644 --- a/src/CommandLine/Program.cs +++ b/src/CommandLine/Program.cs @@ -339,7 +339,8 @@ private static async Task FixAsync(FixCommandLineOptions options) diagnosticFixMap: diagnosticFixMap, diagnosticFixerMap: diagnosticFixerMap, fixAllScope: fixAllScope, - projectFilter: projectFilter); + projectFilter: projectFilter, + fileSystemFilter: FileSystemFilter.CreateOrDefault(options.Include, options.Exclude)); CommandStatus status = await command.ExecuteAsync(paths, options.MSBuildPath, options.Properties); @@ -357,7 +358,7 @@ private static async Task AnalyzeAsync(AnalyzeCommandLineOptions options) if (!TryParsePaths(options.Paths, out ImmutableArray paths)) return ExitCodes.Error; - var command = new AnalyzeCommand(options, severityLevel, projectFilter); + var command = new AnalyzeCommand(options, severityLevel, projectFilter, FileSystemFilter.CreateOrDefault(options.Include, options.Exclude)); CommandStatus status = await command.ExecuteAsync(paths, options.MSBuildPath, options.Properties); @@ -405,7 +406,10 @@ private static async Task FindSymbolsAsync(FindSymbolsCommandLineOptions op if (withoutFlags != SymbolFlags.None) rules.AddRange(SymbolFilterRuleFactory.FromFlags(withoutFlags, invert: true)); + FileSystemFilter fileSystemFilter = FileSystemFilter.CreateOrDefault(options.Include, options.Exclude); + var symbolFinderOptions = new SymbolFinderOptions( + fileSystemFilter, visibility: visibility, symbolGroups: symbolGroups, rules: rules, @@ -415,7 +419,8 @@ private static async Task FindSymbolsAsync(FindSymbolsCommandLineOptions op var command = new FindSymbolsCommand( options: options, symbolFinderOptions: symbolFinderOptions, - projectFilter: projectFilter); + projectFilter: projectFilter, + fileSystemFilter: fileSystemFilter); CommandStatus status = await command.ExecuteAsync(paths, options.MSBuildPath, options.Properties); @@ -478,6 +483,7 @@ private static async Task RenameSymbolAsync(RenameSymbolCommandLineOptions var command = new RenameSymbolCommand( options: options, projectFilter: projectFilter, + fileSystemFilter: FileSystemFilter.CreateOrDefault(options.Include, options.Exclude), scopeFilter: scopeFilter, errorResolution: errorResolution, ignoredCompilerDiagnostics: options.IgnoredCompilerDiagnostics, @@ -536,7 +542,10 @@ private static async Task ListSymbolsAsync(ListSymbolsCommandLineOptions op IgnoredAttributeNameFilterRule.Default, new IgnoredAttributeNameFilterRule(ignoredAttributes)); + FileSystemFilter fileSystemFilter = FileSystemFilter.CreateOrDefault(options.Include, options.Exclude); + var symbolFilterOptions = new SymbolFilterOptions( + fileSystemFilter: fileSystemFilter, visibility: visibilityFilter, symbolGroups: GetSymbolGroupFilter(), rules: rules, @@ -548,7 +557,8 @@ private static async Task ListSymbolsAsync(ListSymbolsCommandLineOptions op wrapListOptions: wrapListOptions, layout: layout, ignoredParts: ignoredParts, - projectFilter: projectFilter); + projectFilter: projectFilter, + fileSystemFilter: fileSystemFilter); CommandStatus status = await command.ExecuteAsync(paths, options.MSBuildPath, options.Properties); @@ -584,7 +594,7 @@ private static async Task FormatAsync(FormatCommandLineOptions options) CommandLineHelpers.WaitForKeyPress(); } - var command = new FormatCommand(options, projectFilter); + var command = new FormatCommand(options, projectFilter, FileSystemFilter.CreateOrDefault(options.Include, options.Exclude)); IEnumerable properties = options.Properties; @@ -644,7 +654,13 @@ private static async Task SpellcheckAsync(SpellcheckCommandLineOptions opti var data = new SpellingData(loaderResult.List, loaderResult.CaseSensitiveList, loaderResult.FixList); - var command = new SpellcheckCommand(options, projectFilter, data, visibility, scopeFilter); + var command = new SpellcheckCommand( + options, + projectFilter, + FileSystemFilter.CreateOrDefault(options.Include, options.Exclude), + data, + visibility, + scopeFilter); CommandStatus status = await command.ExecuteAsync(paths, options.MSBuildPath, options.Properties); @@ -660,7 +676,7 @@ private static async Task SlnListAsync(SlnListCommandLineOptions options) if (!TryParsePaths(options.Paths, out ImmutableArray paths)) return ExitCodes.Error; - var command = new SlnListCommand(options, projectFilter); + var command = new SlnListCommand(options, projectFilter, FileSystemFilter.CreateOrDefault(options.Include, options.Exclude)); CommandStatus status = await command.ExecuteAsync(paths, options.MSBuildPath, options.Properties); @@ -685,7 +701,7 @@ private static async Task PhysicalLinesOfCodeAsync(PhysicalLinesOfCodeComma if (!TryParsePaths(options.Paths, out ImmutableArray paths)) return ExitCodes.Error; - var command = new PhysicalLinesOfCodeCommand(options, projectFilter); + var command = new PhysicalLinesOfCodeCommand(options, projectFilter, FileSystemFilter.CreateOrDefault(options.Include, options.Exclude)); CommandStatus status = await command.ExecuteAsync(paths, options.MSBuildPath, options.Properties); @@ -700,7 +716,7 @@ private static async Task LogicalLinesOrCodeAsync(LogicalLinesOfCodeCommand if (!TryParsePaths(options.Paths, out ImmutableArray paths)) return ExitCodes.Error; - var command = new LogicalLinesOfCodeCommand(options, projectFilter); + var command = new LogicalLinesOfCodeCommand(options, projectFilter, FileSystemFilter.CreateOrDefault(options.Include, options.Exclude)); CommandStatus status = await command.ExecuteAsync(paths, options.MSBuildPath, options.Properties); @@ -776,7 +792,8 @@ private static async Task GenerateDocAsync(GenerateDocCommandLineOptions op filesLayout: filesLayout, groupByCommonNamespace: options.GroupByCommonNamespace, inheritanceStyle: inheritanceStyle, - projectFilter: projectFilter); + projectFilter: projectFilter, + fileSystemFilter: FileSystemFilter.CreateOrDefault(options.Include, options.Exclude)); CommandStatus status = await command.ExecuteAsync(paths, options.MSBuildPath, options.Properties); @@ -820,7 +837,8 @@ private static async Task GenerateDocRootAsync(GenerateDocRootCommandLineOp documentationHost, filesLayout, options.GroupByCommonNamespace, - projectFilter); + projectFilter, + FileSystemFilter.CreateOrDefault(options.Include, options.Exclude)); CommandStatus status = await command.ExecuteAsync(paths, options.MSBuildPath, options.Properties); @@ -848,7 +866,8 @@ private static async Task GenerateSourceReferencesAsync(GenerateSourceRefer options, depth, visibility, - projectFilter); + projectFilter, + FileSystemFilter.CreateOrDefault(options.Include, options.Exclude)); CommandStatus status = await command.ExecuteAsync(paths, options.MSBuildPath, options.Properties); @@ -911,7 +930,8 @@ private static async Task ListReferencesAsync(ListReferencesCommandLineOpti options, display, metadataReferenceFilter, - projectFilter); + projectFilter, + FileSystemFilter.CreateOrDefault(options.Include, options.Exclude)); CommandStatus status = await command.ExecuteAsync(paths, options.MSBuildPath, options.Properties); diff --git a/src/Documentation/DocumentationFilterOptions.cs b/src/Documentation/DocumentationFilterOptions.cs index 299293e509..535df69ff8 100644 --- a/src/Documentation/DocumentationFilterOptions.cs +++ b/src/Documentation/DocumentationFilterOptions.cs @@ -18,7 +18,7 @@ internal DocumentationFilterOptions( VisibilityFilter visibility = VisibilityFilter.All, SymbolGroupFilter symbolGroups = SymbolGroupFilter.TypeOrMember, IEnumerable rules = null, - IEnumerable attributeRules = null) : base(visibility, symbolGroups, rules, attributeRules) + IEnumerable attributeRules = null) : base(null, visibility, symbolGroups, rules, attributeRules) { } } diff --git a/src/Documentation/DocumentationGenerator.cs b/src/Documentation/DocumentationGenerator.cs index 2f74045145..e05ee62993 100644 --- a/src/Documentation/DocumentationGenerator.cs +++ b/src/Documentation/DocumentationGenerator.cs @@ -943,7 +943,8 @@ private IEnumerable GenerateMembers(TypeDocumentat { foreach (IGrouping grouping in typeModel .GetMembers(Options.IgnoredTypeParts) - .GroupBy(f => f.Name)) + .Where(s => !Options.ShouldBeIgnoredByLocation(s)) + .GroupBy(s => s.Name)) { using (IEnumerator en = grouping.GetEnumerator()) { diff --git a/src/Documentation/DocumentationOptions.cs b/src/Documentation/DocumentationOptions.cs index 8822c9c7a0..ee0fb08e71 100644 --- a/src/Documentation/DocumentationOptions.cs +++ b/src/Documentation/DocumentationOptions.cs @@ -97,6 +97,8 @@ public CommonDocumentationParts IgnoredCommonParts public bool ScrollToContent { get; set; } = DefaultValues.ScrollToContent; + internal FileSystemFilter FileSystemFilter { get; set; } + internal bool IncludeContainingNamespace(IncludeContainingNamespaceFilter filter) { return (IncludeContainingNamespaceFilter & filter) == filter; @@ -121,11 +123,19 @@ internal bool ShouldBeIgnored(INamedTypeSymbol typeSymbol) n = n.ContainingNamespace; } } + + if (ShouldBeIgnoredByLocation(typeSymbol)) + return true; } return false; } + internal bool ShouldBeIgnoredByLocation(ISymbol symbol) + { + return FileSystemFilter?.IsMatch(symbol) == false; + } + internal static class DefaultValues { public const DocumentationDepth Depth = DocumentationDepth.Member; diff --git a/src/Workspaces.Core/CodeAnalysisOptions.cs b/src/Workspaces.Core/CodeAnalysisOptions.cs index e76829b707..d81200d632 100644 --- a/src/Workspaces.Core/CodeAnalysisOptions.cs +++ b/src/Workspaces.Core/CodeAnalysisOptions.cs @@ -11,6 +11,7 @@ namespace Roslynator; internal abstract class CodeAnalysisOptions { internal CodeAnalysisOptions( + FileSystemFilter fileSystemFilter = null, DiagnosticSeverity severityLevel = DiagnosticSeverity.Info, bool ignoreAnalyzerReferences = false, bool concurrentAnalysis = true, @@ -26,6 +27,7 @@ internal CodeAnalysisOptions( SeverityLevel = severityLevel; IgnoreAnalyzerReferences = ignoreAnalyzerReferences; ConcurrentAnalysis = concurrentAnalysis; + FileSystemFilter = fileSystemFilter; SupportedDiagnosticIds = supportedDiagnosticIds?.ToImmutableHashSet() ?? ImmutableHashSet.Empty; IgnoredDiagnosticIds = ignoredDiagnosticIds?.ToImmutableHashSet() ?? ImmutableHashSet.Empty; } @@ -40,6 +42,8 @@ internal CodeAnalysisOptions( public ImmutableHashSet IgnoredDiagnosticIds { get; } + public FileSystemFilter FileSystemFilter { get; } + internal bool IsSupportedDiagnosticId(string diagnosticId) { return (SupportedDiagnosticIds.Count > 0) diff --git a/src/Workspaces.Core/CodeFixes/CodeFixer.cs b/src/Workspaces.Core/CodeFixes/CodeFixer.cs index 952bee74b8..053aab8061 100644 --- a/src/Workspaces.Core/CodeFixes/CodeFixer.cs +++ b/src/Workspaces.Core/CodeFixes/CodeFixer.cs @@ -530,8 +530,21 @@ private async Task> GetDiagnosticsAsync( ImmutableArray diagnostics = await GetAnalyzerDiagnosticsAsync(compilation, analyzers, analyzerOptions, cancellationToken).ConfigureAwait(false); return diagnostics - .Where(f => f.IsEffective(Options, compilation.Options) - && analyzersById.ContainsKey(f.Id)) + .Where(diagnostic => + { + if (diagnostic.IsEffective(Options, compilation.Options) + && analyzersById.ContainsKey(diagnostic.Id)) + { + SyntaxTree tree = diagnostic.Location.SourceTree; + if (tree is null + || Options.FileSystemFilter?.IsMatch(tree.FilePath) != false) + { + return true; + } + } + + return false; + }) .Except(except, DiagnosticDeepEqualityComparer.Instance) .ToImmutableArray(); } diff --git a/src/Workspaces.Core/CodeFixes/CodeFixerOptions.cs b/src/Workspaces.Core/CodeFixes/CodeFixerOptions.cs index 88f16df2f5..018d6564bc 100644 --- a/src/Workspaces.Core/CodeFixes/CodeFixerOptions.cs +++ b/src/Workspaces.Core/CodeFixes/CodeFixerOptions.cs @@ -17,6 +17,7 @@ internal class CodeFixerOptions : CodeAnalysisOptions public static CodeFixerOptions Default { get; } = new(); public CodeFixerOptions( + FileSystemFilter fileSystemFilter = null, DiagnosticSeverity severityLevel = DiagnosticSeverity.Info, bool ignoreCompilerErrors = false, bool ignoreAnalyzerReferences = false, @@ -32,6 +33,7 @@ public CodeFixerOptions( int maxIterations = -1, int batchSize = -1, bool format = false) : base( + fileSystemFilter: fileSystemFilter, severityLevel: severityLevel, ignoreAnalyzerReferences: ignoreAnalyzerReferences, concurrentAnalysis: concurrentAnalysis, diff --git a/src/Workspaces.Core/Diagnostics/CodeAnalyzer.cs b/src/Workspaces.Core/Diagnostics/CodeAnalyzer.cs index 6782413efe..a00fd0d188 100644 --- a/src/Workspaces.Core/Diagnostics/CodeAnalyzer.cs +++ b/src/Workspaces.Core/Diagnostics/CodeAnalyzer.cs @@ -201,9 +201,13 @@ private IEnumerable FilterDiagnostics(IEnumerable diagno SyntaxTree tree = diagnostic.Location.SourceTree; if (tree is null - || !GeneratedCodeUtility.IsGeneratedCode(tree, f => MefWorkspaceServices.Default.GetService(tree.Options.Language).IsComment(f), cancellationToken)) + || Options.FileSystemFilter?.IsMatch(tree.FilePath) != false) { - yield return diagnostic; + if (tree is null + || !GeneratedCodeUtility.IsGeneratedCode(tree, f => MefWorkspaceServices.Default.GetService(tree.Options.Language).IsComment(f), cancellationToken)) + { + yield return diagnostic; + } } } else diff --git a/src/Workspaces.Core/Diagnostics/CodeAnalyzerOptions.cs b/src/Workspaces.Core/Diagnostics/CodeAnalyzerOptions.cs index e548ee5e82..688cf05b65 100644 --- a/src/Workspaces.Core/Diagnostics/CodeAnalyzerOptions.cs +++ b/src/Workspaces.Core/Diagnostics/CodeAnalyzerOptions.cs @@ -10,6 +10,7 @@ internal class CodeAnalyzerOptions : CodeAnalysisOptions public static CodeAnalyzerOptions Default { get; } = new(); public CodeAnalyzerOptions( + FileSystemFilter fileSystemFilter = null, bool ignoreAnalyzerReferences = false, bool ignoreCompilerDiagnostics = false, bool reportNotConfigurable = false, @@ -19,6 +20,7 @@ public CodeAnalyzerOptions( DiagnosticSeverity severityLevel = DiagnosticSeverity.Info, IEnumerable supportedDiagnosticIds = null, IEnumerable ignoredDiagnosticIds = null) : base( + fileSystemFilter: fileSystemFilter, severityLevel: severityLevel, ignoreAnalyzerReferences: ignoreAnalyzerReferences, concurrentAnalysis: concurrentAnalysis, diff --git a/src/Workspaces.Core/Extensions/Extensions.cs b/src/Workspaces.Core/Extensions/Extensions.cs index 2b61c5b495..8a02ce90eb 100644 --- a/src/Workspaces.Core/Extensions/Extensions.cs +++ b/src/Workspaces.Core/Extensions/Extensions.cs @@ -11,12 +11,29 @@ using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.Diagnostics.Telemetry; using Microsoft.CodeAnalysis.Text; +using Microsoft.Extensions.FileSystemGlobbing; using Roslynator.CodeMetrics; namespace Roslynator; internal static class Extensions { + public static bool IsMatch(this Matcher matcher, ISymbol symbol) + { + foreach (Location location in symbol.Locations) + { + SyntaxTree tree = location.SourceTree; + + if (tree is not null + && !matcher.Match(tree.FilePath).HasMatches) + { + return false; + } + } + + return true; + } + public static SymbolGroupFilter ToSymbolGroupFilter(this TypeKind typeKind) { switch (typeKind) @@ -296,6 +313,7 @@ public static async Task CountLinesAsync( this ICodeMetricsService service, Project project, LinesOfCodeKind kind, + FileSystemFilter fileSystemFilter, CodeMetricsOptions options = null, CancellationToken cancellationToken = default) { @@ -306,6 +324,14 @@ public static async Task CountLinesAsync( if (!document.SupportsSyntaxTree) continue; + string filePath = document.FilePath; + + if (filePath is not null + && fileSystemFilter?.IsMatch(filePath) == false) + { + continue; + } + CodeMetricsInfo documentMetrics = await service.CountLinesAsync(document, kind, options, cancellationToken).ConfigureAwait(false); codeMetrics = codeMetrics.Add(documentMetrics); diff --git a/src/Workspaces.Core/FileSystemFilter.cs b/src/Workspaces.Core/FileSystemFilter.cs new file mode 100644 index 0000000000..432976e16b --- /dev/null +++ b/src/Workspaces.Core/FileSystemFilter.cs @@ -0,0 +1,82 @@ +// Copyright (c) Josef Pihrt and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.Extensions.FileSystemGlobbing; + +namespace Roslynator; + +internal sealed class FileSystemFilter +{ +#if DEBUG + private static readonly object _lock = new(); +#endif + + private FileSystemFilter(Matcher matcher) + { + Matcher = matcher; + } + + public Matcher Matcher { get; } + + public static FileSystemFilter CreateOrDefault( + IEnumerable include, + IEnumerable exclude) + { + if (!include.Any() + && !exclude.Any()) + { + return null; + } + + var matcher = new Matcher((FileSystemHelpers.IsCaseSensitive) ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase); + + if (include.Any()) + { + matcher.AddIncludePatterns(include); + } + else + { + matcher.AddInclude("**"); + } + + if (exclude.Any()) + matcher.AddExcludePatterns(exclude); + + return new FileSystemFilter(matcher); + } + + public bool IsMatch(ISymbol symbol) + { + bool isMatch = Matcher.IsMatch(symbol); +#if DEBUG + if (!isMatch + && Logger.ShouldWrite(Verbosity.Diagnostic)) + { + lock (_lock) + Logger.WriteLine($"Excluding symbol '{symbol.ToDisplayString(SymbolDisplayFormats.FullName)}'", ConsoleColors.DarkGray, Verbosity.Diagnostic); + } +#endif + return isMatch; + } + + public bool IsMatch(string filePath) + { + PatternMatchingResult result = Matcher.Match(filePath); + +#if DEBUG + Debug.Assert(result.Files.Count() <= 1, result.Files.Count().ToString()); + + if (!result.HasMatches + && Logger.ShouldWrite(Verbosity.Diagnostic)) + { + lock (_lock) + Logger.WriteLine($"Excluding file '{filePath}'", ConsoleColors.DarkGray, Verbosity.Diagnostic); + } +#endif + return result.HasMatches; + } +} diff --git a/src/Workspaces.Core/FindSymbols/SymbolFilterOptions.cs b/src/Workspaces.Core/FindSymbols/SymbolFilterOptions.cs index c5939e1058..34a828e4b5 100644 --- a/src/Workspaces.Core/FindSymbols/SymbolFilterOptions.cs +++ b/src/Workspaces.Core/FindSymbols/SymbolFilterOptions.cs @@ -14,11 +14,13 @@ internal class SymbolFilterOptions public static SymbolFilterOptions Default { get; } = new(); internal SymbolFilterOptions( + FileSystemFilter fileSystemFilter = null, VisibilityFilter visibility = VisibilityFilter.All, SymbolGroupFilter symbolGroups = SymbolGroupFilter.TypeOrMember, IEnumerable rules = null, IEnumerable attributeRules = null) { + FileSystemFilter = fileSystemFilter; Visibility = visibility; SymbolGroups = symbolGroups; @@ -26,6 +28,8 @@ internal SymbolFilterOptions( AttributeRules = attributeRules?.ToImmutableArray() ?? ImmutableArray.Empty; } + public FileSystemFilter FileSystemFilter { get; } + public VisibilityFilter Visibility { get; } public SymbolGroupFilter SymbolGroups { get; } @@ -221,6 +225,9 @@ public virtual SymbolFilterReason GetReason(IMethodSymbol symbol) private SymbolFilterReason GetRulesReason(ISymbol symbol) { + if (FileSystemFilter?.IsMatch(symbol) == false) + return SymbolFilterReason.Other; + foreach (SymbolFilterRule rule in Rules) { if (rule.IsApplicable(symbol) diff --git a/src/Workspaces.Core/FindSymbols/SymbolFinderOptions.cs b/src/Workspaces.Core/FindSymbols/SymbolFinderOptions.cs index c3fe5b95df..8081a302f3 100644 --- a/src/Workspaces.Core/FindSymbols/SymbolFinderOptions.cs +++ b/src/Workspaces.Core/FindSymbols/SymbolFinderOptions.cs @@ -7,12 +7,13 @@ namespace Roslynator.FindSymbols; internal class SymbolFinderOptions : SymbolFilterOptions { internal SymbolFinderOptions( + FileSystemFilter fileSystemFilter = null, VisibilityFilter visibility = VisibilityFilter.All, SymbolGroupFilter symbolGroups = SymbolGroupFilter.TypeOrMember, IEnumerable rules = null, IEnumerable attributeRules = null, bool ignoreGeneratedCode = false, - bool unusedOnly = false) : base(visibility, symbolGroups, rules, attributeRules) + bool unusedOnly = false) : base(fileSystemFilter, visibility, symbolGroups, rules, attributeRules) { IgnoreGeneratedCode = ignoreGeneratedCode; UnusedOnly = unusedOnly; diff --git a/src/Workspaces.Core/Formatting/CodeFormatter.cs b/src/Workspaces.Core/Formatting/CodeFormatter.cs index 9458b617df..dcd2fa1d4f 100644 --- a/src/Workspaces.Core/Formatting/CodeFormatter.cs +++ b/src/Workspaces.Core/Formatting/CodeFormatter.cs @@ -35,19 +35,22 @@ public static async Task FormatProjectAsync( { Document document = project.GetDocument(documentId); - if (options.IncludeGeneratedCode - || !GeneratedCodeUtility.IsGeneratedCodeFile(document.FilePath)) + if (options.FileSystemFilter?.IsMatch(document.FilePath) != false) { - SyntaxNode root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - if (options.IncludeGeneratedCode - || !syntaxFacts.BeginsWithAutoGeneratedComment(root)) + || !GeneratedCodeUtility.IsGeneratedCodeFile(document.FilePath)) { - DocumentOptionSet optionSet = await document.GetOptionsAsync(cancellationToken).ConfigureAwait(false); + SyntaxNode root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + if (options.IncludeGeneratedCode + || !syntaxFacts.BeginsWithAutoGeneratedComment(root)) + { + DocumentOptionSet optionSet = await document.GetOptionsAsync(cancellationToken).ConfigureAwait(false); - Document newDocument = await Formatter.FormatAsync(document, optionSet, cancellationToken).ConfigureAwait(false); + Document newDocument = await Formatter.FormatAsync(document, optionSet, cancellationToken).ConfigureAwait(false); - project = newDocument.Project; + project = newDocument.Project; + } } } } diff --git a/src/Workspaces.Core/Formatting/CodeFormatterOptions.cs b/src/Workspaces.Core/Formatting/CodeFormatterOptions.cs index f4c841d165..18dd632d71 100644 --- a/src/Workspaces.Core/Formatting/CodeFormatterOptions.cs +++ b/src/Workspaces.Core/Formatting/CodeFormatterOptions.cs @@ -7,10 +7,14 @@ internal class CodeFormatterOptions public static CodeFormatterOptions Default { get; } = new(); public CodeFormatterOptions( + FileSystemFilter fileSystemFilter = null, bool includeGeneratedCode = false) { + FileSystemFilter = fileSystemFilter; IncludeGeneratedCode = includeGeneratedCode; } + public FileSystemFilter FileSystemFilter { get; } + public bool IncludeGeneratedCode { get; } } diff --git a/src/Workspaces.Core/Rename/SymbolProvider.cs b/src/Workspaces.Core/Rename/SymbolProvider.cs index fa166b3817..f007bfe400 100644 --- a/src/Workspaces.Core/Rename/SymbolProvider.cs +++ b/src/Workspaces.Core/Rename/SymbolProvider.cs @@ -10,17 +10,17 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.Extensions.FileSystemGlobbing; namespace Roslynator.Rename; +#pragma warning disable RS1001, RS1022 + internal class SymbolProvider { - public SymbolProvider(bool includeGeneratedCode = false) - { - IncludeGeneratedCode = includeGeneratedCode; - } + public bool IncludeGeneratedCode { get; init; } - public bool IncludeGeneratedCode { get; } + public Matcher FileSystemMatcher { get; init; } public async Task> GetSymbolsAsync( Project project, @@ -46,9 +46,13 @@ public async Task> GetSymbolsAsync( _ => throw new InvalidOperationException(), }; - var analyzer = new Analyzer( - symbolKinds, - IncludeGeneratedCode); + var analyzer = new Analyzer() + { + IncludeGeneratedCode = IncludeGeneratedCode, + FileSystemMatcher = FileSystemMatcher, + }; + + analyzer.SymbolKinds.AddRange(symbolKinds); var compilationWithAnalyzers = new CompilationWithAnalyzers( compilation, @@ -60,7 +64,6 @@ public async Task> GetSymbolsAsync( return analyzer.Symbols; } - [SuppressMessage("MicrosoftCodeAnalysisCorrectness", "RS1001:Missing diagnostic analyzer attribute.")] private class Analyzer : DiagnosticAnalyzer { [SuppressMessage("MicrosoftCodeAnalysisReleaseTracking", "RS2008:Enable analyzer release tracking")] @@ -77,19 +80,15 @@ private class Analyzer : DiagnosticAnalyzer private static readonly ImmutableArray _supportedDiagnostics = ImmutableArray.Create(DiagnosticDescriptor); - public Analyzer(ImmutableArray symbolKinds, bool includeGeneratedCode = false) - { - SymbolKinds = symbolKinds; - IncludeGeneratedCode = includeGeneratedCode; - } - public override ImmutableArray SupportedDiagnostics => _supportedDiagnostics; public ConcurrentBag Symbols { get; } = new(); - public ImmutableArray SymbolKinds { get; } + public List SymbolKinds { get; } = new(); + + public bool IncludeGeneratedCode { get; init; } - public bool IncludeGeneratedCode { get; } + public Matcher FileSystemMatcher { get; init; } public override void Initialize(AnalysisContext context) { @@ -99,7 +98,7 @@ public override void Initialize(AnalysisContext context) ? GeneratedCodeAnalysisFlags.Analyze : GeneratedCodeAnalysisFlags.None); - context.RegisterSymbolAction(f => AnalyzeSymbol(f), SymbolKinds); + context.RegisterSymbolAction(f => AnalyzeSymbol(f), SymbolKinds.ToArray()); } private void AnalyzeSymbol(SymbolAnalysisContext context) @@ -116,7 +115,7 @@ private void AnalyzeSymbol(SymbolAnalysisContext context) case SymbolKind.NamedType: case SymbolKind.Property: { - Symbols.Add(symbol); + AddSymbol(symbol); break; } case SymbolKind.Method: @@ -130,7 +129,7 @@ private void AnalyzeSymbol(SymbolAnalysisContext context) case MethodKind.UserDefinedOperator: case MethodKind.Conversion: { - Symbols.Add(methodSymbol); + AddSymbol(methodSymbol); break; } } @@ -144,5 +143,11 @@ private void AnalyzeSymbol(SymbolAnalysisContext context) } } } + + private void AddSymbol(ISymbol symbol) + { + if (FileSystemMatcher?.IsMatch(symbol) != false) + Symbols.Add(symbol); + } } } diff --git a/src/Workspaces.Core/Rename/SymbolRenameState.cs b/src/Workspaces.Core/Rename/SymbolRenameState.cs index df2422a1cb..3d3054ef72 100644 --- a/src/Workspaces.Core/Rename/SymbolRenameState.cs +++ b/src/Workspaces.Core/Rename/SymbolRenameState.cs @@ -131,7 +131,11 @@ protected async Task AnalyzeProjectAsync( while (true) { - var symbolProvider = new SymbolProvider(Options.IncludeGeneratedCode); + var symbolProvider = new SymbolProvider() + { + IncludeGeneratedCode = Options.IncludeGeneratedCode, + FileSystemMatcher = Options.FileSystemMatcher, + }; IEnumerable symbols = await symbolProvider.GetSymbolsAsync(project, scope, cancellationToken).ConfigureAwait(false); diff --git a/src/Workspaces.Core/Rename/SymbolRenamerOptions.cs b/src/Workspaces.Core/Rename/SymbolRenamerOptions.cs index 62074bb942..90fe2d749f 100644 --- a/src/Workspaces.Core/Rename/SymbolRenamerOptions.cs +++ b/src/Workspaces.Core/Rename/SymbolRenamerOptions.cs @@ -1,6 +1,7 @@ // Copyright (c) Josef Pihrt and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; +using Microsoft.Extensions.FileSystemGlobbing; namespace Roslynator.Rename; @@ -62,4 +63,7 @@ public class SymbolRenamerOptions /// If the symbol is a type renames the file containing the type declaration as well. /// public bool RenameFile { get; set; } + + //TODO: SymbolRenameOptions.FileSystemMatcher + internal Matcher FileSystemMatcher { get; set; } } diff --git a/src/Workspaces.Core/Spelling/SpellcheckOptions.cs b/src/Workspaces.Core/Spelling/SpellcheckOptions.cs index 986b57d518..d1641df73a 100644 --- a/src/Workspaces.Core/Spelling/SpellcheckOptions.cs +++ b/src/Workspaces.Core/Spelling/SpellcheckOptions.cs @@ -6,6 +6,8 @@ internal class SpellcheckOptions { public static SpellcheckOptions Default { get; } = new(); + public FileSystemFilter FileSystemFilter { get; init; } + public SpellingScopeFilter ScopeFilter { get; init; } = SpellingScopeFilter.All; public VisibilityFilter SymbolVisibility { get; init; } = VisibilityFilter.All; diff --git a/src/Workspaces.Core/Workspaces.Core.csproj b/src/Workspaces.Core/Workspaces.Core.csproj index e47c2a1fb8..543f224816 100644 --- a/src/Workspaces.Core/Workspaces.Core.csproj +++ b/src/Workspaces.Core/Workspaces.Core.csproj @@ -33,6 +33,7 @@ +