diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f1e3d20 --- /dev/null +++ b/.gitignore @@ -0,0 +1,252 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml diff --git a/README.md b/README.md new file mode 100644 index 0000000..8423812 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# AgodaAnalyzers \ No newline at end of file diff --git a/src/Agoda.Analyzers.CodeFixes/Agoda.Analyzers.CodeFixes.csproj b/src/Agoda.Analyzers.CodeFixes/Agoda.Analyzers.CodeFixes.csproj new file mode 100644 index 0000000..aa94c18 --- /dev/null +++ b/src/Agoda.Analyzers.CodeFixes/Agoda.Analyzers.CodeFixes.csproj @@ -0,0 +1,122 @@ + + + + + 11.0 + Debug + AnyCPU + {A5863784-CD41-4419-9C8F-53D89D509FE9} + Library + Properties + Agoda.Analyzers.CodeFixes + Agoda.Analyzers.CodeFixes + en-US + 512 + {786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Profile7 + v4.5 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + ..\packages\Microsoft.CodeAnalysis.Common.1.3.2\lib\portable-net45+win8\Microsoft.CodeAnalysis.dll + True + + + ..\packages\Microsoft.CodeAnalysis.CSharp.1.3.2\lib\portable-net45+win8\Microsoft.CodeAnalysis.CSharp.dll + True + + + ..\packages\Microsoft.CodeAnalysis.CSharp.Workspaces.1.3.2\lib\portable-net45+win8\Microsoft.CodeAnalysis.CSharp.Workspaces.dll + True + + + ..\packages\Microsoft.CodeAnalysis.Workspaces.Common.1.3.2\lib\portable-net45+win8\Microsoft.CodeAnalysis.Workspaces.dll + True + + + ..\packages\System.Collections.Immutable.1.1.37\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll + True + + + ..\packages\Microsoft.Composition.1.0.27\lib\portable-net45+win8+wp8+wpa81\System.Composition.AttributedModel.dll + True + + + ..\packages\Microsoft.Composition.1.0.27\lib\portable-net45+win8+wp8+wpa81\System.Composition.Convention.dll + True + + + ..\packages\Microsoft.Composition.1.0.27\lib\portable-net45+win8+wp8+wpa81\System.Composition.Hosting.dll + True + + + ..\packages\Microsoft.Composition.1.0.27\lib\portable-net45+win8+wp8+wpa81\System.Composition.Runtime.dll + True + + + ..\packages\Microsoft.Composition.1.0.27\lib\portable-net45+win8+wp8+wpa81\System.Composition.TypedParts.dll + True + + + ..\packages\System.Reflection.Metadata.1.2.0\lib\portable-net45+win8\System.Reflection.Metadata.dll + True + + + + + + + + + {4f934d25-9bff-4153-8965-f12f52ba41df} + Agoda.Analyzers + + + + + + \ No newline at end of file diff --git a/src/Agoda.Analyzers.CodeFixes/Agoda.Analyzers.nuspec b/src/Agoda.Analyzers.CodeFixes/Agoda.Analyzers.nuspec new file mode 100644 index 0000000..af7740b --- /dev/null +++ b/src/Agoda.Analyzers.CodeFixes/Agoda.Analyzers.nuspec @@ -0,0 +1,28 @@ + + + + Agoda.Analyzers + $version$ + Agoda Roslyn Analyzers + Joel Dickson + Agoda Services + false + TBA + Copyright © Agoda 2017 + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Agoda.Analyzers.CodeFixes/Helpers/CustomBatchFixAllProvider.cs b/src/Agoda.Analyzers.CodeFixes/Helpers/CustomBatchFixAllProvider.cs new file mode 100644 index 0000000..c8d1fa1 --- /dev/null +++ b/src/Agoda.Analyzers.CodeFixes/Helpers/CustomBatchFixAllProvider.cs @@ -0,0 +1,429 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Agoda.Analyzers.Helpers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Text; + +namespace Agoda.Analyzers.CodeFixes.Helpers +{ + /// + /// Helper class for "Fix all occurrences" code fix providers. + /// + internal partial class CustomBatchFixAllProvider : FixAllProvider + { + protected CustomBatchFixAllProvider() + { + } + + public static FixAllProvider Instance { get; } = new CustomBatchFixAllProvider(); + + public override async Task GetFixAsync(FixAllContext fixAllContext) + { + if (fixAllContext.Document != null) + { + var documentsAndDiagnosticsToFixMap = await this.GetDocumentDiagnosticsToFixAsync(fixAllContext).ConfigureAwait(false); + return await this.GetFixAsync(documentsAndDiagnosticsToFixMap, fixAllContext).ConfigureAwait(false); + } + else + { + var projectsAndDiagnosticsToFixMap = await this.GetProjectDiagnosticsToFixAsync(fixAllContext).ConfigureAwait(false); + return await this.GetFixAsync(projectsAndDiagnosticsToFixMap, fixAllContext).ConfigureAwait(false); + } + } + + public virtual async Task GetFixAsync( + ImmutableDictionary> documentsAndDiagnosticsToFixMap, + FixAllContext fixAllContext) + { + if (documentsAndDiagnosticsToFixMap != null && documentsAndDiagnosticsToFixMap.Any()) + { + fixAllContext.CancellationToken.ThrowIfCancellationRequested(); + + var documents = documentsAndDiagnosticsToFixMap.Keys.ToImmutableArray(); + var fixesBag = new List[documents.Length]; + var options = new ParallelOptions() { CancellationToken = fixAllContext.CancellationToken }; + Parallel.ForEach(documents, options, (document, state, index) => + { + fixAllContext.CancellationToken.ThrowIfCancellationRequested(); + fixesBag[index] = new List(); + this.AddDocumentFixesAsync(document, documentsAndDiagnosticsToFixMap[document], fixesBag[index].Add, fixAllContext).Wait(fixAllContext.CancellationToken); + }); + + if (fixesBag.Any(fixes => fixes.Count > 0)) + { + return await this.TryGetMergedFixAsync(fixesBag.SelectMany(i => i), fixAllContext).ConfigureAwait(false); + } + } + + return null; + } + + public async virtual Task AddDocumentFixesAsync(Document document, ImmutableArray diagnostics, Action addFix, FixAllContext fixAllContext) + { + Debug.Assert(!diagnostics.IsDefault, "!diagnostics.IsDefault"); + var cancellationToken = fixAllContext.CancellationToken; + var fixerTasks = new Task[diagnostics.Length]; + var fixes = new List[diagnostics.Length]; + + for (var i = 0; i < diagnostics.Length; i++) + { + int currentFixIndex = i; + cancellationToken.ThrowIfCancellationRequested(); + var diagnostic = diagnostics[i]; + fixerTasks[i] = Task.Run(async () => + { + var localFixes = new List(); + var context = new CodeFixContext( + document, + diagnostic, + (a, d) => + { + // TODO: Can we share code between similar lambdas that we pass to this API in BatchFixAllProvider.cs, CodeFixService.cs and CodeRefactoringService.cs? + // Serialize access for thread safety - we don't know what thread the fix provider will call this delegate from. + lock (localFixes) + { + localFixes.Add(a); + } + }, + cancellationToken); + + // TODO: Wrap call to ComputeFixesAsync() below in IExtensionManager.PerformFunctionAsync() so that + // a buggy extension that throws can't bring down the host? + var task = fixAllContext.CodeFixProvider.RegisterCodeFixesAsync(context) ?? SpecializedTasks.CompletedTask; + await task.ConfigureAwait(false); + + cancellationToken.ThrowIfCancellationRequested(); + localFixes.RemoveAll(action => action.EquivalenceKey != fixAllContext.CodeActionEquivalenceKey); + fixes[currentFixIndex] = localFixes; + }); + } + + await Task.WhenAll(fixerTasks).ConfigureAwait(false); + foreach (List fix in fixes) + { + if (fix == null) + { + continue; + } + + foreach (CodeAction action in fix) + { + addFix(action); + } + } + } + + public virtual async Task GetFixAsync( + ImmutableDictionary> projectsAndDiagnosticsToFixMap, + FixAllContext fixAllContext) + { + if (projectsAndDiagnosticsToFixMap != null && projectsAndDiagnosticsToFixMap.Any()) + { + var options = new ParallelOptions() { CancellationToken = fixAllContext.CancellationToken }; + var fixesBag = new List[projectsAndDiagnosticsToFixMap.Count]; + Parallel.ForEach(projectsAndDiagnosticsToFixMap.Keys, options, (project, state, index) => + { + fixAllContext.CancellationToken.ThrowIfCancellationRequested(); + var diagnostics = projectsAndDiagnosticsToFixMap[project]; + fixesBag[index] = new List(); + this.AddProjectFixesAsync(project, diagnostics, fixesBag[index].Add, fixAllContext).Wait(fixAllContext.CancellationToken); + }); + + if (fixesBag.Any(fixes => fixes.Count > 0)) + { + return await this.TryGetMergedFixAsync(fixesBag.SelectMany(i => i), fixAllContext).ConfigureAwait(false); + } + } + + return null; + } + + public virtual Task AddProjectFixesAsync(Project project, IEnumerable diagnostics, Action addFix, FixAllContext fixAllContext) + { + throw new NotImplementedException(); + } + + public virtual async Task TryGetMergedFixAsync(IEnumerable batchOfFixes, FixAllContext fixAllContext) + { + if (batchOfFixes == null) + { + throw new ArgumentNullException(nameof(batchOfFixes)); + } + + if (!batchOfFixes.Any()) + { + throw new ArgumentException($"{nameof(batchOfFixes)} cannot be empty.", nameof(batchOfFixes)); + } + + var solution = fixAllContext.Solution; + var newSolution = await this.TryMergeFixesAsync(solution, batchOfFixes, fixAllContext.CancellationToken).ConfigureAwait(false); + if (newSolution != null && newSolution != solution) + { + var title = this.GetFixAllTitle(fixAllContext); + return CodeAction.Create(title, cancellationToken => Task.FromResult(newSolution)); + } + + return null; + } + + public virtual string GetFixAllTitle(FixAllContext fixAllContext) + { + var diagnosticIds = fixAllContext.DiagnosticIds; + string diagnosticId; + if (diagnosticIds.Count == 1) + { + diagnosticId = diagnosticIds.Single(); + } + else + { + diagnosticId = string.Join(",", diagnosticIds.ToArray()); + } + + switch (fixAllContext.Scope) + { + case FixAllScope.Custom: + return string.Format(HelpersResources.FixAllOccurrencesOfDiagnostic, diagnosticId); + + case FixAllScope.Document: + var document = fixAllContext.Document; + return string.Format(HelpersResources.FixAllOccurrencesOfDiagnosticInScope, diagnosticId, document.Name); + + case FixAllScope.Project: + var project = fixAllContext.Project; + return string.Format(HelpersResources.FixAllOccurrencesOfDiagnosticInScope, diagnosticId, project.Name); + + case FixAllScope.Solution: + return string.Format(HelpersResources.FixAllOccurrencesOfDiagnosticInSolution, diagnosticId); + + default: + throw new InvalidOperationException("Not reachable"); + } + } + + public virtual Task>> GetDocumentDiagnosticsToFixAsync(FixAllContext fixAllContext) + { + return FixAllContextHelper.GetDocumentDiagnosticsToFixAsync(fixAllContext); + } + + public virtual Task>> GetProjectDiagnosticsToFixAsync(FixAllContext fixAllContext) + { + return FixAllContextHelper.GetProjectDiagnosticsToFixAsync(fixAllContext); + } + + public virtual async Task TryMergeFixesAsync(Solution oldSolution, IEnumerable codeActions, CancellationToken cancellationToken) + { + var changedDocumentsMap = new Dictionary(); + Dictionary> documentsToMergeMap = null; + + foreach (var codeAction in codeActions) + { + cancellationToken.ThrowIfCancellationRequested(); + + // TODO: Parallelize GetChangedSolutionInternalAsync for codeActions + ImmutableArray operations = await codeAction.GetPreviewOperationsAsync(cancellationToken).ConfigureAwait(false); + ApplyChangesOperation singleApplyChangesOperation = null; + foreach (var operation in operations) + { + ApplyChangesOperation applyChangesOperation = operation as ApplyChangesOperation; + if (applyChangesOperation == null) + { + continue; + } + + if (singleApplyChangesOperation != null) + { + // Already had an ApplyChangesOperation; only one is supported. + singleApplyChangesOperation = null; + break; + } + + singleApplyChangesOperation = applyChangesOperation; + } + + if (singleApplyChangesOperation == null) + { + continue; + } + + var changedSolution = singleApplyChangesOperation.ChangedSolution; + var solutionChanges = changedSolution.GetChanges(oldSolution); + + // TODO: Handle added/removed documents + // TODO: Handle changed/added/removed additional documents + var documentIdsWithChanges = solutionChanges + .GetProjectChanges() + .SelectMany(p => p.GetChangedDocuments()); + + foreach (var documentId in documentIdsWithChanges) + { + cancellationToken.ThrowIfCancellationRequested(); + var document = changedSolution.GetDocument(documentId); + + Document existingDocument; + if (changedDocumentsMap.TryGetValue(documentId, out existingDocument)) + { + if (existingDocument != null) + { + changedDocumentsMap[documentId] = null; + var documentsToMerge = new List(); + documentsToMerge.Add(existingDocument); + documentsToMerge.Add(document); + documentsToMergeMap = documentsToMergeMap ?? new Dictionary>(); + documentsToMergeMap[documentId] = documentsToMerge; + } + else + { + documentsToMergeMap[documentId].Add(document); + } + } + else + { + changedDocumentsMap[documentId] = document; + } + } + } + + var currentSolution = oldSolution; + foreach (var kvp in changedDocumentsMap) + { + cancellationToken.ThrowIfCancellationRequested(); + var document = kvp.Value; + if (document != null) + { + var documentText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); + currentSolution = currentSolution.WithDocumentText(kvp.Key, documentText); + } + } + + if (documentsToMergeMap != null) + { + var mergedDocuments = new ConcurrentDictionary(); + var documentsToMergeArray = documentsToMergeMap.ToImmutableArray(); + var mergeTasks = new Task[documentsToMergeArray.Length]; + for (int i = 0; i < documentsToMergeArray.Length; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + var kvp = documentsToMergeArray[i]; + var documentId = kvp.Key; + var documentsToMerge = kvp.Value; + var oldDocument = oldSolution.GetDocument(documentId); + + mergeTasks[i] = Task.Run(async () => + { + var appliedChanges = (await documentsToMerge[0].GetTextChangesAsync(oldDocument, cancellationToken).ConfigureAwait(false)).ToList(); + + foreach (var document in documentsToMerge.Skip(1)) + { + cancellationToken.ThrowIfCancellationRequested(); + appliedChanges = await TryAddDocumentMergeChangesAsync( + oldDocument, + document, + appliedChanges, + cancellationToken).ConfigureAwait(false); + } + + var oldText = await oldDocument.GetTextAsync(cancellationToken).ConfigureAwait(false); + var newText = oldText.WithChanges(appliedChanges); + mergedDocuments.TryAdd(documentId, newText); + }); + } + + await Task.WhenAll(mergeTasks).ConfigureAwait(false); + + foreach (var kvp in mergedDocuments) + { + cancellationToken.ThrowIfCancellationRequested(); + currentSolution = currentSolution.WithDocumentText(kvp.Key, kvp.Value); + } + } + + return currentSolution; + } + + /// + /// Try to merge the changes between and into . + /// If there is any conflicting change in with existing , then the original are returned. + /// Otherwise, the newly merged changes are returned. + /// + /// Base document on which FixAll was invoked. + /// New document with a code fix that is being merged. + /// Existing merged changes from other batch fixes into which newDocument changes are being merged. + /// Cancellation token. + /// A representing the asynchronous operation. + private static async Task> TryAddDocumentMergeChangesAsync( + Document oldDocument, + Document newDocument, + List cumulativeChanges, + CancellationToken cancellationToken) + { + var successfullyMergedChanges = new List(); + + int cumulativeChangeIndex = 0; + foreach (var change in await newDocument.GetTextChangesAsync(oldDocument, cancellationToken).ConfigureAwait(false)) + { + cancellationToken.ThrowIfCancellationRequested(); + while (cumulativeChangeIndex < cumulativeChanges.Count && cumulativeChanges[cumulativeChangeIndex].Span.End < change.Span.Start) + { + cancellationToken.ThrowIfCancellationRequested(); + + // Existing change that does not overlap with the current change in consideration + successfullyMergedChanges.Add(cumulativeChanges[cumulativeChangeIndex]); + cumulativeChangeIndex++; + } + + if (cumulativeChangeIndex < cumulativeChanges.Count) + { + var cumulativeChange = cumulativeChanges[cumulativeChangeIndex]; + if (!cumulativeChange.Span.IntersectsWith(change.Span)) + { + // The current change in consideration does not intersect with any existing change + successfullyMergedChanges.Add(change); + } + else + { + if (change.Span != cumulativeChange.Span || change.NewText != cumulativeChange.NewText) + { + // The current change in consideration overlaps an existing change but + // the changes are not identical. + // Bail out merge efforts and return the original 'cumulativeChanges'. + return cumulativeChanges; + } + else + { + // The current change in consideration is identical to an existing change + successfullyMergedChanges.Add(change); + cumulativeChangeIndex++; + } + } + } + else + { + // The current change in consideration does not intersect with any existing change + successfullyMergedChanges.Add(change); + } + } + + while (cumulativeChangeIndex < cumulativeChanges.Count) + { + cancellationToken.ThrowIfCancellationRequested(); + + // Existing change that does not overlap with the current change in consideration + successfullyMergedChanges.Add(cumulativeChanges[cumulativeChangeIndex]); + cumulativeChangeIndex++; + } + + return successfullyMergedChanges; + } + } +} diff --git a/src/Agoda.Analyzers.CodeFixes/Helpers/CustomFixAllProviders.cs b/src/Agoda.Analyzers.CodeFixes/Helpers/CustomFixAllProviders.cs new file mode 100644 index 0000000..4fc5a80 --- /dev/null +++ b/src/Agoda.Analyzers.CodeFixes/Helpers/CustomFixAllProviders.cs @@ -0,0 +1,32 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using Agoda.Analyzers.CodeFixes.Helpers; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; + +namespace Agoda.Analyzers.Helpers +{ + /// + /// Contains custom implementations of . + /// + internal static class CustomFixAllProviders + { + /// + /// Gets the default batch fix all provider. + /// This provider batches all the individual diagnostic fixes across the scope of fix all action, + /// computes fixes in parallel and then merges all the non-conflicting fixes into a single fix all code action. + /// This fixer supports fixes for the following fix all scopes: + /// , and . + /// + /// + /// The batch fix all provider only batches operations (i.e. ) of type + /// present within the individual diagnostic fixes. Other types of + /// operations present within these fixes are ignored. + /// + /// + /// The default batch fix all provider. + /// + public static FixAllProvider BatchFixer => CustomBatchFixAllProvider.Instance; + } +} diff --git a/src/Agoda.Analyzers.CodeFixes/Helpers/DocumentBasedFixAllProvider.cs b/src/Agoda.Analyzers.CodeFixes/Helpers/DocumentBasedFixAllProvider.cs new file mode 100644 index 0000000..d8167dc --- /dev/null +++ b/src/Agoda.Analyzers.CodeFixes/Helpers/DocumentBasedFixAllProvider.cs @@ -0,0 +1,130 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; + +namespace Agoda.Analyzers.Helpers +{ + /// + /// Provides a base class to write a that fixes documents independently. + /// + public abstract class DocumentBasedFixAllProvider : FixAllProvider + { + protected abstract string CodeActionTitle { get; } + + public override Task GetFixAsync(FixAllContext fixAllContext) + { + CodeAction fixAction; + switch (fixAllContext.Scope) + { + case FixAllScope.Document: + fixAction = CodeAction.Create( + this.CodeActionTitle, + cancellationToken => this.GetDocumentFixesAsync(fixAllContext.WithCancellationToken(cancellationToken)), + nameof(DocumentBasedFixAllProvider)); + break; + + case FixAllScope.Project: + fixAction = CodeAction.Create( + this.CodeActionTitle, + cancellationToken => this.GetProjectFixesAsync(fixAllContext.WithCancellationToken(cancellationToken), fixAllContext.Project), + nameof(DocumentBasedFixAllProvider)); + break; + + case FixAllScope.Solution: + fixAction = CodeAction.Create( + this.CodeActionTitle, + cancellationToken => this.GetSolutionFixesAsync(fixAllContext.WithCancellationToken(cancellationToken)), + nameof(DocumentBasedFixAllProvider)); + break; + + case FixAllScope.Custom: + default: + fixAction = null; + break; + } + + return Task.FromResult(fixAction); + } + + /// + /// Fixes all occurrences of a diagnostic in a specific document. + /// + /// The context for the Fix All operation. + /// The document to fix. + /// The diagnostics to fix in the document. + /// + /// The new representing the root of the fixed document. + /// -or- + /// , if no changes were made to the document. + /// + protected abstract Task FixAllInDocumentAsync(FixAllContext fixAllContext, Document document, ImmutableArray diagnostics); + + private async Task GetDocumentFixesAsync(FixAllContext fixAllContext) + { + var documentDiagnosticsToFix = await FixAllContextHelper.GetDocumentDiagnosticsToFixAsync(fixAllContext).ConfigureAwait(false); + ImmutableArray diagnostics; + if (!documentDiagnosticsToFix.TryGetValue(fixAllContext.Document, out diagnostics)) + { + return fixAllContext.Document; + } + + var newRoot = await this.FixAllInDocumentAsync(fixAllContext, fixAllContext.Document, diagnostics).ConfigureAwait(false); + if (newRoot == null) + { + return fixAllContext.Document; + } + + return fixAllContext.Document.WithSyntaxRoot(newRoot); + } + + private async Task GetSolutionFixesAsync(FixAllContext fixAllContext, ImmutableArray documents) + { + var documentDiagnosticsToFix = await FixAllContextHelper.GetDocumentDiagnosticsToFixAsync(fixAllContext).ConfigureAwait(false); + + Solution solution = fixAllContext.Solution; + List> newDocuments = new List>(documents.Length); + foreach (var document in documents) + { + ImmutableArray diagnostics; + if (!documentDiagnosticsToFix.TryGetValue(document, out diagnostics)) + { + newDocuments.Add(document.GetSyntaxRootAsync(fixAllContext.CancellationToken)); + continue; + } + + newDocuments.Add(this.FixAllInDocumentAsync(fixAllContext, document, diagnostics)); + } + + for (int i = 0; i < documents.Length; i++) + { + var newDocumentRoot = await newDocuments[i].ConfigureAwait(false); + if (newDocumentRoot == null) + { + continue; + } + + solution = solution.WithDocumentSyntaxRoot(documents[i].Id, newDocumentRoot); + } + + return solution; + } + + private Task GetProjectFixesAsync(FixAllContext fixAllContext, Project project) + { + return this.GetSolutionFixesAsync(fixAllContext, project.Documents.ToImmutableArray()); + } + + private Task GetSolutionFixesAsync(FixAllContext fixAllContext) + { + ImmutableArray documents = fixAllContext.Solution.Projects.SelectMany(i => i.Documents).ToImmutableArray(); + return this.GetSolutionFixesAsync(fixAllContext, documents); + } + } +} diff --git a/src/Agoda.Analyzers.CodeFixes/Helpers/FixAllContextHelper.cs b/src/Agoda.Analyzers.CodeFixes/Helpers/FixAllContextHelper.cs new file mode 100644 index 0000000..56cfd6d --- /dev/null +++ b/src/Agoda.Analyzers.CodeFixes/Helpers/FixAllContextHelper.cs @@ -0,0 +1,176 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE 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 Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Agoda.Analyzers.Helpers +{ + internal static class FixAllContextHelper + { + public static async Task>> GetDocumentDiagnosticsToFixAsync(FixAllContext fixAllContext) + { + var allDiagnostics = ImmutableArray.Empty; + var projectsToFix = ImmutableArray.Empty; + + var document = fixAllContext.Document; + var project = fixAllContext.Project; + + switch (fixAllContext.Scope) + { + case FixAllScope.Document: + if (document != null) + { + var documentDiagnostics = await fixAllContext.GetDocumentDiagnosticsAsync(document).ConfigureAwait(false); + return ImmutableDictionary>.Empty.SetItem(document, documentDiagnostics); + } + + break; + + case FixAllScope.Project: + projectsToFix = ImmutableArray.Create(project); + allDiagnostics = await GetAllDiagnosticsAsync(fixAllContext, project).ConfigureAwait(false); + break; + + case FixAllScope.Solution: + projectsToFix = project.Solution.Projects + .Where(p => p.Language == project.Language) + .ToImmutableArray(); + + var diagnostics = new ConcurrentDictionary>(); + var tasks = new Task[projectsToFix.Length]; + for (int i = 0; i < projectsToFix.Length; i++) + { + fixAllContext.CancellationToken.ThrowIfCancellationRequested(); + var projectToFix = projectsToFix[i]; + tasks[i] = Task.Run( + async () => + { + var projectDiagnostics = await GetAllDiagnosticsAsync(fixAllContext, projectToFix).ConfigureAwait(false); + diagnostics.TryAdd(projectToFix.Id, projectDiagnostics); + }, fixAllContext.CancellationToken); + } + + await Task.WhenAll(tasks).ConfigureAwait(false); + allDiagnostics = allDiagnostics.AddRange(diagnostics.SelectMany(i => i.Value.Where(x => fixAllContext.DiagnosticIds.Contains(x.Id)))); + break; + } + + if (allDiagnostics.IsEmpty) + { + return ImmutableDictionary>.Empty; + } + + return await GetDocumentDiagnosticsToFixAsync(allDiagnostics, projectsToFix, fixAllContext.CancellationToken).ConfigureAwait(false); + } + + public static async Task>> GetProjectDiagnosticsToFixAsync(FixAllContext fixAllContext) + { + var project = fixAllContext.Project; + if (project != null) + { + switch (fixAllContext.Scope) + { + case FixAllScope.Project: + var diagnostics = await fixAllContext.GetProjectDiagnosticsAsync(project).ConfigureAwait(false); + return ImmutableDictionary>.Empty.SetItem(project, diagnostics); + + case FixAllScope.Solution: + var projectsAndDiagnostics = new ConcurrentDictionary>(); + var options = new ParallelOptions() { CancellationToken = fixAllContext.CancellationToken }; + Parallel.ForEach(project.Solution.Projects, options, proj => + { + fixAllContext.CancellationToken.ThrowIfCancellationRequested(); + var projectDiagnosticsTask = fixAllContext.GetProjectDiagnosticsAsync(proj); + projectDiagnosticsTask.Wait(fixAllContext.CancellationToken); + var projectDiagnostics = projectDiagnosticsTask.Result; + if (projectDiagnostics.Any()) + { + projectsAndDiagnostics.TryAdd(proj, projectDiagnostics); + } + }); + + return projectsAndDiagnostics.ToImmutableDictionary(); + } + } + + return ImmutableDictionary>.Empty; + } + + public static async Task> GetAllDiagnosticsAsync(Compilation compilation, CompilationWithAnalyzers compilationWithAnalyzers, ImmutableArray analyzers, IEnumerable documents, bool includeCompilerDiagnostics, CancellationToken cancellationToken) + { + return await compilationWithAnalyzers.GetAllDiagnosticsAsync().ConfigureAwait(false); + } + + /// + /// Gets all instances within a specific which are relevant to a + /// . + /// + /// The context for the Fix All operation. + /// The project. + /// A representing the asynchronous operation. When the task completes + /// successfully, the will contain the requested diagnostics. + private static async Task> GetAllDiagnosticsAsync(FixAllContext fixAllContext, Project project) + { + return await fixAllContext.GetAllDiagnosticsAsync(project).ConfigureAwait(false); + } + + private static async Task>> GetDocumentDiagnosticsToFixAsync( + ImmutableArray diagnostics, + ImmutableArray projects, + CancellationToken cancellationToken) + { + var treeToDocumentMap = await GetTreeToDocumentMapAsync(projects, cancellationToken).ConfigureAwait(false); + + var builder = ImmutableDictionary.CreateBuilder>(); + foreach (var documentAndDiagnostics in diagnostics.GroupBy(d => GetReportedDocument(d, treeToDocumentMap))) + { + cancellationToken.ThrowIfCancellationRequested(); + var document = documentAndDiagnostics.Key; + var diagnosticsForDocument = documentAndDiagnostics.ToImmutableArray(); + builder.Add(document, diagnosticsForDocument); + } + + return builder.ToImmutable(); + } + + private static async Task> GetTreeToDocumentMapAsync(ImmutableArray projects, CancellationToken cancellationToken) + { + var builder = ImmutableDictionary.CreateBuilder(); + foreach (var project in projects) + { + cancellationToken.ThrowIfCancellationRequested(); + foreach (var document in project.Documents) + { + cancellationToken.ThrowIfCancellationRequested(); + var tree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + builder.Add(tree, document); + } + } + + return builder.ToImmutable(); + } + + private static Document GetReportedDocument(Diagnostic diagnostic, ImmutableDictionary treeToDocumentsMap) + { + var tree = diagnostic.Location.SourceTree; + if (tree != null) + { + Document document; + if (treeToDocumentsMap.TryGetValue(tree, out document)) + { + return document; + } + } + + return null; + } + } +} diff --git a/src/Agoda.Analyzers.CodeFixes/Helpers/TestDiagnosticProvider.cs b/src/Agoda.Analyzers.CodeFixes/Helpers/TestDiagnosticProvider.cs new file mode 100644 index 0000000..1da7120 --- /dev/null +++ b/src/Agoda.Analyzers.CodeFixes/Helpers/TestDiagnosticProvider.cs @@ -0,0 +1,43 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; + +namespace Agoda.Analyzers.CodeFixes.Helpers +{ + public class TestDiagnosticProvider : FixAllContext.DiagnosticProvider + { + private ImmutableArray diagnostics; + + private TestDiagnosticProvider(ImmutableArray diagnostics) + { + this.diagnostics = diagnostics; + } + + public override Task> GetAllDiagnosticsAsync(Project project, CancellationToken cancellationToken) + { + return Task.FromResult>(this.diagnostics); + } + + public override Task> GetDocumentDiagnosticsAsync(Document document, CancellationToken cancellationToken) + { + return Task.FromResult(this.diagnostics.Where(i => i.Location.GetLineSpan().Path == document.Name)); + } + + public override Task> GetProjectDiagnosticsAsync(Project project, CancellationToken cancellationToken) + { + return Task.FromResult(this.diagnostics.Where(i => !i.Location.IsInSource)); + } + + internal static TestDiagnosticProvider Create(ImmutableArray diagnostics) + { + return new TestDiagnosticProvider(diagnostics); + } + } +} diff --git a/src/Agoda.Analyzers.CodeFixes/Properties/AssemblyInfo.cs b/src/Agoda.Analyzers.CodeFixes/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..b6b976c --- /dev/null +++ b/src/Agoda.Analyzers.CodeFixes/Properties/AssemblyInfo.cs @@ -0,0 +1,30 @@ +using System.Resources; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Agoda.Analyzers.CodeFixes")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Agoda.Analyzers.CodeFixes")] +[assembly: AssemblyCopyright("Copyright © 2017")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: NeutralResourcesLanguage("en")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Agoda.Analyzers.CodeFixes/StyleCop/RemoveRegionCodeFixProvider.cs b/src/Agoda.Analyzers.CodeFixes/StyleCop/RemoveRegionCodeFixProvider.cs new file mode 100644 index 0000000..7494d6a --- /dev/null +++ b/src/Agoda.Analyzers.CodeFixes/StyleCop/RemoveRegionCodeFixProvider.cs @@ -0,0 +1,70 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Collections.Immutable; +using System.Composition; +using System.Threading.Tasks; +using Agoda.Analyzers.Helpers; +using Agoda.Analyzers.StyleCop; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Agoda.Analyzers.CodeFixes.StyleCop +{ + /// + /// Implements a code fix for and . + /// + /// + /// To fix a violation of this rule, remove the region. + /// + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(RemoveRegionCodeFixProvider))] + [Shared] + public class RemoveRegionCodeFixProvider : CodeFixProvider + { + /// + public override ImmutableArray FixableDiagnosticIds { get; } = + ImmutableArray.Create(SA1123DoNotPlaceRegionsWithinElements.DiagnosticId); + + /// + public override FixAllProvider GetFixAllProvider() + { + // The batch fixer does not do a very good job if regions are stacked in each other + return new RemoveRegionFixAllProvider(); + } + + /// + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + foreach (var diagnostic in context.Diagnostics) + { + context.RegisterCodeFix( + CodeAction.Create( + ReadabilityResources.RemoveRegionCodeFix, + cancellationToken => GetTransformedDocumentAsync(context.Document, diagnostic), + nameof(RemoveRegionCodeFixProvider)), + diagnostic); + } + + return SpecializedTasks.CompletedTask; + } + + private static async Task GetTransformedDocumentAsync(Document document, Diagnostic diagnostic) + { + var syntaxRoot = await document.GetSyntaxRootAsync().ConfigureAwait(false); + var node = syntaxRoot?.FindNode(diagnostic.Location.SourceSpan, findInsideTrivia: true, getInnermostNodeForTie: true); + if (node != null && node.IsKind(SyntaxKind.RegionDirectiveTrivia)) + { + var regionDirective = node as RegionDirectiveTriviaSyntax; + + var newSyntaxRoot = syntaxRoot.RemoveNodes(regionDirective.GetRelatedDirectives(), SyntaxRemoveOptions.AddElasticMarker); + + return document.WithSyntaxRoot(newSyntaxRoot); + } + + return document.WithSyntaxRoot(syntaxRoot); + } + } +} diff --git a/src/Agoda.Analyzers.CodeFixes/StyleCop/RemoveRegionFixAllProvider.cs b/src/Agoda.Analyzers.CodeFixes/StyleCop/RemoveRegionFixAllProvider.cs new file mode 100644 index 0000000..958ef9a --- /dev/null +++ b/src/Agoda.Analyzers.CodeFixes/StyleCop/RemoveRegionFixAllProvider.cs @@ -0,0 +1,36 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using Agoda.Analyzers.Helpers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Agoda.Analyzers.CodeFixes.StyleCop +{ + internal sealed class RemoveRegionFixAllProvider : DocumentBasedFixAllProvider + { + protected override string CodeActionTitle => "Remove region"; + + protected override async Task FixAllInDocumentAsync(FixAllContext fixAllContext, Document document, ImmutableArray diagnostics) + { + if (diagnostics.IsEmpty) + { + return null; + } + + SyntaxNode root = await document.GetSyntaxRootAsync().ConfigureAwait(false); + + var nodesToRemove = diagnostics.Select(d => root.FindNode(d.Location.SourceSpan, findInsideTrivia: true)) + .Where(node => node != null && !node.IsMissing) + .OfType() + .SelectMany(node => node.GetRelatedDirectives()) + .Where(node => !node.IsMissing); + + return root.RemoveNodes(nodesToRemove, SyntaxRemoveOptions.AddElasticMarker); + } + } +} diff --git a/src/Agoda.Analyzers.CodeFixes/StyleCop/SA1106CodeFixProvider.cs b/src/Agoda.Analyzers.CodeFixes/StyleCop/SA1106CodeFixProvider.cs new file mode 100644 index 0000000..c6b480b --- /dev/null +++ b/src/Agoda.Analyzers.CodeFixes/StyleCop/SA1106CodeFixProvider.cs @@ -0,0 +1,131 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Collections.Immutable; +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Agoda.Analyzers.Helpers; +using Agoda.Analyzers.StyleCop; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace Agoda.Analyzers.CodeFixes.StyleCop +{ + /// + /// This class provides a code fix for . + /// + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(SA1106CodeFixProvider))] + [Shared] + public class SA1106CodeFixProvider : CodeFixProvider + { + /// + public override ImmutableArray FixableDiagnosticIds { get; } + = ImmutableArray.Create(SA1106CodeMustNotContainEmptyStatements.DiagnosticId); + + /// + public override FixAllProvider GetFixAllProvider() + { + return CustomFixAllProviders.BatchFixer; + } + + /// + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + foreach (var diagnostic in context.Diagnostics) + { + context.RegisterCodeFix( + CodeAction.Create( + ReadabilityResources.SA1106CodeFix, + cancellationToken => GetTransformedDocumentAsync(context.Document, diagnostic, cancellationToken), + nameof(SA1106CodeFixProvider)), + diagnostic); + } + + return SpecializedTasks.CompletedTask; + } + + private static async Task GetTransformedDocumentAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken) + { + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var token = root.FindToken(diagnostic.Location.SourceSpan.Start); + + if (!token.Parent.IsKind(SyntaxKind.EmptyStatement)) + { + return await RemoveSemicolonTextAsync(document, token, cancellationToken).ConfigureAwait(false); + } + + return await RemoveEmptyStatementAsync(document, root, (EmptyStatementSyntax)token.Parent, cancellationToken).ConfigureAwait(false); + } + + private static async Task RemoveEmptyStatementAsync(Document document, SyntaxNode root, EmptyStatementSyntax node, CancellationToken cancellationToken) + { + SyntaxNode newRoot; + + switch (node.Parent.Kind()) + { + case SyntaxKind.Block: + case SyntaxKind.SwitchSection: + // empty statements in a block or switch section can be removed + return await RemoveSemicolonTextAsync(document, node.SemicolonToken, cancellationToken).ConfigureAwait(false); + + case SyntaxKind.IfStatement: + case SyntaxKind.ElseClause: + case SyntaxKind.ForStatement: + case SyntaxKind.WhileStatement: + case SyntaxKind.DoStatement: + // these cases are always replaced with an empty block + newRoot = root.ReplaceNode(node, SyntaxFactory.Block().WithTriviaFrom(node)); + return document.WithSyntaxRoot(newRoot); + + case SyntaxKind.LabeledStatement: + // handle this case as a text manipulation for simplicity + return await RemoveSemicolonTextAsync(document, node.SemicolonToken, cancellationToken).ConfigureAwait(false); + + default: + return document; + } + } + + private static async Task RemoveSemicolonTextAsync(Document document, SyntaxToken token, CancellationToken cancellationToken) + { + TextChange textChange; + + SourceText sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); + TextLine line = sourceText.Lines.GetLineFromPosition(token.SpanStart); + if (sourceText.ToString(line.Span).Trim() == token.Text) + { + // remove the line containing the semicolon token + textChange = new TextChange(line.SpanIncludingLineBreak, string.Empty); + return document.WithText(sourceText.WithChanges(textChange)); + } + + TextSpan spanToRemove; + var whitespaceIndex = TriviaHelper.IndexOfTrailingWhitespace(token.LeadingTrivia); + if (whitespaceIndex >= 0) + { + spanToRemove = TextSpan.FromBounds(token.LeadingTrivia[whitespaceIndex].Span.Start, token.Span.End); + } + else + { + var previousToken = token.GetPreviousToken(); + whitespaceIndex = TriviaHelper.IndexOfTrailingWhitespace(previousToken.TrailingTrivia); + if (whitespaceIndex >= 0) + { + spanToRemove = TextSpan.FromBounds(previousToken.TrailingTrivia[whitespaceIndex].Span.Start, token.Span.End); + } + else + { + spanToRemove = token.Span; + } + } + + textChange = new TextChange(spanToRemove, string.Empty); + return document.WithText(sourceText.WithChanges(textChange)); + } + } +} diff --git a/src/Agoda.Analyzers.CodeFixes/StyleCop/SA1107CodeFixProvider.cs b/src/Agoda.Analyzers.CodeFixes/StyleCop/SA1107CodeFixProvider.cs new file mode 100644 index 0000000..1126aa5 --- /dev/null +++ b/src/Agoda.Analyzers.CodeFixes/StyleCop/SA1107CodeFixProvider.cs @@ -0,0 +1,71 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Collections.Immutable; +using System.Composition; +using System.Diagnostics; +using System.Threading.Tasks; +using Agoda.Analyzers.StyleCop; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Agoda.Analyzers.CodeFixes.StyleCop +{ + /// + /// Implements a code fix for . + /// + /// + /// To fix a violation of this rule, add or remove a space after the keyword, according to the description + /// above. + /// + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(SA1107CodeFixProvider))] + [Shared] + public class SA1107CodeFixProvider : CodeFixProvider + { + private static readonly SA1107FixAllProvider FixAllProvider = new SA1107FixAllProvider(); + + /// + public override ImmutableArray FixableDiagnosticIds { get; } = + ImmutableArray.Create(SA1107CodeMustNotContainMultipleStatementsOnOneLine.DiagnosticId); + + /// + public override FixAllProvider GetFixAllProvider() + { + return FixAllProvider; + } + + /// + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + foreach (var diagnostic in context.Diagnostics) + { + var node = root?.FindNode(diagnostic.Location.SourceSpan, findInsideTrivia: true, getInnermostNodeForTie: true); + + if (node?.Parent as BlockSyntax != null) + { + context.RegisterCodeFix( + CodeAction.Create( + ReadabilityResources.SA1107CodeFix, + cancellationToken => GetTransformedDocumentAsync(context.Document, root, node), + nameof(SA1107CodeFixProvider)), + diagnostic); + } + } + } + + private static Task GetTransformedDocumentAsync(Document document, SyntaxNode root, SyntaxNode node) + { + SyntaxNode newSyntaxRoot = root; + Debug.Assert(!node.HasLeadingTrivia, "The trivia should be trailing trivia of the previous node"); + + SyntaxNode newNode = node.WithLeadingTrivia(SyntaxFactory.ElasticCarriageReturnLineFeed); + newSyntaxRoot = newSyntaxRoot.ReplaceNode(node, newNode); + + return Task.FromResult(document.WithSyntaxRoot(newSyntaxRoot)); + } + } +} diff --git a/src/Agoda.Analyzers.CodeFixes/StyleCop/SA1107FixAllProvider.cs b/src/Agoda.Analyzers.CodeFixes/StyleCop/SA1107FixAllProvider.cs new file mode 100644 index 0000000..43d0215 --- /dev/null +++ b/src/Agoda.Analyzers.CodeFixes/StyleCop/SA1107FixAllProvider.cs @@ -0,0 +1,52 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Collections.Immutable; +using System.Threading.Tasks; +using Agoda.Analyzers.Helpers; +using Agoda.Analyzers.StyleCop; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Editing; + +namespace Agoda.Analyzers.CodeFixes.StyleCop +{ + public class SA1107FixAllProvider : DocumentBasedFixAllProvider + { + protected override string CodeActionTitle => ReadabilityResources.SA1107CodeFix; + + protected override async Task FixAllInDocumentAsync(FixAllContext fixAllContext, Document document, ImmutableArray diagnostics) + { + if (diagnostics.IsEmpty) + { + return null; + } + + DocumentEditor editor = await DocumentEditor.CreateAsync(document, fixAllContext.CancellationToken).ConfigureAwait(false); + + SyntaxNode root = editor.GetChangedRoot(); + + ImmutableList nodesToChange = ImmutableList.Create(); + + // Make sure all nodes we care about are tracked + foreach (var diagnostic in diagnostics) + { + var location = diagnostic.Location; + var syntaxNode = root.FindNode(location.SourceSpan); + if (syntaxNode != null) + { + editor.TrackNode(syntaxNode); + nodesToChange = nodesToChange.Add(syntaxNode); + } + } + + foreach (var node in nodesToChange) + { + editor.ReplaceNode(node, node.WithLeadingTrivia(SyntaxFactory.ElasticCarriageReturnLineFeed)); + } + + return editor.GetChangedRoot(); + } + } +} diff --git a/src/Agoda.Analyzers.CodeFixes/app.config b/src/Agoda.Analyzers.CodeFixes/app.config new file mode 100644 index 0000000..9541974 --- /dev/null +++ b/src/Agoda.Analyzers.CodeFixes/app.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/Agoda.Analyzers.CodeFixes/packages.config b/src/Agoda.Analyzers.CodeFixes/packages.config new file mode 100644 index 0000000..b89f8c5 --- /dev/null +++ b/src/Agoda.Analyzers.CodeFixes/packages.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/Agoda.Analyzers.CodeFixes/tools/install.ps1 b/src/Agoda.Analyzers.CodeFixes/tools/install.ps1 new file mode 100644 index 0000000..60f1caf --- /dev/null +++ b/src/Agoda.Analyzers.CodeFixes/tools/install.ps1 @@ -0,0 +1,25 @@ +param($installPath, $toolsPath, $package, $project) +# https://johnkoerner.com/csharp/creating-a-nuget-package-for-your-analyzer/ + +$analyzersPath = join-path $toolsPath "analyzers" + +# Install the language agnostic analyzers. +foreach ($analyzerFilePath in Get-ChildItem $analyzersPath -Filter *.dll) +{ + if($project.Object.AnalyzerReferences) + { + $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName) + } +} + +# Install language specific analyzers. +# $project.Type gives the language name like (C# or VB.NET) +$languageAnalyzersPath = join-path $analyzersPath $project.Type + +foreach ($analyzerFilePath in Get-ChildItem $languageAnalyzersPath -Filter *.dll) +{ + if($project.Object.AnalyzerReferences) + { + $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName) + } +} \ No newline at end of file diff --git a/src/Agoda.Analyzers.CodeFixes/tools/uninstall.ps1 b/src/Agoda.Analyzers.CodeFixes/tools/uninstall.ps1 new file mode 100644 index 0000000..f97ec9f --- /dev/null +++ b/src/Agoda.Analyzers.CodeFixes/tools/uninstall.ps1 @@ -0,0 +1,25 @@ +param($installPath, $toolsPath, $package, $project) +# https://johnkoerner.com/csharp/creating-a-nuget-package-for-your-analyzer/ + +$analyzersPath = join-path $toolsPath "analyzers" + +# Install the language agnostic analyzers. +foreach ($analyzerFilePath in Get-ChildItem $analyzersPath -Filter *.dll) +{ + if($project.Object.AnalyzerReferences) + { + $project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName) + } +} + +# Install language specific analyzers. +# $project.Type gives the language name like (C# or VB.NET) +$languageAnalyzersPath = join-path $analyzersPath $project.Type + +foreach ($analyzerFilePath in Get-ChildItem $languageAnalyzersPath -Filter *.dll) +{ + if($project.Object.AnalyzerReferences) + { + $project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName) + } +} \ No newline at end of file diff --git a/src/Agoda.Analyzers.Test/Agoda.Analyzers.Test.csproj b/src/Agoda.Analyzers.Test/Agoda.Analyzers.Test.csproj new file mode 100644 index 0000000..c118e47 --- /dev/null +++ b/src/Agoda.Analyzers.Test/Agoda.Analyzers.Test.csproj @@ -0,0 +1,188 @@ + + + + Debug + AnyCPU + {756B9DD8-2FE7-485D-8640-6E2755514EAE} + Library + Properties + Agoda.Analyzers.Test + Agoda.Analyzers.Test + v4.5.2 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\Microsoft.CodeAnalysis.Common.1.3.2\lib\net45\Microsoft.CodeAnalysis.dll + True + + + ..\packages\Microsoft.CodeAnalysis.CSharp.1.3.2\lib\net45\Microsoft.CodeAnalysis.CSharp.dll + True + + + ..\packages\Microsoft.CodeAnalysis.CSharp.Workspaces.1.3.2\lib\net45\Microsoft.CodeAnalysis.CSharp.Workspaces.dll + True + + + ..\packages\Microsoft.CodeAnalysis.Workspaces.Common.1.3.2\lib\net45\Microsoft.CodeAnalysis.Workspaces.dll + True + + + ..\packages\Microsoft.CodeAnalysis.Workspaces.Common.1.3.2\lib\net45\Microsoft.CodeAnalysis.Workspaces.Desktop.dll + True + + + ..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll + + + ..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + True + + + ..\packages\NUnit.3.6.0\lib\net45\nunit.framework.dll + True + + + + ..\packages\System.Collections.Immutable.1.1.37\lib\dotnet\System.Collections.Immutable.dll + True + + + ..\packages\Microsoft.Composition.1.0.27\lib\portable-net45+win8+wp8+wpa81\System.Composition.AttributedModel.dll + True + + + ..\packages\Microsoft.Composition.1.0.27\lib\portable-net45+win8+wp8+wpa81\System.Composition.Convention.dll + True + + + ..\packages\Microsoft.Composition.1.0.27\lib\portable-net45+win8+wp8+wpa81\System.Composition.Hosting.dll + True + + + ..\packages\Microsoft.Composition.1.0.27\lib\portable-net45+win8+wp8+wpa81\System.Composition.Runtime.dll + True + + + ..\packages\Microsoft.Composition.1.0.27\lib\portable-net45+win8+wp8+wpa81\System.Composition.TypedParts.dll + True + + + ..\packages\System.Reflection.Metadata.1.2.0\lib\portable-net45+win8\System.Reflection.Metadata.dll + True + + + ..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.Helpers.dll + + + ..\packages\Microsoft.AspNet.Mvc.5.2.3\lib\net45\System.Web.Mvc.dll + + + ..\packages\Microsoft.AspNet.Razor.3.2.3\lib\net45\System.Web.Razor.dll + + + ..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.dll + + + ..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.Deployment.dll + + + ..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.Razor.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {a5863784-cd41-4419-9c8f-53d89d509fe9} + Agoda.Analyzers.CodeFixes + + + {4f934d25-9bff-4153-8965-f12f52ba41df} + Agoda.Analyzers + + + + + + + False + + + False + + + False + + + False + + + + + + + + \ No newline at end of file diff --git a/src/Agoda.Analyzers.Test/AgodaCustom/AG0001UnitTests.cs b/src/Agoda.Analyzers.Test/AgodaCustom/AG0001UnitTests.cs new file mode 100644 index 0000000..17c7f68 --- /dev/null +++ b/src/Agoda.Analyzers.Test/AgodaCustom/AG0001UnitTests.cs @@ -0,0 +1,53 @@ +using Agoda.Analyzers.Test.Helpers; +using NUnit.Framework; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Diagnostics; +using Agoda.Analyzers.AgodaCustom; +using Microsoft.CodeAnalysis; +using System.Collections.Immutable; + + +namespace Agoda.Analyzers.Test.AgodaCustom +{ + class AG0001UnitTests: DiagnosticVerifier + { + [Test] + public async Task TestDependencyResolverUsageAsync() + { + var code = $@" + interface ISomething {{ + void DoSomething(); + }} + + class TestClass {{ + public void TestMethod() {{ + var instance = System.Web.Mvc.DependencyResolver.Current.GetService(typeof(ISomething)); + //instance.DoSomething(); + }} + }} + "; + + var reference = MetadataReference.CreateFromFile(typeof(System.Web.Mvc.DependencyResolver).Assembly.Location); + + var doc = CreateProject(new string[] { code }) + .AddMetadataReference(reference) + .Documents + .First(); + + var analyzersArray = GetCSharpDiagnosticAnalyzers().ToImmutableArray(); + + var diag = await GetSortedDiagnosticsFromDocumentsAsync(analyzersArray, new Document[] { doc }, CancellationToken.None).ConfigureAwait(false); + DiagnosticResult expected = CSharpDiagnostic("AG0001").WithLocation(8, 37); + + VerifyDiagnosticResults(diag, analyzersArray, new DiagnosticResult[] { expected }); + } + + protected override IEnumerable GetCSharpDiagnosticAnalyzers() + { + yield return new AG0001DependencyResolverMustNotBeUsed(); + } + } +} diff --git a/src/Agoda.Analyzers.Test/Helpers/CodeFixVerifier.Helper.cs b/src/Agoda.Analyzers.Test/Helpers/CodeFixVerifier.Helper.cs new file mode 100644 index 0000000..f66086f --- /dev/null +++ b/src/Agoda.Analyzers.Test/Helpers/CodeFixVerifier.Helper.cs @@ -0,0 +1,153 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Simplification; +using Microsoft.CodeAnalysis.Text; + +namespace Agoda.Analyzers.Test.Helpers +{ + /// + /// Diagnostic Producer class with extra methods dealing with applying code fixes. + /// All methods are static + /// + public abstract partial class CodeFixVerifier : DiagnosticVerifier + { + /// + /// Apply the inputted to the inputted document. + /// Meant to be used to apply code fixes. + /// + /// The to apply the fix on + /// A that will be applied to the + /// . + /// The that the task will observe. + /// A with the changes from the . + private static async Task ApplyFixAsync(Project project, CodeAction codeAction, CancellationToken cancellationToken) + { + var operations = await codeAction.GetOperationsAsync(cancellationToken).ConfigureAwait(false); + var solution = operations.OfType().Single().ChangedSolution; + return solution.GetProject(project.Id); + } + + /// + /// Compare two collections of s, and return a list of any new diagnostics that appear + /// only in the second collection. + /// + /// Considers to be the same if they have the same s. + /// In the case of multiple diagnostics with the same in a row, this method may not + /// necessarily return the new one. + /// + /// + /// The s that existed in the code before the code fix was + /// applied. + /// The s that exist in the code after the code fix was + /// applied. + /// A list of s that only surfaced in the code after the code fix was + /// applied. + private static IEnumerable GetNewDiagnostics(IEnumerable diagnostics, IEnumerable newDiagnostics) + { + var oldArray = diagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray(); + var newArray = newDiagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray(); + + int oldIndex = 0; + int newIndex = 0; + + while (newIndex < newArray.Length) + { + if (oldIndex < oldArray.Length && oldArray[oldIndex].Id == newArray[newIndex].Id) + { + ++oldIndex; + ++newIndex; + } + else + { + yield return newArray[newIndex++]; + } + } + } + + /// + /// Get the existing compiler diagnostics on the input document. + /// + /// The to run the compiler diagnostic analyzers on. + /// The that the task will observe. + /// The compiler diagnostics that were found in the code. + private static async Task> GetCompilerDiagnosticsAsync(Project project, CancellationToken cancellationToken) + { + var allDiagnostics = ImmutableArray.Create(); + + foreach (var document in project.Documents) + { + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + allDiagnostics = allDiagnostics.AddRange(semanticModel.GetDiagnostics(cancellationToken: cancellationToken)); + } + + return allDiagnostics; + } + + /// + /// Given a document, turn it into a string based on the syntax root. + /// + /// The to be converted to a string. + /// The that the task will observe. + /// A string containing the syntax of the after formatting. + private static async Task GetStringFromDocumentAsync(Document document, CancellationToken cancellationToken) + { + var simplifiedDoc = await Simplifier.ReduceAsync(document, Simplifier.Annotation, cancellationToken: cancellationToken).ConfigureAwait(false); + var formatted = await Formatter.FormatAsync(simplifiedDoc, Formatter.Annotation, cancellationToken: cancellationToken).ConfigureAwait(false); + var sourceText = await formatted.GetTextAsync(cancellationToken).ConfigureAwait(false); + return sourceText.ToString(); + } + + /// + /// Implements a workaround for issue #936, force re-parsing to get the same sort of syntax tree as the original document. + /// + /// The project to update. + /// The . + /// The updated . + private static async Task RecreateProjectDocumentsAsync(Project project, CancellationToken cancellationToken) + { + foreach (var documentId in project.DocumentIds) + { + var document = project.GetDocument(documentId); + document = await RecreateDocumentAsync(document, cancellationToken).ConfigureAwait(false); + project = document.Project; + } + + return project; + } + + private static async Task RecreateDocumentAsync(Document document, CancellationToken cancellationToken) + { + var newText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); + newText = newText.WithChanges(new TextChange(new TextSpan(0, 0), " ")); + newText = newText.WithChanges(new TextChange(new TextSpan(0, 1), string.Empty)); + return document.WithText(newText); + } + + /// + /// Formats the whitespace in all documents of the specified . + /// + /// The project to update. + /// The . + /// The updated . + private static async Task ReformatProjectDocumentsAsync(Project project, CancellationToken cancellationToken) + { + foreach (var documentId in project.DocumentIds) + { + var document = project.GetDocument(documentId); + document = await Formatter.FormatAsync(document, Formatter.Annotation, cancellationToken: cancellationToken).ConfigureAwait(false); + project = document.Project; + } + + return project; + } + } +} diff --git a/src/Agoda.Analyzers.Test/Helpers/CodeFixVerifier.cs b/src/Agoda.Analyzers.Test/Helpers/CodeFixVerifier.cs new file mode 100644 index 0000000..b7f069a --- /dev/null +++ b/src/Agoda.Analyzers.Test/Helpers/CodeFixVerifier.cs @@ -0,0 +1,442 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Agoda.Analyzers.Test.Helpers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Diagnostics; +using NUnit.Framework; + +namespace Agoda.Analyzers.Test.Helpers +{ + /// + /// Superclass of all unit tests made for diagnostics with code fixes. + /// Contains methods used to verify correctness of code fixes. + /// + public abstract partial class CodeFixVerifier : DiagnosticVerifier + { + private const int DefaultNumberOfIncrementalIterations = -1000; + + /// + /// Returns the code fix being tested (C#) - to be implemented in non-abstract class. + /// + /// The to be used for C# code. + protected abstract CodeFixProvider GetCSharpCodeFixProvider(); + + /// + /// Called to test a C# code fix when applied on the input source as a string. + /// + /// A class in the form of a string before the code fix was applied to it. + /// A class in the form of a string after the code fix was applied to it. + /// A class in the form of a string after the batch fixer was applied to it. + /// The name of the file in the project before the code fix was applied. + /// The name of the file in the project after the code fix was applied. + /// Index determining which code fix to apply if there are multiple. + /// A value indicating whether or not the test will fail if the code fix introduces other warnings after being applied. + /// The number of iterations the incremental fixer will be called. + /// If this value is less than 0, the negated value is treated as an upper limit as opposed to an exact + /// value. + /// The number of iterations the Fix All fixer will be called. If this + /// value is less than 0, the negated value is treated as an upper limit as opposed to an exact value. + /// The that the task will observe. + /// A representing the asynchronous operation. + protected Task VerifyCSharpFixAsync(string oldSource, string newSource, string batchNewSource = null, string oldFileName = null, string newFileName = null, int? codeFixIndex = null, bool allowNewCompilerDiagnostics = false, int numberOfIncrementalIterations = DefaultNumberOfIncrementalIterations, int numberOfFixAllIterations = 1, CancellationToken cancellationToken = default(CancellationToken)) + { + var batchNewSources = batchNewSource == null ? null : new[] { batchNewSource }; + var oldFileNames = oldFileName == null ? null : new[] { oldFileName }; + var newFileNames = newFileName == null ? null : new[] { newFileName }; + return this.VerifyCSharpFixAsync(new[] { oldSource }, new[] { newSource }, batchNewSources, oldFileNames, newFileNames, codeFixIndex, allowNewCompilerDiagnostics, numberOfIncrementalIterations, numberOfFixAllIterations, cancellationToken); + } + + /// + /// Called to test a C# code fix when applied on the input source as a string. + /// + /// An array of sources in the form of strings before the code fix was applied to them. + /// An array of sources in the form of strings after the code fix was applied to them. + /// An array of sources in the form of a strings after the batch fixer was applied to them. + /// An array of file names in the project before the code fix was applied. + /// An array of file names in the project after the code fix was applied. + /// Index determining which code fix to apply if there are multiple. + /// A value indicating whether or not the test will fail if the code fix introduces other warnings after being applied. + /// The number of iterations the incremental fixer will be called. + /// If this value is less than 0, the negated value is treated as an upper limit as opposed to an exact + /// value. + /// The number of iterations the Fix All fixer will be called. If this + /// value is less than 0, the negated value is treated as an upper limit as opposed to an exact value. + /// The that the task will observe. + /// A representing the asynchronous operation. + protected async Task VerifyCSharpFixAsync(string[] oldSources, string[] newSources, string[] batchNewSources = null, string[] oldFileNames = null, string[] newFileNames = null, int? codeFixIndex = null, bool allowNewCompilerDiagnostics = false, int numberOfIncrementalIterations = DefaultNumberOfIncrementalIterations, int numberOfFixAllIterations = 1, CancellationToken cancellationToken = default(CancellationToken)) + { + var t1 = this.VerifyFixInternalAsync(LanguageNames.CSharp, this.GetCSharpDiagnosticAnalyzers().ToImmutableArray(), this.GetCSharpCodeFixProvider(), oldSources, newSources, oldFileNames, newFileNames, codeFixIndex, allowNewCompilerDiagnostics, numberOfIncrementalIterations, FixEachAnalyzerDiagnosticAsync, cancellationToken).ConfigureAwait(false); + + var fixAllProvider = this.GetCSharpCodeFixProvider().GetFixAllProvider(); + Assert.AreNotEqual(WellKnownFixAllProviders.BatchFixer, fixAllProvider); + + if (fixAllProvider == null) + { + await t1; + } + else + { + if (Debugger.IsAttached) + { + await t1; + } + + var t2 = this.VerifyFixInternalAsync(LanguageNames.CSharp, this.GetCSharpDiagnosticAnalyzers().ToImmutableArray(), this.GetCSharpCodeFixProvider(), oldSources, batchNewSources ?? newSources, oldFileNames, newFileNames, codeFixIndex, allowNewCompilerDiagnostics, numberOfFixAllIterations, FixAllAnalyzerDiagnosticsInDocumentAsync, cancellationToken).ConfigureAwait(false); + if (Debugger.IsAttached) + { + await t2; + } + + var t3 = this.VerifyFixInternalAsync(LanguageNames.CSharp, this.GetCSharpDiagnosticAnalyzers().ToImmutableArray(), this.GetCSharpCodeFixProvider(), oldSources, batchNewSources ?? newSources, oldFileNames, newFileNames, codeFixIndex, allowNewCompilerDiagnostics, numberOfFixAllIterations, FixAllAnalyzerDiagnosticsInProjectAsync, cancellationToken).ConfigureAwait(false); + if (Debugger.IsAttached) + { + await t3; + } + + var t4 = this.VerifyFixInternalAsync(LanguageNames.CSharp, this.GetCSharpDiagnosticAnalyzers().ToImmutableArray(), this.GetCSharpCodeFixProvider(), oldSources, batchNewSources ?? newSources, oldFileNames, newFileNames, codeFixIndex, allowNewCompilerDiagnostics, numberOfFixAllIterations, FixAllAnalyzerDiagnosticsInSolutionAsync, cancellationToken).ConfigureAwait(false); + if (Debugger.IsAttached) + { + await t4; + } + + if (!Debugger.IsAttached) + { + // Allow the operations to run in parallel + await t1; + await t2; + await t3; + await t4; + } + } + } + + /// + /// Called to test a C# fix all provider when applied on the input source as a string. + /// + /// A class in the form of a string before the code fix was applied to it. + /// A class in the form of a string after the code fix was applied to it. + /// Index determining which code fix to apply if there are multiple. + /// A value indicating whether or not the test will fail if the code fix introduces other warnings after being applied. + /// The number of iterations the fixer will be called. If this value is less + /// than 0, the negated value is treated as an upper limit as opposed to an exact value. + /// The that the task will observe. + /// A representing the asynchronous operation. + protected async Task VerifyCSharpFixAllFixAsync(string oldSource, string newSource, int? codeFixIndex = null, bool allowNewCompilerDiagnostics = false, int numberOfIterations = 1, CancellationToken cancellationToken = default(CancellationToken)) + { + await this.VerifyFixInternalAsync(LanguageNames.CSharp, this.GetCSharpDiagnosticAnalyzers().ToImmutableArray(), this.GetCSharpCodeFixProvider(), new[] { oldSource }, new[] { newSource }, null, null, codeFixIndex, allowNewCompilerDiagnostics, numberOfIterations, FixAllAnalyzerDiagnosticsInDocumentAsync, cancellationToken).ConfigureAwait(false); + } + + /// + /// Gets all offered code fixes for the specified diagnostic within the given source. + /// + /// A valid C# source file in the form of a string. + /// Index determining which diagnostic to use for determining the offered code fixes. Uses the first diagnostic if null. + /// The that the task will observe. + /// The collection of offered code actions. This collection may be empty. + protected async Task> GetOfferedCSharpFixesAsync(string source, int? diagnosticIndex = null, CancellationToken cancellationToken = default(CancellationToken)) + { + return await this.GetOfferedFixesInternalAsync(LanguageNames.CSharp, source, diagnosticIndex, this.GetCSharpDiagnosticAnalyzers().ToImmutableArray(), this.GetCSharpCodeFixProvider(), cancellationToken).ConfigureAwait(false); + } + + private static async Task FixEachAnalyzerDiagnosticAsync(ImmutableArray analyzers, CodeFixProvider codeFixProvider, int? codeFixIndex, Project project, int numberOfIterations, CancellationToken cancellationToken) + { + int expectedNumberOfIterations = numberOfIterations; + if (numberOfIterations < 0) + { + numberOfIterations = -numberOfIterations; + } + + var previousDiagnostics = ImmutableArray.Create(); + + bool done; + do + { + var analyzerDiagnostics = await GetSortedDiagnosticsFromDocumentsAsync(analyzers, project.Documents.ToArray(), cancellationToken).ConfigureAwait(false); + if (analyzerDiagnostics.Length == 0) + { + break; + } + + if (!AreDiagnosticsDifferent(analyzerDiagnostics, previousDiagnostics)) + { + break; + } + + if (--numberOfIterations < 0) + { + Assert.True(false, "The upper limit for the number of code fix iterations was exceeded"); + } + + previousDiagnostics = analyzerDiagnostics; + + done = true; + foreach (var diagnostic in analyzerDiagnostics) + { + if (!codeFixProvider.FixableDiagnosticIds.Contains(diagnostic.Id)) + { + // do not pass unsupported diagnostics to a code fix provider + continue; + } + + var actions = new List(); + var context = new CodeFixContext(project.GetDocument(diagnostic.Location.SourceTree), diagnostic, (a, d) => actions.Add(a), cancellationToken); + await codeFixProvider.RegisterCodeFixesAsync(context).ConfigureAwait(false); + + if (actions.Count > 0) + { + var fixedProject = await ApplyFixAsync(project, actions.ElementAt(codeFixIndex.GetValueOrDefault(0)), cancellationToken).ConfigureAwait(false); + if (fixedProject != project) + { + done = false; + + project = await RecreateProjectDocumentsAsync(fixedProject, cancellationToken).ConfigureAwait(false); + break; + } + } + } + } + while (!done); + + if (expectedNumberOfIterations >= 0) + { + Assert.AreEqual($"{expectedNumberOfIterations} iterations", $"{expectedNumberOfIterations - numberOfIterations} iterations"); + } + + return project; + } + + private static Task FixAllAnalyzerDiagnosticsInDocumentAsync(ImmutableArray analyzers, CodeFixProvider codeFixProvider, int? codeFixIndex, Project project, int numberOfIterations, CancellationToken cancellationToken) + { + return FixAllAnalyerDiagnosticsInScopeAsync(FixAllScope.Document, analyzers, codeFixProvider, codeFixIndex, project, numberOfIterations, cancellationToken); + } + + private static Task FixAllAnalyzerDiagnosticsInProjectAsync(ImmutableArray analyzers, CodeFixProvider codeFixProvider, int? codeFixIndex, Project project, int numberOfIterations, CancellationToken cancellationToken) + { + return FixAllAnalyerDiagnosticsInScopeAsync(FixAllScope.Project, analyzers, codeFixProvider, codeFixIndex, project, numberOfIterations, cancellationToken); + } + + private static Task FixAllAnalyzerDiagnosticsInSolutionAsync(ImmutableArray analyzers, CodeFixProvider codeFixProvider, int? codeFixIndex, Project project, int numberOfIterations, CancellationToken cancellationToken) + { + return FixAllAnalyerDiagnosticsInScopeAsync(FixAllScope.Solution, analyzers, codeFixProvider, codeFixIndex, project, numberOfIterations, cancellationToken); + } + + private static async Task FixAllAnalyerDiagnosticsInScopeAsync(FixAllScope scope, ImmutableArray analyzers, CodeFixProvider codeFixProvider, int? codeFixIndex, Project project, int numberOfIterations, CancellationToken cancellationToken) + { + int expectedNumberOfIterations = numberOfIterations; + if (numberOfIterations < 0) + { + numberOfIterations = -numberOfIterations; + } + + var previousDiagnostics = ImmutableArray.Create(); + + var fixAllProvider = codeFixProvider.GetFixAllProvider(); + + if (fixAllProvider == null) + { + return null; + } + + bool done; + do + { + var analyzerDiagnostics = await GetSortedDiagnosticsFromDocumentsAsync(analyzers, project.Documents.ToArray(), cancellationToken).ConfigureAwait(false); + if (analyzerDiagnostics.Length == 0) + { + break; + } + + if (!AreDiagnosticsDifferent(analyzerDiagnostics, previousDiagnostics)) + { + break; + } + + if (--numberOfIterations < 0) + { + Assert.True(false, "The upper limit for the number of fix all iterations was exceeded"); + } + + Diagnostic firstDiagnostic = null; + string equivalenceKey = null; + foreach (var diagnostic in analyzerDiagnostics) + { + if (!codeFixProvider.FixableDiagnosticIds.Contains(diagnostic.Id)) + { + // do not pass unsupported diagnostics to a code fix provider + continue; + } + + var actions = new List(); + var context = new CodeFixContext(project.GetDocument(diagnostic.Location.SourceTree), diagnostic, (a, d) => actions.Add(a), cancellationToken); + await codeFixProvider.RegisterCodeFixesAsync(context).ConfigureAwait(false); + if (actions.Count > (codeFixIndex ?? 0)) + { + firstDiagnostic = diagnostic; + equivalenceKey = actions[codeFixIndex ?? 0].EquivalenceKey; + break; + } + } + + if (firstDiagnostic == null) + { + return project; + } + + previousDiagnostics = analyzerDiagnostics; + + done = true; + + FixAllContext.DiagnosticProvider fixAllDiagnosticProvider = TestDiagnosticProvider.Create(analyzerDiagnostics); + + IEnumerable analyzerDiagnosticIds = analyzers.SelectMany(x => x.SupportedDiagnostics).Select(x => x.Id); + IEnumerable compilerDiagnosticIds = codeFixProvider.FixableDiagnosticIds.Where(x => x.StartsWith("CS", StringComparison.Ordinal)); + IEnumerable disabledDiagnosticIds = project.CompilationOptions.SpecificDiagnosticOptions.Where(x => x.Value == ReportDiagnostic.Suppress).Select(x => x.Key); + IEnumerable relevantIds = analyzerDiagnosticIds.Concat(compilerDiagnosticIds).Except(disabledDiagnosticIds).Distinct(); + FixAllContext fixAllContext = new FixAllContext(project.GetDocument(firstDiagnostic.Location.SourceTree), codeFixProvider, scope, equivalenceKey, relevantIds, fixAllDiagnosticProvider, cancellationToken); + + CodeAction action = await fixAllProvider.GetFixAsync(fixAllContext).ConfigureAwait(false); + if (action == null) + { + return project; + } + + var fixedProject = await ApplyFixAsync(project, action, cancellationToken).ConfigureAwait(false); + if (fixedProject != project) + { + done = false; + + project = await RecreateProjectDocumentsAsync(fixedProject, cancellationToken).ConfigureAwait(false); + } + } + while (!done); + + if (expectedNumberOfIterations >= 0) + { + Assert.AreEqual($"{expectedNumberOfIterations} iterations", $"{expectedNumberOfIterations - numberOfIterations} iterations"); + } + + return project; + } + + private static bool AreDiagnosticsDifferent(ImmutableArray analyzerDiagnostics, ImmutableArray previousDiagnostics) + { + if (analyzerDiagnostics.Length != previousDiagnostics.Length) + { + return true; + } + + for (var i = 0; i < analyzerDiagnostics.Length; i++) + { + if ((analyzerDiagnostics[i].Id != previousDiagnostics[i].Id) + || (analyzerDiagnostics[i].Location.SourceSpan != previousDiagnostics[i].Location.SourceSpan)) + { + return true; + } + } + + return false; + } + + private async Task VerifyFixInternalAsync( + string language, + ImmutableArray analyzers, + CodeFixProvider codeFixProvider, + string[] oldSources, + string[] newSources, + string[] oldFileNames, + string[] newFileNames, + int? codeFixIndex, + bool allowNewCompilerDiagnostics, + int numberOfIterations, + Func, CodeFixProvider, int?, Project, int, CancellationToken, Task> getFixedProject, + CancellationToken cancellationToken) + { + if (oldFileNames != null) + { + // Make sure the test case is consistent regarding the number of sources and file names before the code fix + Assert.AreEqual($"{oldSources.Length} old file names", $"{oldFileNames.Length} old file names"); + } + + if (newFileNames != null) + { + // Make sure the test case is consistent regarding the number of sources and file names after the code fix + Assert.AreEqual($"{newSources.Length} new file names", $"{newFileNames.Length} new file names"); + } + + var project = this.CreateProject(oldSources, language, oldFileNames); + var compilerDiagnostics = await GetCompilerDiagnosticsAsync(project, cancellationToken).ConfigureAwait(false); + + project = await getFixedProject(analyzers, codeFixProvider, codeFixIndex, project, numberOfIterations, cancellationToken).ConfigureAwait(false); + + var newCompilerDiagnostics = GetNewDiagnostics(compilerDiagnostics, await GetCompilerDiagnosticsAsync(project, cancellationToken).ConfigureAwait(false)); + + // Check if applying the code fix introduced any new compiler diagnostics + if (!allowNewCompilerDiagnostics && newCompilerDiagnostics.Any()) + { + // Format and get the compiler diagnostics again so that the locations make sense in the output + project = await ReformatProjectDocumentsAsync(project, cancellationToken).ConfigureAwait(false); + newCompilerDiagnostics = GetNewDiagnostics(compilerDiagnostics, await GetCompilerDiagnosticsAsync(project, cancellationToken).ConfigureAwait(false)); + + var message = new StringBuilder(); + message.Append("Fix introduced new compiler diagnostics:\r\n"); + newCompilerDiagnostics.Aggregate(message, (sb, d) => sb.Append(d.ToString()).Append("\r\n")); + foreach (var document in project.Documents) + { + message.Append("\r\n").Append(document.Name).Append(":\r\n"); + message.Append((await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false)).ToFullString()); + message.Append("\r\n"); + } + + Assert.True(false, message.ToString()); + } + + // After applying all of the code fixes, compare the resulting string to the inputted one + var updatedDocuments = project.Documents.ToArray(); + + Assert.AreEqual($"{newSources.Length} documents", $"{updatedDocuments.Length} documents"); + + for (int i = 0; i < updatedDocuments.Length; i++) + { + var actual = await GetStringFromDocumentAsync(updatedDocuments[i], cancellationToken).ConfigureAwait(false); + Assert.AreEqual(newSources[i], actual); + + if (newFileNames != null) + { + Assert.AreEqual(newFileNames[i], updatedDocuments[i].Name); + } + } + } + + private async Task> GetOfferedFixesInternalAsync(string language, string source, int? diagnosticIndex, ImmutableArray analyzers, CodeFixProvider codeFixProvider, CancellationToken cancellationToken) + { + var document = this.CreateDocument(source, language); + var analyzerDiagnostics = await GetSortedDiagnosticsFromDocumentsAsync(analyzers, new[] { document }, cancellationToken).ConfigureAwait(false); + + var index = diagnosticIndex.HasValue ? diagnosticIndex.Value : 0; + + Assert.True(index < analyzerDiagnostics.Count()); + + var actions = new List(); + + // do not pass unsupported diagnostics to a code fix provider + if (codeFixProvider.FixableDiagnosticIds.Contains(analyzerDiagnostics[index].Id)) + { + var context = new CodeFixContext(document, analyzerDiagnostics[index], (a, d) => actions.Add(a), cancellationToken); + await codeFixProvider.RegisterCodeFixesAsync(context).ConfigureAwait(false); + } + + return actions.ToImmutableArray(); + } + } +} diff --git a/src/Agoda.Analyzers.Test/Helpers/DiagnosticResult.cs b/src/Agoda.Analyzers.Test/Helpers/DiagnosticResult.cs new file mode 100644 index 0000000..0ca2eac --- /dev/null +++ b/src/Agoda.Analyzers.Test/Helpers/DiagnosticResult.cs @@ -0,0 +1,184 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace Agoda.Analyzers.Test.Helpers +{ + /// + /// Structure that stores information about a appearing in a source. + /// + public struct DiagnosticResult + { + private const string DefaultPath = "Test0.cs"; + + private static readonly object[] EmptyArguments = new object[0]; + + private FileLinePositionSpan[] spans; + private string message; + + public DiagnosticResult(DiagnosticDescriptor descriptor) + : this() + { + this.Id = descriptor.Id; + this.Severity = descriptor.DefaultSeverity; + this.MessageFormat = descriptor.MessageFormat; + } + + public FileLinePositionSpan[] Spans + { + get + { + return this.spans ?? (this.spans = new FileLinePositionSpan[] { }); + } + + set + { + this.spans = value; + } + } + + public DiagnosticSeverity Severity + { + get; set; + } + + public string Id + { + get; set; + } + + public string Message + { + get + { + if (this.message != null) + { + return this.message; + } + + if (this.MessageFormat != null) + { + return string.Format(this.MessageFormat.ToString(), this.MessageArguments ?? EmptyArguments); + } + + return null; + } + + set + { + this.message = value; + } + } + + public LocalizableString MessageFormat + { + get; + set; + } + + public object[] MessageArguments + { + get; + set; + } + + public bool HasLocation + { + get + { + return (this.spans != null) && (this.spans.Length > 0); + } + } + + public DiagnosticResult WithArguments(params object[] arguments) + { + DiagnosticResult result = this; + result.MessageArguments = arguments; + return result; + } + + public DiagnosticResult WithMessage(string message) + { + DiagnosticResult result = this; + result.Message = message; + return result; + } + + public DiagnosticResult WithMessageFormat(LocalizableString messageFormat) + { + DiagnosticResult result = this; + result.MessageFormat = messageFormat; + return result; + } + + public DiagnosticResult WithLocation(int line, int column) + { + return this.WithLocation(DefaultPath, line, column); + } + + public DiagnosticResult WithLocation(string path, int line, int column) + { + var linePosition = new LinePosition(line, column); + + return this.AppendSpan(new FileLinePositionSpan(path, linePosition, linePosition)); + } + + public DiagnosticResult WithSpan(int startLine, int startColumn, int endLine, int endColumn) + { + return this.WithSpan(DefaultPath, startLine, startColumn, endLine, endColumn); + } + + public DiagnosticResult WithSpan(string path, int startLine, int startColumn, int endLine, int endColumn) + { + return this.AppendSpan(new FileLinePositionSpan(path, new LinePosition(startLine, startColumn), new LinePosition(endLine, endColumn))); + } + + public DiagnosticResult WithLineOffset(int offset) + { + DiagnosticResult result = this; + Array.Resize(ref result.spans, result.spans?.Length ?? 0); + for (int i = 0; i < result.spans.Length; i++) + { + var newStartLinePosition = new LinePosition(result.spans[i].StartLinePosition.Line + offset, result.spans[i].StartLinePosition.Character); + var newEndLinePosition = new LinePosition(result.spans[i].EndLinePosition.Line + offset, result.spans[i].EndLinePosition.Character); + + result.spans[i] = new FileLinePositionSpan(result.spans[i].Path, newStartLinePosition, newEndLinePosition); + } + + return result; + } + + private DiagnosticResult AppendSpan(FileLinePositionSpan span) + { + FileLinePositionSpan[] newSpans; + + if (this.spans != null) + { + newSpans = new FileLinePositionSpan[this.spans.Length + 1]; + Array.Copy(this.spans, newSpans, this.spans.Length); + newSpans[this.spans.Length] = span; + } + else + { + newSpans = new FileLinePositionSpan[1] + { + span, + }; + } + + // clone the object, so that the fluent syntax will work on immutable objects. + return new DiagnosticResult + { + Id = this.Id, + Message = this.message, + MessageFormat = this.MessageFormat, + MessageArguments = this.MessageArguments, + Severity = this.Severity, + Spans = newSpans, + }; + } + } +} diff --git a/src/Agoda.Analyzers.Test/Helpers/DiagnosticVerifier.Helper.cs b/src/Agoda.Analyzers.Test/Helpers/DiagnosticVerifier.Helper.cs new file mode 100644 index 0000000..397894f --- /dev/null +++ b/src/Agoda.Analyzers.Test/Helpers/DiagnosticVerifier.Helper.cs @@ -0,0 +1,366 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Agoda.Analyzers.StyleCop.Settings; +using Agoda.Analyzers.StyleCop.Settings.ObjectModel; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Text; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Agoda.Analyzers.Test.Helpers +{ + /// + /// Class for turning strings into documents and getting the diagnostics on them. + /// All methods are static. + /// + public abstract partial class DiagnosticVerifier + { + private static readonly string DefaultFilePathPrefix = "Test"; + private static readonly string CSharpDefaultFileExt = "cs"; + private static readonly string VisualBasicDefaultExt = "vb"; + private static readonly string CSharpDefaultFilePath = DefaultFilePathPrefix + 0 + "." + CSharpDefaultFileExt; + private static readonly string VisualBasicDefaultFilePath = DefaultFilePathPrefix + 0 + "." + VisualBasicDefaultExt; + private static readonly string TestProjectName = "TestProject"; + + /// + /// Given an analyzer and a collection of documents to apply it to, run the analyzer and gather an array of + /// diagnostics found. The returned diagnostics are then ordered by location in the source documents. + /// + /// The analyzer to run on the documents. + /// The s that the analyzer will be run on. + /// The that the task will observe. + /// A collection of s that surfaced in the source code, sorted by + /// . + protected static async Task> GetSortedDiagnosticsFromDocumentsAsync(ImmutableArray analyzers, Document[] documents, CancellationToken cancellationToken) + { + var projects = new HashSet(); + foreach (var document in documents) + { + projects.Add(document.Project); + } + + var diagnostics = ImmutableArray.CreateBuilder(); + foreach (var project in projects) + { + var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); + var compilationWithAnalyzers = compilation.WithAnalyzers(analyzers, project.AnalyzerOptions, cancellationToken); + var compilerDiagnostics = compilation.GetDiagnostics(cancellationToken); + var compilerErrors = compilerDiagnostics.Where(i => i.Severity == DiagnosticSeverity.Error); + var diags = await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync().ConfigureAwait(false); + var allDiagnostics = await compilationWithAnalyzers.GetAllDiagnosticsAsync().ConfigureAwait(false); + var failureDiagnostics = allDiagnostics.Where(diagnostic => diagnostic.Id == "AD0001"); + foreach (var diag in diags.Concat(compilerErrors).Concat(failureDiagnostics)) + { + if (diag.Location == Location.None || diag.Location.IsInMetadata) + { + diagnostics.Add(diag); + } + else + { + for (int i = 0; i < documents.Length; i++) + { + var document = documents[i]; + var tree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + if (tree == diag.Location.SourceTree) + { + diagnostics.Add(diag); + } + } + } + } + } + + var results = SortDistinctDiagnostics(diagnostics); + return results.ToImmutableArray(); + } + + /// + /// Create a from a string through creating a project that contains it. + /// + /// Classes in the form of a string. + /// The language the source classes are in. Values may be taken from the + /// class. + /// The file name for the document, or to generate a default + /// filename according to the specified . + /// A created from the source string. + protected Document CreateDocument(string source, string language = LanguageNames.CSharp, string fileName = null) + { + string[] filenames = null; + if (fileName != null) + { + filenames = new[] { fileName }; + } + + return this.CreateProject(new[] { source }, language, filenames).Documents.Single(); + } + + /// + /// Creates a solution that will be used as parent for the sources that need to be checked. + /// + /// The project identifier to use. + /// The language for which the solution is being created. + /// The created solution. + protected virtual Solution CreateSolution(ProjectId projectId, string language) + { + var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, allowUnsafe: true); + + Solution solution = new AdhocWorkspace() + .CurrentSolution + .AddProject(projectId, TestProjectName, TestProjectName, language) + .WithProjectCompilationOptions(projectId, compilationOptions) + .AddMetadataReference(projectId, MetadataReferences.CorlibReference) + .AddMetadataReference(projectId, MetadataReferences.SystemReference) + .AddMetadataReference(projectId, MetadataReferences.SystemCoreReference) + .AddMetadataReference(projectId, MetadataReferences.CSharpSymbolsReference) + .AddMetadataReference(projectId, MetadataReferences.CodeAnalysisReference); + + solution.Workspace.Options = + solution.Workspace.Options + .WithChangedOption(FormattingOptions.IndentationSize, language, this.IndentationSize) + .WithChangedOption(FormattingOptions.TabSize, language, this.TabSize) + .WithChangedOption(FormattingOptions.UseTabs, language, this.UseTabs); + + var settings = this.GetSettings(); + + StyleCopSettings defaultSettings = new StyleCopSettings(); + if (this.IndentationSize != defaultSettings.Indentation.IndentationSize + || this.UseTabs != defaultSettings.Indentation.UseTabs + || this.TabSize != defaultSettings.Indentation.TabSize) + { + var indentationSettings = $@" +{{ + ""settings"": {{ + ""indentation"": {{ + ""indentationSize"": {this.IndentationSize}, + ""useTabs"": {this.UseTabs.ToString().ToLowerInvariant()}, + ""tabSize"": {this.TabSize} + }} + }} +}} +"; + + if (string.IsNullOrEmpty(settings)) + { + settings = indentationSettings; + } + else + { + JObject mergedSettings = JsonConvert.DeserializeObject(settings); + mergedSettings.Merge(JsonConvert.DeserializeObject(indentationSettings)); + settings = JsonConvert.SerializeObject(mergedSettings); + } + } + + if (!string.IsNullOrEmpty(settings)) + { + var documentId = DocumentId.CreateNewId(projectId); + solution = solution.AddAdditionalDocument(documentId, SettingsHelper.SettingsFileName, settings); + } + + ParseOptions parseOptions = solution.GetProject(projectId).ParseOptions; + return solution.WithProjectParseOptions(projectId, parseOptions.WithDocumentationMode(DocumentationMode.Diagnose)); + } + + /// + /// Gets the diagnostics that will be suppressed. + /// + /// A collection of diagnostic identifiers. + protected virtual IEnumerable GetDisabledDiagnostics() + { + return Enumerable.Empty(); + } + + /// + /// Gets the content of the settings file to use. + /// + /// The contents of the settings file to use. + protected virtual string GetSettings() + { + return null; + } + + protected DiagnosticResult CSharpDiagnostic(string diagnosticId = null) + { + var analyzers = this.GetCSharpDiagnosticAnalyzers(); + var supportedDiagnostics = Enumerable.SelectMany(analyzers, analyzer => analyzer.SupportedDiagnostics); + if (diagnosticId == null) + { + return this.CSharpDiagnostic(supportedDiagnostics.Single()); + } + else + { + return this.CSharpDiagnostic(supportedDiagnostics.Single(i => i.Id == diagnosticId)); + } + } + + protected DiagnosticResult CSharpDiagnostic(DiagnosticDescriptor descriptor) + { + return new DiagnosticResult(descriptor); + } + + protected DiagnosticResult CSharpCompilerError(string errorIdentifier) + { + return new DiagnosticResult + { + Id = errorIdentifier, + Severity = DiagnosticSeverity.Error, + }; + } + + /// + /// Create a project using the input strings as sources. + /// + /// + /// This method first creates a by calling , and then + /// applies compilation options to the project by calling . + /// + /// Classes in the form of strings. + /// The language the source classes are in. Values may be taken from the + /// class. + /// The filenames or null if the default filename should be used + /// A created out of the s created from the source + /// strings. + protected Project CreateProject(string[] sources, string language = LanguageNames.CSharp, string[] filenames = null) + { + Project project = this.CreateProjectImpl(sources, language, filenames); + return this.ApplyCompilationOptions(project); + } + + /// + /// Create a project using the input strings as sources. + /// + /// Classes in the form of strings. + /// The language the source classes are in. Values may be taken from the + /// class. + /// The filenames or null if the default filename should be used + /// A created out of the s created from the source + /// strings. + protected virtual Project CreateProjectImpl(string[] sources, string language, string[] filenames) + { + string fileNamePrefix = DefaultFilePathPrefix; + string fileExt = language == LanguageNames.CSharp ? CSharpDefaultFileExt : VisualBasicDefaultExt; + + var projectId = ProjectId.CreateNewId(debugName: TestProjectName); + var solution = this.CreateSolution(projectId, language); + + int count = 0; + for (int i = 0; i < sources.Length; i++) + { + string source = sources[i]; + var newFileName = filenames?[i] ?? fileNamePrefix + count + "." + fileExt; + var documentId = DocumentId.CreateNewId(projectId, debugName: newFileName); + solution = solution.AddDocument(documentId, newFileName, SourceText.From(source)); + count++; + } + + return solution.GetProject(projectId); + } + + /// + /// Applies compilation options to a project. + /// + /// + /// The default implementation configures the project by enabling all supported diagnostics of analyzers + /// included in as well as AD0001. After configuring these + /// diagnostics, any diagnostic IDs indicated in are explictly supressed + /// using . + /// + /// The project. + /// The modified project. + protected virtual Project ApplyCompilationOptions(Project project) + { + var analyzers = this.GetCSharpDiagnosticAnalyzers(); + + var supportedDiagnosticsSpecificOptions = new Dictionary(); + foreach (var analyzer in analyzers) + { + foreach (var diagnostic in analyzer.SupportedDiagnostics) + { + // make sure the analyzers we are testing are enabled + supportedDiagnosticsSpecificOptions[diagnostic.Id] = ReportDiagnostic.Default; + } + } + + // Report exceptions during the analysis process as errors + supportedDiagnosticsSpecificOptions.Add("AD0001", ReportDiagnostic.Error); + + foreach (var id in this.GetDisabledDiagnostics()) + { + supportedDiagnosticsSpecificOptions[id] = ReportDiagnostic.Suppress; + } + + // update the project compilation options + var modifiedSpecificDiagnosticOptions = supportedDiagnosticsSpecificOptions.ToImmutableDictionary().SetItems(project.CompilationOptions.SpecificDiagnosticOptions); + var modifiedCompilationOptions = project.CompilationOptions.WithSpecificDiagnosticOptions(modifiedSpecificDiagnosticOptions); + + Solution solution = project.Solution.WithProjectCompilationOptions(project.Id, modifiedCompilationOptions); + return solution.GetProject(project.Id); + } + + /// + /// Sort s by location in source document. + /// + /// A collection of s to be sorted. + /// A collection containing the input , sorted by + /// and . + private static Diagnostic[] SortDistinctDiagnostics(IEnumerable diagnostics) + { + return diagnostics.OrderBy(d => d.Location.SourceSpan.Start).ThenBy(d => d.Id).ToArray(); + } + + /// + /// Given classes in the form of strings, their language, and an to apply to + /// it, return the s found in the string after converting it to a + /// . + /// + /// Classes in the form of strings. + /// The language the source classes are in. Values may be taken from the + /// class. + /// The analyzers to be run on the sources. + /// The that the task will observe. + /// The filenames or null if the default filename should be used + /// A collection of s that surfaced in the source code, sorted by + /// . + private Task> GetSortedDiagnosticsAsync(string[] sources, string language, ImmutableArray analyzers, CancellationToken cancellationToken, string[] filenames) + { + return GetSortedDiagnosticsFromDocumentsAsync(analyzers, this.GetDocuments(sources, language, filenames), cancellationToken); + } + + /// + /// Given an array of strings as sources and a language, turn them into a and return the + /// documents and spans of it. + /// + /// Classes in the form of strings. + /// The language the source classes are in. Values may be taken from the + /// class. + /// The filenames or null if the default filename should be used + /// A collection of s representing the sources. + private Document[] GetDocuments(string[] sources, string language, string[] filenames) + { + if (language != LanguageNames.CSharp && language != LanguageNames.VisualBasic) + { + throw new ArgumentException("Unsupported Language"); + } + + var project = this.CreateProject(sources, language, filenames); + var documents = project.Documents.ToArray(); + + if (sources.Length != documents.Length) + { + throw new SystemException("Amount of sources did not match amount of Documents created"); + } + + return documents; + } + } +} diff --git a/src/Agoda.Analyzers.Test/Helpers/DiagnosticVerifier.cs b/src/Agoda.Analyzers.Test/Helpers/DiagnosticVerifier.cs new file mode 100644 index 0000000..7779924 --- /dev/null +++ b/src/Agoda.Analyzers.Test/Helpers/DiagnosticVerifier.cs @@ -0,0 +1,441 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Text; +using NUnit.Framework; + +namespace Agoda.Analyzers.Test.Helpers +{ + /// + /// Superclass of all unit tests for s. + /// + public abstract partial class DiagnosticVerifier + { + private const int DefaultIndentationSize = 4; + private const int DefaultTabSize = 4; + private const bool DefaultUseTabs = false; + + public DiagnosticVerifier() + { + this.IndentationSize = DefaultIndentationSize; + this.TabSize = DefaultTabSize; + this.UseTabs = DefaultUseTabs; + } + + /// + /// Gets or sets the value of the to apply to the test + /// workspace. + /// + /// + /// The value of the to apply to the test workspace. + /// + public int IndentationSize + { + get; + protected set; + } + + /// + /// Gets or sets a value indicating whether the option is applied to the + /// test workspace. + /// + /// + /// The value of the to apply to the test workspace. + /// + public bool UseTabs + { + get; + protected set; + } + + /// + /// Gets or sets the value of the to apply to the test workspace. + /// + /// + /// The value of the to apply to the test workspace. + /// + public int TabSize + { + get; + protected set; + } + + protected static DiagnosticResult[] EmptyDiagnosticResults { get; } = { }; + + /// + /// Verifies that the analyzer will properly handle an empty source. + /// + /// A representing the asynchronous unit test. + [Test] + public async Task TestEmptySourceAsync() + { + var testCode = string.Empty; + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + /// + /// Verifies that each diagnostics contains a in the expected + /// format. + /// + [Test] + public void TestHelpLink() + { + foreach (var diagnosticAnalyzer in this.GetCSharpDiagnosticAnalyzers()) + { + foreach (var diagnostic in diagnosticAnalyzer.SupportedDiagnostics) + { + if (diagnostic.DefaultSeverity == DiagnosticSeverity.Hidden && diagnostic.CustomTags.Contains(WellKnownDiagnosticTags.NotConfigurable)) + { + // This diagnostic will never appear in the UI. + continue; + } + + string expected = $"https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/{diagnostic.Id}.md"; + Assert.AreEqual(expected, diagnostic.HelpLinkUri); + } + } + } + + /// + /// Gets the C# analyzers being tested + /// + /// + /// New instances of all the C# analyzers being tested. + /// + protected abstract IEnumerable GetCSharpDiagnosticAnalyzers(); + + /// + /// Called to test a C# when applied on the single input source as a string. + /// + /// Input a for the expected . + /// + /// + /// A class in the form of a string to run the analyzer on. + /// A s describing the that should + /// be reported by the analyzer for the specified source. + /// The that the task will observe. + /// The filename or null if the default filename should be used + /// A representing the asynchronous operation. + protected Task VerifyCSharpDiagnosticAsync(string source, DiagnosticResult expected, CancellationToken cancellationToken, string filename = null) + { + return VerifyCSharpDiagnosticAsync(source, (DiagnosticResult[]) new[] { expected }, cancellationToken, filename); + } + + /// + /// Called to test a C# when applied on the single input source as a string. + /// + /// Input a for each expected. + /// + /// + /// A class in the form of a string to run the analyzer on. + /// A collection of s describing the + /// s that should be reported by the analyzer for the specified source. + /// The that the task will observe. + /// The filename or null if the default filename should be used + /// A representing the asynchronous operation. + protected Task VerifyCSharpDiagnosticAsync(string source, DiagnosticResult[] expected, CancellationToken cancellationToken, string filename = null) + { + return this.VerifyDiagnosticsAsync(new[] { source }, LanguageNames.CSharp, this.GetCSharpDiagnosticAnalyzers().ToImmutableArray(), expected, cancellationToken, filename != null ? new[] { filename } : null); + } + + /// + /// Called to test a C# when applied on the input strings as sources. + /// + /// Input a for each expected. + /// + /// + /// A collection of strings to create source documents from to run the analyzers + /// on. + /// A collection of s describing the + /// s that should be reported by the analyzer for the specified sources. + /// The that the task will observe. + /// The filenames or null if the default filename should be used + /// A representing the asynchronous operation. + protected Task VerifyCSharpDiagnosticAsync(string[] sources, DiagnosticResult[] expected, CancellationToken cancellationToken, string[] filenames = null) + { + return this.VerifyDiagnosticsAsync(sources, LanguageNames.CSharp, this.GetCSharpDiagnosticAnalyzers().ToImmutableArray(), expected, cancellationToken, filenames); + } + + /// + /// Checks each of the actual s found and compares them with the corresponding + /// in the array of expected results. s are considered + /// equal only if the , , + /// , and of the + /// match the actual . + /// + /// The s found by the compiler after running the analyzer + /// on the source code. + /// The analyzers that have been run on the sources. + /// A collection of s describing the expected + /// diagnostics for the sources. + protected static void VerifyDiagnosticResults(IEnumerable actualResults, ImmutableArray analyzers, DiagnosticResult[] expectedResults) + { + int expectedCount = expectedResults.Length; + int actualCount = actualResults.Count(); + + if (expectedCount != actualCount) + { + string diagnosticsOutput = actualResults.Any() ? FormatDiagnostics(analyzers, actualResults.ToArray()) : " NONE."; + + Assert.True( + false, + string.Format("Mismatch between number of diagnostics returned, expected \"{0}\" actual \"{1}\"\r\n\r\nDiagnostics:\r\n{2}\r\n", expectedCount, actualCount, diagnosticsOutput)); + } + + for (int i = 0; i < expectedResults.Length; i++) + { + var actual = actualResults.ElementAt(i); + var expected = expectedResults[i]; + + if (!expected.HasLocation) + { + if (actual.Location != Location.None) + { + string message = + string.Format( + "Expected:\nA project diagnostic with No location\nActual:\n{0}", + FormatDiagnostics(analyzers, actual)); + Assert.True(false, message); + } + } + else + { + VerifyDiagnosticLocation(analyzers, actual, actual.Location, expected.Spans.First()); + var additionalLocations = actual.AdditionalLocations.ToArray(); + + if (additionalLocations.Length != expected.Spans.Length - 1) + { + Assert.True( + false, + string.Format( + "Expected {0} additional locations but got {1} for Diagnostic:\r\n {2}\r\n", + expected.Spans.Length - 1, + additionalLocations.Length, + FormatDiagnostics(analyzers, actual))); + } + + for (int j = 0; j < additionalLocations.Length; ++j) + { + VerifyDiagnosticLocation(analyzers, actual, additionalLocations[j], expected.Spans[j + 1]); + } + } + + if (actual.Id != expected.Id) + { + string message = + string.Format( + "Expected diagnostic id to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", + expected.Id, + actual.Id, + FormatDiagnostics(analyzers, actual)); + Assert.True(false, message); + } + + if (actual.Severity != expected.Severity) + { + string message = + string.Format( + "Expected diagnostic severity to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", + expected.Severity, + actual.Severity, + FormatDiagnostics(analyzers, actual)); + Assert.True(false, message); + } + + if (actual.GetMessage() != expected.Message) + { + string message = + string.Format( + "Expected diagnostic message to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", + expected.Message, + actual.GetMessage(), + FormatDiagnostics(analyzers, actual)); + Assert.True(false, message); + } + } + } + + /// + /// Helper method to that checks the location of a + /// and compares it with the location described by a + /// . + /// + /// The analyzer that have been run on the sources. + /// The diagnostic that was found in the code. + /// The location of the diagnostic found in the code. + /// The describing the expected location of the + /// diagnostic. + private static void VerifyDiagnosticLocation(ImmutableArray analyzers, Diagnostic diagnostic, Location actual, FileLinePositionSpan expected) + { + var actualSpan = actual.GetLineSpan(); + + string message = + string.Format( + "Expected diagnostic to be in file \"{0}\" was actually in file \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", + expected.Path, + actualSpan.Path, + FormatDiagnostics(analyzers, diagnostic)); + Assert.True( + actualSpan.Path == expected.Path || (actualSpan.Path != null && actualSpan.Path.Contains("Test0.") && expected.Path.Contains("Test.")), + message); + + var actualStartLinePosition = actualSpan.StartLinePosition; + var actualEndLinePosition = actualSpan.EndLinePosition; + + VerifyLinePosition(analyzers, diagnostic, actualSpan.StartLinePosition, expected.StartLinePosition, "start"); + if (expected.StartLinePosition < expected.EndLinePosition) + { + VerifyLinePosition(analyzers, diagnostic, actualSpan.EndLinePosition, expected.EndLinePosition, "end"); + } + } + + private static void VerifyLinePosition(ImmutableArray analyzers, Diagnostic diagnostic, LinePosition actualLinePosition, LinePosition expectedLinePosition, string positionText) + { + // Only check the line position if it matters + if (expectedLinePosition.Line > 0) + { + Assert.True( + (actualLinePosition.Line + 1) == expectedLinePosition.Line, + string.Format( + "Expected diagnostic to {0} on line \"{1}\" was actually on line \"{2}\"\r\n\r\nDiagnostic:\r\n {3}\r\n", + positionText, + expectedLinePosition.Line, + actualLinePosition.Line + 1, + FormatDiagnostics(analyzers, diagnostic))); + } + + // Only check the column position if it matters + if (expectedLinePosition.Character > 0) + { + Assert.True( + (actualLinePosition.Character + 1) == expectedLinePosition.Character, + string.Format( + "Expected diagnostic to {0} at column \"{1}\" was actually at column \"{2}\"\r\n\r\nDiagnostic:\r\n {3}\r\n", + positionText, + expectedLinePosition.Character, + actualLinePosition.Character + 1, + FormatDiagnostics(analyzers, diagnostic))); + } + } + + /// + /// Helper method to format a into an easily readable string. + /// + /// The analyzers that this verifier tests. + /// A collection of s to be formatted. + /// The formatted as a string. + private static string FormatDiagnostics(ImmutableArray analyzers, params Diagnostic[] diagnostics) + { + var builder = new StringBuilder(); + for (int i = 0; i < diagnostics.Length; ++i) + { + var diagnosticsId = diagnostics[i].Id; + + builder.Append("// ").AppendLine(diagnostics[i].ToString()); + + var applicableAnalyzer = analyzers.FirstOrDefault(a => a.SupportedDiagnostics.Any(dd => dd.Id == diagnosticsId)); + if (applicableAnalyzer != null) + { + var analyzerType = applicableAnalyzer.GetType(); + + var location = diagnostics[i].Location; + if (location == Location.None) + { + builder.AppendFormat("GetGlobalResult({0}.{1})", analyzerType.Name, diagnosticsId); + } + else + { + Assert.True( + location.IsInSource, + string.Format("Test base does not currently handle diagnostics in metadata locations. Diagnostic in metadata:\r\n{0}", diagnostics[i])); + + string resultMethodName = diagnostics[i].Location.SourceTree.FilePath.EndsWith(".cs") ? "GetCSharpResultAt" : "GetBasicResultAt"; + var linePosition = diagnostics[i].Location.GetLineSpan().StartLinePosition; + + builder.AppendFormat( + "{0}({1}, {2}, {3}.{4})", + resultMethodName, + linePosition.Line + 1, + linePosition.Character + 1, + analyzerType.Name, + diagnosticsId); + } + + if (i != diagnostics.Length - 1) + { + builder.Append(','); + } + + builder.AppendLine(); + } + } + + return builder.ToString(); + } + + private static bool IsSubjectToExclusion(DiagnosticResult result) + { + if (result.Id.StartsWith("CS", StringComparison.Ordinal)) + { + return false; + } + + if (result.Id.StartsWith("AD", StringComparison.Ordinal)) + { + return false; + } + + if (result.Spans.Length == 0) + { + return false; + } + + return true; + } + + /// + /// General method that gets a collection of actual s found in the source after the + /// analyzer is run, then verifies each of them. + /// + /// An array of strings to create source documents from to run the analyzers on. + /// The language of the classes represented by the source strings. + /// The analyzers to be run on the source code. + /// A collection of s that should appear after the analyzer + /// is run on the sources. + /// The that the task will observe. + /// The filenames or null if the default filename should be used + /// A representing the asynchronous operation. + private async Task VerifyDiagnosticsAsync(string[] sources, string language, ImmutableArray analyzers, DiagnosticResult[] expected, CancellationToken cancellationToken, string[] filenames) + { + VerifyDiagnosticResults(await this.GetSortedDiagnosticsAsync(sources, language, analyzers, cancellationToken, filenames).ConfigureAwait(false), analyzers, expected); + + // If filenames is null we want to test for exclusions too + if (filenames == null) + { + // Also check if the analyzer honors exclusions + if (expected.Any(IsSubjectToExclusion)) + { + // Diagnostics reported by the compiler and analyzer diagnostics which don't have a location will + // still be reported. We also insert a new line at the beginning so we have to move all diagnostic + // locations which have a specific position down by one line. + var expectedResults = expected + .Where(x => !IsSubjectToExclusion(x)) + .Select(x => x.WithLineOffset(1)) + .ToArray(); + + VerifyDiagnosticResults(await this.GetSortedDiagnosticsAsync(sources.Select(x => " // \r\n" + x).ToArray(), language, analyzers, cancellationToken, null).ConfigureAwait(false), analyzers, expectedResults); + } + } + } + } +} diff --git a/src/Agoda.Analyzers.Test/Helpers/MetadataReferences.cs b/src/Agoda.Analyzers.Test/Helpers/MetadataReferences.cs new file mode 100644 index 0000000..97e223b --- /dev/null +++ b/src/Agoda.Analyzers.Test/Helpers/MetadataReferences.cs @@ -0,0 +1,22 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace Agoda.Analyzers.Test.Helpers +{ + /// + /// Metadata references used to create test projects. + /// + internal static class MetadataReferences + { + internal static readonly MetadataReference CorlibReference = MetadataReference.CreateFromFile(typeof(object).Assembly.Location).WithAliases(ImmutableArray.Create("global", "corlib")); + internal static readonly MetadataReference SystemReference = MetadataReference.CreateFromFile(typeof(System.Diagnostics.Debug).Assembly.Location).WithAliases(ImmutableArray.Create("global", "system")); + internal static readonly MetadataReference SystemCoreReference = MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location); + internal static readonly MetadataReference CSharpSymbolsReference = MetadataReference.CreateFromFile(typeof(CSharpCompilation).Assembly.Location); + internal static readonly MetadataReference CodeAnalysisReference = MetadataReference.CreateFromFile(typeof(Compilation).Assembly.Location); + } +} diff --git a/src/Agoda.Analyzers.Test/Helpers/TestDiagnosticProvider.cs b/src/Agoda.Analyzers.Test/Helpers/TestDiagnosticProvider.cs new file mode 100644 index 0000000..390a503 --- /dev/null +++ b/src/Agoda.Analyzers.Test/Helpers/TestDiagnosticProvider.cs @@ -0,0 +1,43 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; + +namespace Agoda.Analyzers.Test.Helpers +{ + internal sealed class TestDiagnosticProvider : FixAllContext.DiagnosticProvider + { + private ImmutableArray diagnostics; + + private TestDiagnosticProvider(ImmutableArray diagnostics) + { + this.diagnostics = diagnostics; + } + + public override Task> GetAllDiagnosticsAsync(Project project, CancellationToken cancellationToken) + { + return Task.FromResult>(this.diagnostics); + } + + public override Task> GetDocumentDiagnosticsAsync(Document document, CancellationToken cancellationToken) + { + return Task.FromResult(this.diagnostics.Where(i => i.Location.GetLineSpan().Path == document.Name)); + } + + public override Task> GetProjectDiagnosticsAsync(Project project, CancellationToken cancellationToken) + { + return Task.FromResult(this.diagnostics.Where(i => !i.Location.IsInSource)); + } + + internal static TestDiagnosticProvider Create(ImmutableArray diagnostics) + { + return new TestDiagnosticProvider(diagnostics); + } + } +} diff --git a/src/Agoda.Analyzers.Test/Properties/AssemblyInfo.cs b/src/Agoda.Analyzers.Test/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..ca930c5 --- /dev/null +++ b/src/Agoda.Analyzers.Test/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Agoda.Analyzers.Test")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Agoda.Analyzers.Test")] +[assembly: AssemblyCopyright("Copyright © 2017")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("756b9dd8-2fe7-485d-8640-6e2755514eae")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Agoda.Analyzers.Test/StyleCop/SA1106UnitTests.cs b/src/Agoda.Analyzers.Test/StyleCop/SA1106UnitTests.cs new file mode 100644 index 0000000..f2318e4 --- /dev/null +++ b/src/Agoda.Analyzers.Test/StyleCop/SA1106UnitTests.cs @@ -0,0 +1,433 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Agoda.Analyzers.CodeFixes.StyleCop; +using Agoda.Analyzers.StyleCop; +using Agoda.Analyzers.Test.Helpers; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Diagnostics; +using NUnit.Framework; + +namespace Agoda.Analyzers.Test.StyleCop +{ + public class SA1106UnitTests : CodeFixVerifier + { + [Test] + [TestCase("if (true)")] + [TestCase("if (true) { } else")] + [TestCase("for (int i = 0; i < 10; i++)")] + [TestCase("while (true)")] + public async Task TestEmptyStatementAsBlockAsync(string controlFlowConstruct) + { + var testCode = $@" +class TestClass +{{ + public void TestMethod() + {{ + {controlFlowConstruct} + ; + }} +}}"; + var fixedCode = $@" +class TestClass +{{ + public void TestMethod() + {{ + {controlFlowConstruct} + {{ + }} + }} +}}"; + + DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(7, 13); + + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedCode).ConfigureAwait(false); + } + + [Test] + public async Task TestEmptyStatementAsBlockInDoWhileAsync() + { + var testCode = @" +class TestClass +{ + public void TestMethod() + { + do + ; + while (false); + } +}"; + var fixedCode = @" +class TestClass +{ + public void TestMethod() + { + do + { + } + while (false); + } +}"; + + DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(7, 13); + + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedCode).ConfigureAwait(false); + } + + [Test] + public async Task TestEmptyStatementWithinBlockAsync() + { + var testCode = @" +class TestClass +{ + public void TestMethod() + { + for (int i = 0; i < 10; i++) + { + var temp = i; + ; + } + } +}"; + var fixedCode = @" +class TestClass +{ + public void TestMethod() + { + for (int i = 0; i < 10; i++) + { + var temp = i; + } + } +}"; + + DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(9, 13); + + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedCode).ConfigureAwait(false); + } + + [Test] + public async Task TestEmptyStatementInForStatementAsync() + { + var testCode = @" +class TestClass +{ + public void TestMethod() + { + for (;;) + { + } + } +}"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Test] + public async Task TestEmptyStatementAsync() + { + var testCode = @" +class TestClass +{ + public void TestMethod() + { + ; + } +}"; + var fixedCode = @" +class TestClass +{ + public void TestMethod() + { + } +}"; + + DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(6, 9); + + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedCode).ConfigureAwait(false); + } + + [Test] + public async Task TestLabeledEmptyStatementAsync() + { + var testCode = @" +class TestClass +{ + public void TestMethod() + { + label: + ; + } +}"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Test] + public async Task TestLabeledEmptyStatementFollowedByEmptyStatementAsync() + { + var testCode = @" +class TestClass +{ + public void TestMethod() + { + label: + ; + ; + } +}"; + var fixedCode = @" +class TestClass +{ + public void TestMethod() + { + label: + ; + } +}"; + + DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(8, 9); + + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedCode).ConfigureAwait(false); + } + + [Test] + public async Task TestLabeledEmptyStatementFollowedByNonEmptyStatementAsync() + { + var testCode = @" +class TestClass +{ + public void TestMethod() + { + label: + ; + int x = 3; + } +}"; + var fixedCode = @" +class TestClass +{ + public void TestMethod() + { + label: + int x = 3; + } +}"; + + DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(7, 9); + + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedCode).ConfigureAwait(false); + } + + [Test] + public async Task TestConsecutiveLabelsAsync() + { + var testCode = @" +class TestClass +{ + public void TestMethod() + { + label1: + label2: + ; + int x = 3; + } +}"; + var fixedCode = @" +class TestClass +{ + public void TestMethod() + { + label1: + label2: + int x = 3; + } +}"; + + DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(8, 9); + + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedCode).ConfigureAwait(false); + } + + [Test] + public async Task TestSwitchCasesAsync() + { + var testCode = @" +class TestClass +{ + public void TestMethod() + { + switch (default(int)) + { + case 0: + ; + break; + + case 1: + case 2: + ; + break; + + default: + ; + break; + } + } +}"; + var fixedCode = @" +class TestClass +{ + public void TestMethod() + { + switch (default(int)) + { + case 0: + break; + + case 1: + case 2: + break; + + default: + break; + } + } +}"; + + DiagnosticResult[] expected = + { + this.CSharpDiagnostic().WithLocation(9, 13), + this.CSharpDiagnostic().WithLocation(14, 13), + this.CSharpDiagnostic().WithLocation(18, 13), + }; + + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedCode).ConfigureAwait(false); + } + + [Test] + [TestCase("class Foo { }")] + [TestCase("struct Foo { }")] + [TestCase("interface IFoo { }")] + [TestCase("enum Foo { }")] + [TestCase("namespace Foo { }")] + public async Task TestMemberAsync(string declaration) + { + var testCode = declaration + ";"; + var fixedCode = declaration; + + DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(1, declaration.Length + 1); + + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedCode).ConfigureAwait(false); + } + + /// + /// Verifies that the code fix will remove all unnecessary whitespace. + /// This is a regression for #1556 + /// + /// A representing the asynchronous unit test. + [Test] + public async Task VerifyCodeFixWillRemoveUnnecessaryWhitespaceAsync() + { + var testCode = @" +class TestClass +{ + public void TestMethod1() + { + throw new System.NotImplementedException(); ; + } + + public void TestMethod2() + { + throw new System.NotImplementedException(); /* c1 */ ; + } +}"; + var fixedCode = @" +class TestClass +{ + public void TestMethod1() + { + throw new System.NotImplementedException(); + } + + public void TestMethod2() + { + throw new System.NotImplementedException(); /* c1 */ + } +}"; + + DiagnosticResult[] expected = + { + this.CSharpDiagnostic().WithLocation(6, 53), + this.CSharpDiagnostic().WithLocation(11, 62), + }; + + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedCode).ConfigureAwait(false); + } + + /// + /// Verifies that the code fix will not remove relevant trivia. + /// + /// A representing the asynchronous unit test. + [Test] + public async Task VerifyCodeFixWillNotRemoveTriviaAsync() + { + var testCode = @" +class TestClass +{ + public void TestMethod() + { + /* do nothing */ ; + } +}"; + var fixedCode = @" +class TestClass +{ + public void TestMethod() + { + /* do nothing */ + } +}"; + + var expected = this.CSharpDiagnostic().WithLocation(6, 26); + + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedCode).ConfigureAwait(false); + } + + /// + protected override IEnumerable GetCSharpDiagnosticAnalyzers() + { + yield return new SA1106CodeMustNotContainEmptyStatements(); + } + + /// + protected override CodeFixProvider GetCSharpCodeFixProvider() + { + return new SA1106CodeFixProvider(); + } + } +} diff --git a/src/Agoda.Analyzers.Test/StyleCop/SA1107UnitTests.cs b/src/Agoda.Analyzers.Test/StyleCop/SA1107UnitTests.cs new file mode 100644 index 0000000..b29f3fc --- /dev/null +++ b/src/Agoda.Analyzers.Test/StyleCop/SA1107UnitTests.cs @@ -0,0 +1,166 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Agoda.Analyzers.CodeFixes.StyleCop; +using Agoda.Analyzers.StyleCop; +using Agoda.Analyzers.Test.Helpers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Diagnostics; +using NUnit.Framework; + +namespace Agoda.Analyzers.Test.StyleCop +{ + /// + /// This class contains unit tests for and + /// . + /// + public class SA1107UnitTests : CodeFixVerifier + { + [Test] + public async Task TestCorrectCodeAsync() + { + string testCode = @" +using System; +class ClassName +{ + public static void Foo(string a, string b) + { + int i = 5; + int j = 6, k = 3; + if(true) + { + i++; + } + else + { + j++; + } + Foo(""a"", ""b""); + + Func f = (c, d) => c + d; + Func g = (c, d) => { return c + d; }; + } +} +"; + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Test] + public async Task TestWrongCodeAsync() + { + string testCode = @" +using System; +class ClassName +{ + public static void Foo(string a, string b) + { + int i = 5; int j = 6, k = 3; if(true) + { + i++; + } + else + { + j++; + } Foo(""a"", ""b""); + + Func g = (c, d) => { c++; return c + d; }; + } +} +"; + var expected = new[] + { + this.CSharpDiagnostic().WithLocation(7, 20), + this.CSharpDiagnostic().WithLocation(7, 38), + this.CSharpDiagnostic().WithLocation(14, 11), + this.CSharpDiagnostic().WithLocation(16, 50), + }; + + string fixedCode = @" +using System; +class ClassName +{ + public static void Foo(string a, string b) + { + int i = 5; + int j = 6, k = 3; + if (true) + { + i++; + } + else + { + j++; + } + + Foo(""a"", ""b""); + + Func g = (c, d) => { c++; + return c + d; }; + } +} +"; + + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false); + } + + [Test] + public async Task TestThatAnalyzerDoesntCrashOnEmptyBlockAsync() + { + string testCode = @" +using System; +class ClassName +{ + public static void Foo(string a, string b) + { + } +} +"; + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Test] + public async Task TestThatAnalyzerIgnoresStatementsWithMissingTokenAsync() + { + string testCode = @" +using System; +class ClassName +{ + public static void Foo(string a, string b) + { + int i + if (true) + { + Console.WriteLine(""Bar""); + } + } +} +"; + DiagnosticResult expected = new DiagnosticResult + { + Id = "CS1002", + Message = "; expected", + Severity = DiagnosticSeverity.Error, + }; + + expected = expected.WithLocation(7, 14); + + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + } + + protected override IEnumerable GetCSharpDiagnosticAnalyzers() + { + yield return new SA1107CodeMustNotContainMultipleStatementsOnOneLine(); + } + + protected override CodeFixProvider GetCSharpCodeFixProvider() + { + return new SA1107CodeFixProvider(); + } + } +} diff --git a/src/Agoda.Analyzers.Test/StyleCop/SA1123UnitTests.cs b/src/Agoda.Analyzers.Test/StyleCop/SA1123UnitTests.cs new file mode 100644 index 0000000..459920b --- /dev/null +++ b/src/Agoda.Analyzers.Test/StyleCop/SA1123UnitTests.cs @@ -0,0 +1,197 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Agoda.Analyzers.CodeFixes.StyleCop; +using Agoda.Analyzers.StyleCop; +using Agoda.Analyzers.Test.Helpers; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Diagnostics; +using NUnit.Framework; + +namespace Agoda.Analyzers.Test.StyleCop +{ + /// + /// This class contains unit tests for and + /// . + /// + public class SA1123UnitTests : CodeFixVerifier + { + [Test] + public async Task TestRegionInMethodAsync() + { + var testCode = @"public class Foo +{ + public void Bar() + { +#region Foo + string test = """"; +#endregion + } +}"; + + DiagnosticResult expected = this.CSharpDiagnostic().WithLocation(5, 1); + + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + + string fixedCode = @"public class Foo +{ + public void Bar() + { + string test = """"; + } +}"; + + await this.VerifyCSharpFixAsync(testCode, fixedCode).ConfigureAwait(false); + await this.VerifyCSharpFixAllFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false); + } + + [Test] + public async Task TestRegionPartialyInMethodAsync() + { + var testCode = @"public class Foo +{ + public void Bar() + { +#region Foo + string test = """"; + } +#endregion +}"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Test] + public async Task TestRegionPartialyInMethod2Async() + { + var testCode = @"public class Foo +{ + public void Bar() +#region Foo + { + string test = """"; + } +#endregion +}"; + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Test] + public async Task TestRegionPartialyMultipleMethodsAsync() + { + var testCode = @"public class Foo +{ + public void Bar() + { +#region Foo + string test = """"; + } + public void FooBar() + { + string test = """"; +#endregion + } +}"; + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Test] + public async Task TestEndRegionInMethodAsync() + { + var testCode = @"public class Foo +{ +#region Foo + public void Bar() + { + string test = """"; +#endregion + } +}"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Test] + public async Task TestRegionOutsideMethodAsync() + { + var testCode = @"public class Foo +{ +#region Foo +#endregion + public void Bar() + { + string test = """"; + } +}"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Test] + public async Task TestRegionOutsideMethod2Async() + { + var testCode = @"public class Foo +{ +#region Foo + public void Bar() + { + string test = """"; + } +#endregion +}"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Test] + public async Task TestFixAllProviderAsync() + { + string testCode = @" +class ClassName +{ + void MethodName() + { + #region Foo + #region Foo + #region Foo + #endregion + #endregion + #endregion + #region Foo + #region Foo + #region Foo + // Test + #endregion + #endregion + #endregion + } +} +"; + + string fixedCode = @" +class ClassName +{ + void MethodName() + { + // Test + } +} +"; + await this.VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAllFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false); + } + + protected override IEnumerable GetCSharpDiagnosticAnalyzers() + { + yield return new SA1123DoNotPlaceRegionsWithinElements(); + } + + protected override CodeFixProvider GetCSharpCodeFixProvider() + { + return new RemoveRegionCodeFixProvider(); + } + } +} diff --git a/src/Agoda.Analyzers.Test/packages.config b/src/Agoda.Analyzers.Test/packages.config new file mode 100644 index 0000000..d62435f --- /dev/null +++ b/src/Agoda.Analyzers.Test/packages.config @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Agoda.Analyzers/Agoda.Analyzers.csproj b/src/Agoda.Analyzers/Agoda.Analyzers.csproj new file mode 100644 index 0000000..427df7a --- /dev/null +++ b/src/Agoda.Analyzers/Agoda.Analyzers.csproj @@ -0,0 +1,123 @@ + + + + + 11.0 + Debug + AnyCPU + {4F934D25-9BFF-4153-8965-F12F52BA41DF} + Library + Properties + Agoda.Analyzers + Agoda.Analyzers + en-US + 512 + {786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Profile7 + v4.5 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + True + True + CustomRulesResources.resx + + + + + True + True + HelpersResources.resx + + + + + + + + True + True + ReadabilityResources.resx + + + + + + + + + + + + + + + + + ..\packages\Microsoft.CodeAnalysis.Common.1.3.2\lib\portable-net45+win8\Microsoft.CodeAnalysis.dll + True + + + ..\packages\Microsoft.CodeAnalysis.CSharp.1.3.2\lib\portable-net45+win8\Microsoft.CodeAnalysis.CSharp.dll + True + + + ..\packages\Newtonsoft.Json.8.0.3\lib\portable-net40+sl5+wp80+win8+wpa81\Newtonsoft.Json.dll + True + + + ..\packages\System.Collections.Immutable.1.1.37\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll + True + + + ..\packages\System.Reflection.Metadata.1.2.0\lib\portable-net45+win8\System.Reflection.Metadata.dll + True + + + + + ResXFileCodeGenerator + CustomRulesResources.Designer.cs + + + ResXFileCodeGenerator + HelpersResources.Designer.cs + + + ResXFileCodeGenerator + ReadabilityResources.Designer.cs + + + + + + \ No newline at end of file diff --git a/src/Agoda.Analyzers/AgodaCustom/AG0001DependencyResolverMustNotBeUsed.cs b/src/Agoda.Analyzers/AgodaCustom/AG0001DependencyResolverMustNotBeUsed.cs new file mode 100644 index 0000000..d84f27d --- /dev/null +++ b/src/Agoda.Analyzers/AgodaCustom/AG0001DependencyResolverMustNotBeUsed.cs @@ -0,0 +1,51 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using StyleCop.Analyzers; +using System; +using System.Collections.Immutable; + +namespace Agoda.Analyzers.AgodaCustom +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class AG0001DependencyResolverMustNotBeUsed: DiagnosticAnalyzer + { + public const string DiagnosticId = "AG0001"; + private static readonly LocalizableString Title = new LocalizableResourceString(nameof(CustomRulesResources.AG0001Title), CustomRulesResources.ResourceManager, typeof(CustomRulesResources)); + private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(CustomRulesResources.AG0001MessageFormat), CustomRulesResources.ResourceManager, typeof(CustomRulesResources)); + private static readonly LocalizableString Description = new LocalizableResourceString(nameof(CustomRulesResources.AG0001Description), CustomRulesResources.ResourceManager, typeof(CustomRulesResources)); + private static readonly string HelpLink = "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1106.md"; + + private static readonly DiagnosticDescriptor Descriptor = + new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, AnalyzerCategory.CustomQualityRules, + DiagnosticSeverity.Warning, AnalyzerConstants.EnabledByDefault, Description, HelpLink, WellKnownDiagnosticTags.EditAndContinue); + + private static readonly Action DependencyResolverUsageAction = HandleDependencyResolverUsage; + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor); + + private static void HandleDependencyResolverUsage(SyntaxNodeAnalysisContext context) + { + var identifier = (context.Node as IdentifierNameSyntax); + if (identifier?.Identifier.Text != "DependencyResolver") + return; + + // making sure this is exactly the type of DependencyResolver we want to prevent being used + if (context.SemanticModel.GetTypeInfo(identifier).Type.ToDisplayString() == "System.Web.Mvc.DependencyResolver") + { + context.ReportDiagnostic(Diagnostic.Create(Descriptor, context.Node.GetLocation())); + } + } + + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterSyntaxNodeAction(DependencyResolverUsageAction, SyntaxKind.IdentifierName); + } + + } +} diff --git a/src/Agoda.Analyzers/AgodaCustom/CustomRulesResources.Designer.cs b/src/Agoda.Analyzers/AgodaCustom/CustomRulesResources.Designer.cs new file mode 100644 index 0000000..41c231f --- /dev/null +++ b/src/Agoda.Analyzers/AgodaCustom/CustomRulesResources.Designer.cs @@ -0,0 +1,91 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Agoda.Analyzers.AgodaCustom { + using System; + using System.Reflection; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class CustomRulesResources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal CustomRulesResources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Agoda.Analyzers.AgodaCustom.CustomRulesResources", typeof(CustomRulesResources).GetTypeInfo().Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Access dependencies in a resolver-agnostic way. + /// + internal static string AG0001Description { + get { + return ResourceManager.GetString("AG0001Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Access dependencies in a resolver-agnostic way. + /// + internal static string AG0001MessageFormat { + get { + return ResourceManager.GetString("AG0001MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Do not use DependencyResolver directly. + /// + internal static string AG0001Title { + get { + return ResourceManager.GetString("AG0001Title", resourceCulture); + } + } + } +} diff --git a/src/Agoda.Analyzers/AgodaCustom/CustomRulesResources.resx b/src/Agoda.Analyzers/AgodaCustom/CustomRulesResources.resx new file mode 100644 index 0000000..24830d8 --- /dev/null +++ b/src/Agoda.Analyzers/AgodaCustom/CustomRulesResources.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Access dependencies in a resolver-agnostic way + + + Access dependencies in a resolver-agnostic way + + + Do not use DependencyResolver directly + + \ No newline at end of file diff --git a/src/Agoda.Analyzers/AnalyzerCategory.cs b/src/Agoda.Analyzers/AnalyzerCategory.cs new file mode 100644 index 0000000..51cdbad --- /dev/null +++ b/src/Agoda.Analyzers/AnalyzerCategory.cs @@ -0,0 +1,56 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +namespace Agoda.Analyzers +{ + /// + /// Class defining the analyzer category constants. + /// + internal static class AnalyzerCategory + { + /// + /// Category definition for Agoda custom code quality rules + /// + internal const string CustomQualityRules = "Agoda.CSharp.CustomQualityRules"; + + /// + /// Category definition for documentation rules. + /// + internal const string DocumentationRules = "StyleCop.CSharp.DocumentationRules"; + + /// + /// Category definition for layout rules. + /// + internal const string LayoutRules = "StyleCop.CSharp.LayoutRules"; + + /// + /// Category definition for maintainability rules. + /// + internal const string MaintainabilityRules = "StyleCop.CSharp.MaintainabilityRules"; + + /// + /// Category definition for naming rules. + /// + internal const string NamingRules = "StyleCop.CSharp.NamingRules"; + + /// + /// Category definition for ordering rules. + /// + internal const string OrderingRules = "StyleCop.CSharp.OrderingRules"; + + /// + /// Category definition for readability rules. + /// + internal const string ReadabilityRules = "StyleCop.CSharp.ReadabilityRules"; + + /// + /// Category definition for spacing rules. + /// + internal const string SpacingRules = "StyleCop.CSharp.SpacingRules"; + + /// + /// Category definition for special purpose rules. + /// + internal const string SpecialRules = "StyleCop.CSharp.SpecialRules"; + } +} diff --git a/src/Agoda.Analyzers/AnalyzerConstants.cs b/src/Agoda.Analyzers/AnalyzerConstants.cs new file mode 100644 index 0000000..9e134f6 --- /dev/null +++ b/src/Agoda.Analyzers/AnalyzerConstants.cs @@ -0,0 +1,75 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +namespace StyleCop.Analyzers +{ + using System.Diagnostics.CodeAnalysis; + using Microsoft.CodeAnalysis; + + internal static class AnalyzerConstants + { + static AnalyzerConstants() + { +#if DEBUG + // In DEBUG builds, the tests are enabled to simplify development and testing. + DisabledNoTests = true; +#else + DisabledNoTests = false; +#endif + } + + /// + /// Gets a reference value which can be passed to + /// + /// to disable a diagnostic which is currently untested. + /// + /// + /// A reference value which can be passed to + /// + /// to disable a diagnostic which is currently untested. + /// + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1623:Property summary documentation must match accessors.", Justification = "This property behaves more like an opaque value than a Boolean.")] + internal static bool DisabledNoTests { get; } + + /// + /// Gets a reference value which can be passed to + /// + /// to indicate that the diagnostic is disabled by default because it is an alternative to a reference StyleCop + /// rule. + /// + /// + /// A reference value which can be passed to + /// + /// to indicate that the diagnostic is disabled by default because it is an alternative to a reference StyleCop + /// rule. + /// + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1623:Property summary documentation must match accessors.", Justification = "This property behaves more like an opaque value than a Boolean.")] + internal static bool DisabledAlternative => false; + + /// + /// Gets a reference value which can be passed to + /// + /// to indicate that the diagnostic should be enabled by default. + /// + /// + /// A reference value which can be passed to + /// + /// to indicate that the diagnostic should be enabled by default. + /// + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1623:Property summary documentation must match accessors.", Justification = "This property behaves more like an opaque value than a Boolean.")] + internal static bool EnabledByDefault => true; + + /// + /// Gets a reference value which can be passed to + /// + /// to indicate that the diagnostic should be disabled by default. + /// + /// + /// A reference value which can be passed to + /// + /// to indicate that the diagnostic should be disabled by default. + /// + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1623:Property summary documentation must match accessors.", Justification = "This property behaves more like an opaque value than a Boolean.")] + internal static bool DisabledByDefault => false; + } +} diff --git a/src/Agoda.Analyzers/Helpers/HelpersResources.Designer.cs b/src/Agoda.Analyzers/Helpers/HelpersResources.Designer.cs new file mode 100644 index 0000000..8c546a1 --- /dev/null +++ b/src/Agoda.Analyzers/Helpers/HelpersResources.Designer.cs @@ -0,0 +1,91 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Agoda.Analyzers.Helpers { + using System; + using System.Reflection; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class HelpersResources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal HelpersResources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Agoda.Analyzers.Helpers.HelpersResources", typeof(HelpersResources).GetTypeInfo().Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Fix all '{0}'. + /// + public static string FixAllOccurrencesOfDiagnostic { + get { + return ResourceManager.GetString("FixAllOccurrencesOfDiagnostic", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Fix all '{0}' in '{1}'. + /// + public static string FixAllOccurrencesOfDiagnosticInScope { + get { + return ResourceManager.GetString("FixAllOccurrencesOfDiagnosticInScope", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Fix all '{0}' in Solution. + /// + public static string FixAllOccurrencesOfDiagnosticInSolution { + get { + return ResourceManager.GetString("FixAllOccurrencesOfDiagnosticInSolution", resourceCulture); + } + } + } +} diff --git a/src/Agoda.Analyzers/Helpers/HelpersResources.resx b/src/Agoda.Analyzers/Helpers/HelpersResources.resx new file mode 100644 index 0000000..82893fb --- /dev/null +++ b/src/Agoda.Analyzers/Helpers/HelpersResources.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Fix all '{0}' + + + Fix all '{0}' in '{1}' + + + Fix all '{0}' in Solution + + \ No newline at end of file diff --git a/src/Agoda.Analyzers/Helpers/LocationHelpers.cs b/src/Agoda.Analyzers/Helpers/LocationHelpers.cs new file mode 100644 index 0000000..0ce1121 --- /dev/null +++ b/src/Agoda.Analyzers/Helpers/LocationHelpers.cs @@ -0,0 +1,139 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using Microsoft.CodeAnalysis; + +namespace Agoda.Analyzers.Helpers +{ + /// + /// Provides helper methods for working with source file locations. + /// + internal static class LocationHelpers + { + /// + /// Gets the location in terms of path, line and column for a given token. + /// + /// The token to use. + /// The location in terms of path, line and column for a given token. + internal static FileLinePositionSpan GetLineSpan(this SyntaxToken token) + { + return token.SyntaxTree.GetLineSpan(token.Span); + } + + /// + /// Gets the location in terms of path, line and column for a given node. + /// + /// The node to use. + /// The location in terms of path, line and column for a given node. + internal static FileLinePositionSpan GetLineSpan(this SyntaxNode node) + { + return node.SyntaxTree.GetLineSpan(node.Span); + } + + /// + /// Gets the location in terms of path, line and column for a given trivia. + /// + /// The trivia to use. + /// The location in terms of path, line and column for a given trivia. + internal static FileLinePositionSpan GetLineSpan(this SyntaxTrivia trivia) + { + return trivia.SyntaxTree.GetLineSpan(trivia.Span); + } + + /// + /// Gets the location in terms of path, line and column for a given node or token. + /// + /// The trivia to use. + /// The location in terms of path, line and column for a given node or token. + internal static FileLinePositionSpan GetLineSpan(this SyntaxNodeOrToken nodeOrToken) + { + return nodeOrToken.SyntaxTree.GetLineSpan(nodeOrToken.Span); + } + + /// + /// Gets the line on which the given token occurs. + /// + /// The token to use. + /// The line on which the given token occurs. + internal static int GetLine(this SyntaxToken token) + { + return token.GetLineSpan().StartLinePosition.Line; + } + + /// + /// Gets the line on which the given node occurs. + /// + /// The node to use. + /// The line on which the given node occurs. + internal static int GetLine(this SyntaxNode node) + { + return node.GetLineSpan().StartLinePosition.Line; + } + + /// + /// Gets the line on which the given trivia occurs. + /// + /// The trivia to use. + /// The line on which the given trivia occurs. + internal static int GetLine(this SyntaxTrivia trivia) + { + return trivia.GetLineSpan().StartLinePosition.Line; + } + + /// + /// Gets the end line of the given token. + /// + /// The token to use. + /// The line on which the given token ends. + internal static int GetEndLine(this SyntaxToken token) + { + return token.GetLineSpan().EndLinePosition.Line; + } + + /// + /// Gets the end line of the given node. + /// + /// The node to use. + /// The line on which the given node ends. + internal static int GetEndLine(this SyntaxNode node) + { + return node.GetLineSpan().EndLinePosition.Line; + } + + /// + /// Gets the end line of the given trivia. + /// + /// The trivia to use. + /// The line on which the given trivia ends. + internal static int GetEndLine(this SyntaxTrivia trivia) + { + return trivia.GetLineSpan().EndLinePosition.Line; + } + + /// + /// Get a value indicating whether the given node span multiple source text lines. + /// + /// The node to check. + /// True, if the node spans multiple source text lines. + internal static bool SpansMultipleLines(this SyntaxNode node) + { + var lineSpan = node.GetLineSpan(); + + return lineSpan.StartLinePosition.Line < lineSpan.EndLinePosition.Line; + } + + /// + /// Gets a value indicating whether the given trivia span multiple source text lines. + /// + /// The trivia to check. + /// + /// if the trivia spans multiple source text lines; otherwise, . + /// + internal static bool SpansMultipleLines(this SyntaxTrivia trivia) + { + var lineSpan = trivia.GetLineSpan(); + + return lineSpan.StartLinePosition.Line < lineSpan.EndLinePosition.Line; + } + } +} diff --git a/src/Agoda.Analyzers/Helpers/SpecializedTasks.cs b/src/Agoda.Analyzers/Helpers/SpecializedTasks.cs new file mode 100644 index 0000000..95d8b08 --- /dev/null +++ b/src/Agoda.Analyzers/Helpers/SpecializedTasks.cs @@ -0,0 +1,16 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Threading.Tasks; + +namespace Agoda.Analyzers.Helpers +{ + public static class SpecializedTasks + { + public static Task CompletedTask { get; } = Task.FromResult(default(VoidResult)); + + private struct VoidResult + { + } + } +} diff --git a/src/Agoda.Analyzers/Helpers/SyntaxKinds.cs b/src/Agoda.Analyzers/Helpers/SyntaxKinds.cs new file mode 100644 index 0000000..1c693de --- /dev/null +++ b/src/Agoda.Analyzers/Helpers/SyntaxKinds.cs @@ -0,0 +1,232 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Agoda.Analyzers.Helpers +{ + internal static class SyntaxKinds + { + /// + /// Gets a collection of values which appear in the syntax tree as a + /// . + /// + /// + /// A collection of values which appear in the syntax tree as a + /// . + /// + public static ImmutableArray BaseTypeDeclaration { get; } = + ImmutableArray.Create( + SyntaxKind.ClassDeclaration, + SyntaxKind.StructDeclaration, + SyntaxKind.InterfaceDeclaration, + SyntaxKind.EnumDeclaration); + + /// + /// Gets a collection of values which appear in the syntax tree as a + /// . + /// + /// + /// A collection of values which appear in the syntax tree as a + /// . + /// + public static ImmutableArray TypeDeclaration { get; } = + ImmutableArray.Create( + SyntaxKind.ClassDeclaration, + SyntaxKind.StructDeclaration, + SyntaxKind.InterfaceDeclaration); + + /// + /// Gets a collection of values which appear in the syntax tree as a + /// . + /// + /// + /// A collection of values which appear in the syntax tree as a + /// . + /// + public static ImmutableArray BaseFieldDeclaration { get; } = + ImmutableArray.Create( + SyntaxKind.FieldDeclaration, + SyntaxKind.EventFieldDeclaration); + + /// + /// Gets a collection of values which appear in the syntax tree as a + /// . + /// + /// + /// A collection of values which appear in the syntax tree as a + /// . + /// + public static ImmutableArray BaseMethodDeclaration { get; } = + ImmutableArray.Create( + SyntaxKind.MethodDeclaration, + SyntaxKind.ConstructorDeclaration, + SyntaxKind.DestructorDeclaration, + SyntaxKind.OperatorDeclaration, + SyntaxKind.ConversionOperatorDeclaration); + + /// + /// Gets a collection of values which appear in the syntax tree as a + /// . + /// + /// + /// A collection of values which appear in the syntax tree as a + /// . + /// + public static ImmutableArray BasePropertyDeclaration { get; } = + ImmutableArray.Create( + SyntaxKind.PropertyDeclaration, + SyntaxKind.EventDeclaration, + SyntaxKind.IndexerDeclaration); + + /// + /// Gets a collection of values which appear in the syntax tree as an + /// . + /// + /// + /// A collection of values which appear in the syntax tree as an + /// . + /// + public static ImmutableArray AccessorDeclaration { get; } = + ImmutableArray.Create( + SyntaxKind.GetAccessorDeclaration, + SyntaxKind.SetAccessorDeclaration, + SyntaxKind.AddAccessorDeclaration, + SyntaxKind.RemoveAccessorDeclaration, + SyntaxKind.UnknownAccessorDeclaration); + + /// + /// Gets a collection of values which appear in the syntax tree as an + /// . + /// + /// + /// A collection of values which appear in the syntax tree as an + /// . + /// + public static ImmutableArray InitializerExpression { get; } = + ImmutableArray.Create( + SyntaxKind.ArrayInitializerExpression, + SyntaxKind.CollectionInitializerExpression, + SyntaxKind.ComplexElementInitializerExpression, + SyntaxKind.ObjectInitializerExpression); + + /// + /// Gets a collection of values which appear in the syntax tree as a + /// . + /// + /// + /// A collection of values which appear in the syntax tree as a + /// . + /// + public static ImmutableArray DocumentationComment { get; } = + ImmutableArray.Create( + SyntaxKind.SingleLineDocumentationCommentTrivia, + SyntaxKind.MultiLineDocumentationCommentTrivia); + + /// + /// Gets a collection of values which appear in the syntax tree as a + /// . + /// + /// + /// A collection of values which appear in the syntax tree as a + /// . + /// + public static ImmutableArray ConstructorInitializer { get; } = + ImmutableArray.Create( + SyntaxKind.BaseConstructorInitializer, + SyntaxKind.ThisConstructorInitializer); + + /// + /// Gets a collection of values which appear in the syntax tree as a + /// . + /// + /// + /// A collection of values which appear in the syntax tree as a + /// . + /// + public static ImmutableArray LambdaExpression { get; } = + ImmutableArray.Create( + SyntaxKind.ParenthesizedLambdaExpression, + SyntaxKind.SimpleLambdaExpression); + + /// + /// Gets a collection of values which appear in the syntax tree as an + /// . + /// + /// + /// A collection of values which appear in the syntax tree as an + /// . + /// + public static ImmutableArray AnonymousFunctionExpression { get; } = + ImmutableArray.Create( + SyntaxKind.ParenthesizedLambdaExpression, + SyntaxKind.SimpleLambdaExpression, + SyntaxKind.AnonymousMethodExpression); + + /// + /// Gets a collection of values which appear in the syntax tree as a + /// . + /// + /// + /// A collection of values which appear in the syntax tree as a + /// . + /// + public static ImmutableArray SimpleName { get; } = + ImmutableArray.Create( + SyntaxKind.GenericName, + SyntaxKind.IdentifierName); + + /// + /// Gets a collection of values which appear in the syntax tree as a + /// . + /// + /// + /// A collection of values which appear in the syntax tree as a + /// . + /// + public static ImmutableArray BaseParameterList { get; } = + ImmutableArray.Create( + SyntaxKind.ParameterList, + SyntaxKind.BracketedParameterList); + + /// + /// Gets a collection of values which appear in the syntax tree as a + /// . + /// + /// + /// A collection of values which appear in the syntax tree as a + /// . + /// + public static ImmutableArray BaseArgumentList { get; } = + ImmutableArray.Create( + SyntaxKind.ArgumentList, + SyntaxKind.BracketedArgumentList); + + /// + /// Gets a collection of values which represent keywords of integer literals. + /// + /// + /// A collection of values which represent keywords of integer literals. + /// + public static ImmutableArray IntegerLiteralKeyword { get; } = + ImmutableArray.Create( + SyntaxKind.IntKeyword, + SyntaxKind.LongKeyword, + SyntaxKind.ULongKeyword, + SyntaxKind.UIntKeyword); + + /// + /// Gets a collection of values which represent keywords of real literals. + /// + /// + /// A collection of values which represent keywords of real literals. + /// + public static ImmutableArray RealLiteralKeyword { get; } = + ImmutableArray.Create( + SyntaxKind.FloatKeyword, + SyntaxKind.DoubleKeyword, + SyntaxKind.DecimalKeyword); + } +} diff --git a/src/Agoda.Analyzers/Helpers/TriviaHelper.cs b/src/Agoda.Analyzers/Helpers/TriviaHelper.cs new file mode 100644 index 0000000..db868d0 --- /dev/null +++ b/src/Agoda.Analyzers/Helpers/TriviaHelper.cs @@ -0,0 +1,603 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace Agoda.Analyzers.Helpers +{ + /// + /// Provides helper methods to work with trivia (lists). + /// + public static class TriviaHelper + { + /// + /// Returns the index of the first non-whitespace trivia in the given trivia list. + /// + /// The trivia list to process. + /// to treat + /// as whitespace; otherwise, . + /// The type of the trivia list. + /// The index where the non-whitespace starts, or -1 if there is no non-whitespace trivia. + internal static int IndexOfFirstNonWhitespaceTrivia(T triviaList, bool endOfLineIsWhitespace = true) + where T : IReadOnlyList + { + for (var index = 0; index < triviaList.Count; index++) + { + var currentTrivia = triviaList[index]; + switch (currentTrivia.Kind()) + { + case SyntaxKind.EndOfLineTrivia: + if (!endOfLineIsWhitespace) + { + return index; + } + + break; + + case SyntaxKind.WhitespaceTrivia: + break; + + default: + // encountered non-whitespace trivia -> the search is done. + return index; + } + } + + return -1; + } + + /// + /// Returns the index of the first trivia that is not part of a blank line. + /// + /// The trivia list to process. + /// The type of the trivia list. + /// The index of the first trivia that is not part of a blank line, or -1 if there is no such trivia. + internal static int IndexOfFirstNonBlankLineTrivia(T triviaList) + where T : IReadOnlyList + { + var firstNonWhitespaceTriviaIndex = IndexOfFirstNonWhitespaceTrivia(triviaList); + var startIndex = (firstNonWhitespaceTriviaIndex == -1) ? triviaList.Count : firstNonWhitespaceTriviaIndex; + + for (var index = startIndex - 1; index >= 0; index--) + { + // Find an end-of-line trivia, to indicate that there actually are blank lines and not just excess whitespace. + if (triviaList[index].IsKind(SyntaxKind.EndOfLineTrivia)) + { + return index == (triviaList.Count - 1) ? -1 : index + 1; + } + } + + return 0; + } + + /// + /// Returns the index into the trivia list where the trailing whitespace starts. + /// + /// The trivia list to process. + /// The type of the trivia list. + /// The index where the trailing whitespace starts, or -1 if there is no trailing whitespace. + public static int IndexOfTrailingWhitespace(T triviaList) + where T : IReadOnlyList + { + var done = false; + int whiteSpaceStartIndex = -1; + var previousTriviaWasEndOfLine = false; + + for (var index = triviaList.Count - 1; !done && (index >= 0); index--) + { + var currentTrivia = triviaList[index]; + switch (currentTrivia.Kind()) + { + case SyntaxKind.EndOfLineTrivia: + whiteSpaceStartIndex = index; + previousTriviaWasEndOfLine = true; + break; + + case SyntaxKind.WhitespaceTrivia: + whiteSpaceStartIndex = index; + previousTriviaWasEndOfLine = false; + break; + + default: + // encountered non-whitespace trivia -> the search is done. + if (previousTriviaWasEndOfLine) + { + whiteSpaceStartIndex++; + } + + done = true; + break; + } + } + + return (whiteSpaceStartIndex < triviaList.Count) ? whiteSpaceStartIndex : -1; + } + + /// + /// Removes a range of elements from the . + /// + /// The list to remove elements from. + /// The zero-based starting index of the range of elements to remove. + /// The number of elements to remove. + /// A copy of with the specified range of elements removed. + /// + /// If is less than 0. + /// -or- + /// If is less than 0. + /// + /// + /// If and do not denote a valid range of elements in + /// the . + /// + internal static SyntaxTriviaList RemoveRange(this SyntaxTriviaList list, int index, int count) + { + if (index < 0) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + if (index > list.Count - count) + { + throw new ArgumentException("The specified range of elements does not exist in the list."); + } + + SyntaxTrivia[] trivia = new SyntaxTrivia[list.Count - count]; + for (int i = 0; i < index; i++) + { + trivia[i] = list[i]; + } + + for (int i = index; i + count < list.Count; i++) + { + trivia[i] = list[i + count]; + } + + return SyntaxFactory.TriviaList(trivia); + } + + internal static SyntaxTriviaList WithoutDirectiveTrivia(this SyntaxTriviaList triviaList) + { + var resultTriviaList = new List(triviaList.Count); + foreach (var trivia in triviaList) + { + if (!trivia.IsDirective) + { + resultTriviaList.Add(trivia); + } + } + + return SyntaxFactory.TriviaList(resultTriviaList); + } + + /// + /// Returns the index of the last trivia of a specified kind in the trivia list. + /// + /// The trivia list. + /// The syntax kind to find. + /// + /// The non-negative index of the last trivia which matches . + /// -or- + /// -1, if the list did not contain any matching trivia. + /// + internal static int LastIndexOf(this SyntaxTriviaList list, SyntaxKind kind) + { + for (int i = list.Count - 1; i >= 0; i--) + { + if (list[i].IsKind(kind)) + { + return i; + } + } + + return -1; + } + + /// + /// Strips all trailing whitespace trivia from the trivia list until a non-whitespace trivia is encountered. + /// + /// The trivia list to strip of its trailing whitespace. + /// The modified triviaList. + internal static SyntaxTriviaList WithoutTrailingWhitespace(this SyntaxTriviaList triviaList) + { + var trailingWhitespaceIndex = IndexOfTrailingWhitespace(triviaList); + return (trailingWhitespaceIndex >= 0) ? SyntaxFactory.TriviaList(triviaList.Take(trailingWhitespaceIndex)) : triviaList; + } + + /// + /// Strips all leading whitespace trivia from the trivia list until a non-whitespace trivia is encountered. + /// + /// The trivia list to strip of its leading whitespace. + /// to treat + /// as whitespace; otherwise, . + /// The modified triviaList. + internal static SyntaxTriviaList WithoutLeadingWhitespace(this SyntaxTriviaList triviaList, bool endOfLineIsWhitespace = true) + { + var nonWhitespaceIndex = IndexOfFirstNonWhitespaceTrivia(triviaList, endOfLineIsWhitespace); + return (nonWhitespaceIndex >= 0) ? SyntaxFactory.TriviaList(triviaList.Skip(nonWhitespaceIndex)) : SyntaxFactory.TriviaList(); + } + + /// + /// + /// Builds a trivia list that contains the given trivia. + /// + /// + /// This method combines the trailing and leading trivia of the tokens between which the given trivia is defined. + /// + /// + /// The trivia to create the list from. + /// The index of the trivia in the created trivia list. + /// The created trivia list. + internal static DualTriviaListHelper GetContainingTriviaList(SyntaxTrivia trivia, out int triviaIndex) + { + var token = trivia.Token; + SyntaxTriviaList part1; + SyntaxTriviaList part2; + + triviaIndex = BinarySearch(token.TrailingTrivia, trivia); + if (triviaIndex != -1) + { + var nextToken = token.GetNextToken(includeZeroWidth: true); + + part1 = token.TrailingTrivia; + part2 = nextToken.LeadingTrivia; + } + else + { + var prevToken = token.GetPreviousToken(); + triviaIndex = prevToken.TrailingTrivia.Count + BinarySearch(token.LeadingTrivia, trivia); + + part1 = prevToken.TrailingTrivia; + part2 = token.LeadingTrivia; + } + + return new DualTriviaListHelper(part1, part2); + } + + /// + /// Merges the given trivia lists into a new single trivia list. + /// + /// The first part of the new list. + /// The second part of the new list. + /// The merged trivia list. + internal static DualTriviaListHelper MergeTriviaLists(SyntaxTriviaList list1, SyntaxTriviaList list2) + { + return new DualTriviaListHelper(list1, list2); + } + + /// + /// Determines if the given token is immediately preceded by blank lines. Leading whitespace on the same line as + /// the token is ignored. + /// + /// The token to check for immediately preceding blank lines. + /// + /// if the token is immediately preceded by blank lines; otherwise, . + /// + internal static bool IsPrecededByBlankLines(this SyntaxToken token) + { + if (!token.HasLeadingTrivia) + { + return false; + } + + var triviaList = token.LeadingTrivia; + + // skip any leading whitespace + var index = triviaList.Count - 1; + while ((index >= 0) && triviaList[index].IsKind(SyntaxKind.WhitespaceTrivia)) + { + index--; + } + + if ((index < 0) || !triviaList[index].HasBuiltinEndLine()) + { + return false; + } + + var blankLineCount = -1; + while (index >= 0) + { + if (triviaList[index].HasBuiltinEndLine() && !triviaList[index].IsKind(SyntaxKind.EndOfLineTrivia)) + { + blankLineCount++; + return blankLineCount > 0; + } + + switch (triviaList[index].Kind()) + { + case SyntaxKind.WhitespaceTrivia: + // ignore; + break; + + case SyntaxKind.EndOfLineTrivia: + blankLineCount++; + break; + + default: + return blankLineCount > 0; + } + + index--; + } + + return true; + } + + /// + /// Strips all leading blank lines from the given token. + /// + /// The token to strip. + /// A new token without leading blank lines. + internal static SyntaxToken WithoutLeadingBlankLines(this SyntaxToken token) + { + var triviaList = token.LeadingTrivia; + var leadingWhitespaceStart = triviaList.Count - 1; + + // skip leading whitespace in front of the while keyword + while ((leadingWhitespaceStart > 0) && triviaList[leadingWhitespaceStart - 1].IsKind(SyntaxKind.WhitespaceTrivia)) + { + leadingWhitespaceStart--; + } + + var blankLinesStart = leadingWhitespaceStart - 1; + var done = false; + while (!done && (blankLinesStart >= 0)) + { + switch (triviaList[blankLinesStart].Kind()) + { + case SyntaxKind.WhitespaceTrivia: + case SyntaxKind.EndOfLineTrivia: + blankLinesStart--; + break; + + case SyntaxKind.IfDirectiveTrivia: + case SyntaxKind.ElifDirectiveTrivia: + case SyntaxKind.ElseDirectiveTrivia: + case SyntaxKind.EndIfDirectiveTrivia: + // directives include an embedded end of line + blankLinesStart++; + done = true; + break; + + default: + // include the first end of line (as it is part of the non blank line trivia) + while (!triviaList[blankLinesStart].HasBuiltinEndLine()) + { + blankLinesStart++; + } + + blankLinesStart++; + done = true; + break; + } + } + + var newLeadingTrivia = SyntaxFactory.TriviaList(triviaList.Take(blankLinesStart).Concat(triviaList.Skip(leadingWhitespaceStart))); + return token.WithLeadingTrivia(newLeadingTrivia); + } + + internal static bool HasBuiltinEndLine(this SyntaxTrivia trivia) + { + return trivia.IsDirective + || trivia.IsKind(SyntaxKind.SingleLineDocumentationCommentTrivia) + || trivia.IsKind(SyntaxKind.EndOfLineTrivia); + } + + /// + /// Checks whether the given trivia list contains one or more blank lines. + /// + /// The trivia list to check. + /// Indicates if the trivia list starts on a new line. + /// True if the given trivia list contains one or more blank lines. + internal static bool ContainsBlankLines(this IReadOnlyList triviaList, bool startsOnNewLine = true) + { + bool onBlankLine = startsOnNewLine; + + foreach (var trivia in triviaList) + { + switch (trivia.Kind()) + { + case SyntaxKind.WhitespaceTrivia: + // ignore whitespace + break; + + case SyntaxKind.EndOfLineTrivia: + if (onBlankLine) + { + return true; + } + + onBlankLine = true; + break; + + default: + // directive trivia have a builtin end-of-line. + onBlankLine = trivia.IsDirective; + break; + } + } + + return false; + } + + /// + /// Removes all blank lines from the given trivia list. + /// + /// The trivia list to process. + /// Indicates if the trivia list starts on a new line. + /// A new that is a copy of the passed without blank lines. + internal static SyntaxTriviaList WithoutBlankLines(this SyntaxTriviaList triviaList, bool startsOnNewLine = true) + { + bool onBlankLine = startsOnNewLine; + var newTriviaList = new List(); + + for (var i = 0; i < triviaList.Count; i++) + { + var trivia = triviaList[i]; + + switch (trivia.Kind()) + { + case SyntaxKind.WhitespaceTrivia: + newTriviaList.Add(trivia); + break; + + case SyntaxKind.EndOfLineTrivia: + if (onBlankLine) + { + // strip all preceding white space in the blank line. + while ((newTriviaList.Count > 0) && newTriviaList[newTriviaList.Count - 1].IsKind(SyntaxKind.WhitespaceTrivia)) + { + newTriviaList.RemoveAt(newTriviaList.Count - 1); + } + } + else + { + newTriviaList.Add(trivia); + onBlankLine = true; + } + + break; + + default: + newTriviaList.Add(trivia); + + // directive trivia have a builtin end-of-line. + onBlankLine = trivia.IsDirective; + break; + } + } + + return SyntaxFactory.TriviaList(newTriviaList); + } + + private static int BinarySearch(SyntaxTriviaList leadingTrivia, SyntaxTrivia trivia) + { + int low = 0; + int high = leadingTrivia.Count - 1; + while (low <= high) + { + int index = low + ((high - low) >> 1); + int order = leadingTrivia[index].Span.CompareTo(trivia.Span); + + if (order == 0) + { + return index; + } + + if (order < 0) + { + low = index + 1; + } + else + { + high = index - 1; + } + } + + // Entry was not found + return -1; + } + + /// + /// Helper class that merges two SyntaxTriviaLists with (hopefully) the lowest possible performance penalty. + /// + internal struct DualTriviaListHelper : IReadOnlyList + { + private readonly SyntaxTriviaList part1; + private readonly int part1Count; + private readonly SyntaxTriviaList part2; + + public DualTriviaListHelper(SyntaxTriviaList part1, SyntaxTriviaList part2) + { + this.part1 = part1; + this.part2 = part2; + this.part1Count = part1.Count; + this.Count = part1.Count + part2.Count; + } + + public int Count { get; } + + public SyntaxTrivia this[int index] + { + get + { + if (index < this.part1Count) + { + return this.part1[index]; + } + else if (index < this.Count) + { + return this.part2[index - this.part1Count]; + } + else + { + throw new IndexOutOfRangeException(); + } + } + } + + public IEnumerator GetEnumerator() + { + foreach (var item in this.part1) + { + yield return item; + } + + foreach (var item in this.part2) + { + yield return item; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + + public SyntaxTrivia First() + { + return this[0]; + } + + public SyntaxTrivia Last() + { + return this[this.Count - 1]; + } + + public bool Any(SyntaxKind kind) + { + return this.part1.Any(kind) || this.part2.Any(kind); + } + + public bool All(Func predicate) + { + foreach (var trivia in this.part1) + { + if (!predicate(trivia)) + { + return false; + } + } + + foreach (var trivia in this.part2) + { + if (!predicate(trivia)) + { + return false; + } + } + + return true; + } + } + } +} diff --git a/src/Agoda.Analyzers/Properties/AssemblyInfo.cs b/src/Agoda.Analyzers/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..2ff1f70 --- /dev/null +++ b/src/Agoda.Analyzers/Properties/AssemblyInfo.cs @@ -0,0 +1,30 @@ +using System.Resources; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Agoda.Analyzers")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Agoda.Analyzers")] +[assembly: AssemblyCopyright("Copyright © 2017")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: NeutralResourcesLanguage("en")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Agoda.Analyzers/StyleCop/ReadabilityResources.Designer.cs b/src/Agoda.Analyzers/StyleCop/ReadabilityResources.Designer.cs new file mode 100644 index 0000000..a63a1e0 --- /dev/null +++ b/src/Agoda.Analyzers/StyleCop/ReadabilityResources.Designer.cs @@ -0,0 +1,1315 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Agoda.Analyzers.StyleCop { + using System; + using System.Reflection; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class ReadabilityResources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal ReadabilityResources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Agoda.Analyzers.StyleCop.ReadabilityResources", typeof(ReadabilityResources).GetTypeInfo().Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Fix indentation. + /// + internal static string IndentationCodeFix { + get { + return ResourceManager.GetString("IndentationCodeFix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remove region. + /// + internal static string RemoveRegionCodeFix { + get { + return ResourceManager.GetString("RemoveRegionCodeFix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Replace 'base.' with 'this.'. + /// + internal static string SA1100CodeFix { + get { + return ResourceManager.GetString("SA1100CodeFix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A call to a member from an inherited class begins with 'base.', and the local class does not contain an override or implementation of the member.. + /// + internal static string SA1100Description { + get { + return ResourceManager.GetString("SA1100Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Do not prefix calls with base unless local implementation exists. + /// + internal static string SA1100MessageFormat { + get { + return ResourceManager.GetString("SA1100MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Do not prefix calls with base unless local implementation exists. + /// + internal static string SA1100Title { + get { + return ResourceManager.GetString("SA1100Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Prefix reference with 'this.'. + /// + internal static string SA1101CodeFix { + get { + return ResourceManager.GetString("SA1101CodeFix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A call to an instance member of the local class or a base class is not prefixed with 'this.', within a C# code file.. + /// + internal static string SA1101Description { + get { + return ResourceManager.GetString("SA1101Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Prefix local calls with this. + /// + internal static string SA1101MessageFormat { + get { + return ResourceManager.GetString("SA1101MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Prefix local calls with this. + /// + internal static string SA1101Title { + get { + return ResourceManager.GetString("SA1101Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remove separating lines. + /// + internal static string SA1102CodeFix { + get { + return ResourceManager.GetString("SA1102CodeFix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A C# query clause does not begin on the same line as the previous clause, or on the next line.. + /// + internal static string SA1102Description { + get { + return ResourceManager.GetString("SA1102Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Query clause must follow previous clause.. + /// + internal static string SA1102MessageFormat { + get { + return ResourceManager.GetString("SA1102MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Query clause must follow previous clause. + /// + internal static string SA1102Title { + get { + return ResourceManager.GetString("SA1102Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Place on multiple lines. + /// + internal static string SA1103CodeFixMultipleLines { + get { + return ResourceManager.GetString("SA1103CodeFixMultipleLines", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Place on single line. + /// + internal static string SA1103CodeFixSingleLine { + get { + return ResourceManager.GetString("SA1103CodeFixSingleLine", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The clauses within a C# query expression are not all placed on the same line, and each clause is not placed on its own line.. + /// + internal static string SA1103Description { + get { + return ResourceManager.GetString("SA1103Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Query clauses must be on separate lines or all on one line. + /// + internal static string SA1103MessageFormat { + get { + return ResourceManager.GetString("SA1103MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Query clauses must be on separate lines or all on one line. + /// + internal static string SA1103Title { + get { + return ResourceManager.GetString("SA1103Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A clause within a C# query expression begins on the same line as the previous clause, when the previous clause spans across multiple lines.. + /// + internal static string SA1104Description { + get { + return ResourceManager.GetString("SA1104Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Query clause must begin on new line when previous clause spans multiple lines. + /// + internal static string SA1104MessageFormat { + get { + return ResourceManager.GetString("SA1104MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Insert new line. + /// + internal static string SA1104SA1105CodeFix { + get { + return ResourceManager.GetString("SA1104SA1105CodeFix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Query clause must begin on new line when previous clause spans multiple lines. + /// + internal static string SA1104Title { + get { + return ResourceManager.GetString("SA1104Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A clause within a C# query expression spans across multiple lines, and does not begin on its own line.. + /// + internal static string SA1105Description { + get { + return ResourceManager.GetString("SA1105Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Query clauses spanning multiple lines must begin on own line. + /// + internal static string SA1105MessageFormat { + get { + return ResourceManager.GetString("SA1105MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Query clauses spanning multiple lines must begin on own line. + /// + internal static string SA1105Title { + get { + return ResourceManager.GetString("SA1105Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remove empty statement. + /// + internal static string SA1106CodeFix { + get { + return ResourceManager.GetString("SA1106CodeFix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The C# code contains an extra semicolon.. + /// + internal static string SA1106Description { + get { + return ResourceManager.GetString("SA1106Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Code must not contain empty statements. + /// + internal static string SA1106MessageFormat { + get { + return ResourceManager.GetString("SA1106MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Code must not contain empty statements. + /// + internal static string SA1106Title { + get { + return ResourceManager.GetString("SA1106Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Enter new line. + /// + internal static string SA1107CodeFix { + get { + return ResourceManager.GetString("SA1107CodeFix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The C# code contains more than one statement on a single line.. + /// + internal static string SA1107Description { + get { + return ResourceManager.GetString("SA1107Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Code must not contain multiple statements on one line. + /// + internal static string SA1107MessageFormat { + get { + return ResourceManager.GetString("SA1107MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Code must not contain multiple statements on one line. + /// + internal static string SA1107Title { + get { + return ResourceManager.GetString("SA1107Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A C# statement contains a comment between the declaration of the statement and the opening brace of the statement.. + /// + internal static string SA1108Description { + get { + return ResourceManager.GetString("SA1108Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Block statements must not contain embedded comments. + /// + internal static string SA1108MessageFormat { + get { + return ResourceManager.GetString("SA1108MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Block statements must not contain embedded comments. + /// + internal static string SA1108Title { + get { + return ResourceManager.GetString("SA1108Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A C# statement contains a region tag between the declaration of the statement and the opening brace of the statement.. + /// + internal static string SA1109Description { + get { + return ResourceManager.GetString("SA1109Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to . + /// + internal static string SA1109MessageFormat { + get { + return ResourceManager.GetString("SA1109MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Block statements must not contain embedded regions. + /// + internal static string SA1109Title { + get { + return ResourceManager.GetString("SA1109Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The opening parenthesis or bracket is not placed on the same line as the method/indexer/attribute/array name.. + /// + internal static string SA1110Description { + get { + return ResourceManager.GetString("SA1110Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Opening parenthesis or bracket must be on declaration line.. + /// + internal static string SA1110MessageFormat { + get { + return ResourceManager.GetString("SA1110MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Opening parenthesis or bracket must be on declaration line. + /// + internal static string SA1110Title { + get { + return ResourceManager.GetString("SA1110Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The closing parenthesis or bracket in a call to or declaration of a C# method/indexer/attribute/array/constructor/delegate is not placed on the same line as the last parameter.. + /// + internal static string SA1111Description { + get { + return ResourceManager.GetString("SA1111Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Closing parenthesis must be on line of last parameter. + /// + internal static string SA1111MessageFormat { + get { + return ResourceManager.GetString("SA1111MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Closing parenthesis must be on line of last parameter. + /// + internal static string SA1111Title { + get { + return ResourceManager.GetString("SA1111Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The closing parenthesis or bracket in a call to a C# method or indexer, or the declaration of a method or indexer, is not placed on the same line as the opening bracket when the element does not take any parameters.. + /// + internal static string SA1112Description { + get { + return ResourceManager.GetString("SA1112Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Closing parenthesis must be on line of opening parenthesis. + /// + internal static string SA1112MessageFormat { + get { + return ResourceManager.GetString("SA1112MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Closing parenthesis must be on line of opening parenthesis. + /// + internal static string SA1112Title { + get { + return ResourceManager.GetString("SA1112Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A comma between two parameters in a call to a C# method or indexer, or in the declaration of a method or indexer, is not placed on the same line as the previous parameter.. + /// + internal static string SA1113Description { + get { + return ResourceManager.GetString("SA1113Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Comma must be on the same line as previous parameter.. + /// + internal static string SA1113MessageFormat { + get { + return ResourceManager.GetString("SA1113MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Comma must be on the same line as previous parameter. + /// + internal static string SA1113Title { + get { + return ResourceManager.GetString("SA1113Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The start of the parameter list for a method/constructor/indexer/array/operator call or declaration does not begin on the same line as the opening bracket, or on the line after the opening bracket.. + /// + internal static string SA1114Description { + get { + return ResourceManager.GetString("SA1114Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Parameter list must follow declaration. + /// + internal static string SA1114MessageFormat { + get { + return ResourceManager.GetString("SA1114MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Parameter list must follow declaration. + /// + internal static string SA1114Title { + get { + return ResourceManager.GetString("SA1114Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A parameter within a C# method or indexer call or declaration does not begin on the same line as the previous parameter, or on the next line.. + /// + internal static string SA1115Description { + get { + return ResourceManager.GetString("SA1115Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The parameter must begin on the line after the previous parameter.. + /// + internal static string SA1115MessageFormat { + get { + return ResourceManager.GetString("SA1115MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Parameter must follow comma. + /// + internal static string SA1115Title { + get { + return ResourceManager.GetString("SA1115Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Move first argument to next line. + /// + internal static string SA1116CodeFix { + get { + return ResourceManager.GetString("SA1116CodeFix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The parameters to a C# method or indexer call or declaration span across multiple lines, but the first parameter does not start on the line after the opening bracket.. + /// + internal static string SA1116Description { + get { + return ResourceManager.GetString("SA1116Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The parameters must begin on the line after the declaration, whenever the parameter span across multiple lines. + /// + internal static string SA1116MessageFormat { + get { + return ResourceManager.GetString("SA1116MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Split parameters must start on line after declaration. + /// + internal static string SA1116Title { + get { + return ResourceManager.GetString("SA1116Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The parameters to a C# method or indexer call or declaration are not all on the same line or each on a separate line.. + /// + internal static string SA1117Description { + get { + return ResourceManager.GetString("SA1117Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The parameters must all be placed on the same line or each parameter must be placed on its own line.. + /// + internal static string SA1117MessageFormat { + get { + return ResourceManager.GetString("SA1117MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Parameters must be on same line or separate lines. + /// + internal static string SA1117Title { + get { + return ResourceManager.GetString("SA1117Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A parameter to a C# method/indexer/attribute/array, other than the first parameter, spans across multiple lines. If the parameter is short, place the entire parameter on a single line. Otherwise, save the contents of the parameter in a temporary variable and pass the temporary variable as a parameter.. + /// + internal static string SA1118Description { + get { + return ResourceManager.GetString("SA1118Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The parameter spans multiple lines. + /// + internal static string SA1118MessageFormat { + get { + return ResourceManager.GetString("SA1118MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Parameter must not span multiple lines. + /// + internal static string SA1118Title { + get { + return ResourceManager.GetString("SA1118Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remove empty comment. + /// + internal static string SA1120CodeFix { + get { + return ResourceManager.GetString("SA1120CodeFix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The C# comment does not contain any comment text.. + /// + internal static string SA1120Description { + get { + return ResourceManager.GetString("SA1120Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Comments must contain text. + /// + internal static string SA1120MessageFormat { + get { + return ResourceManager.GetString("SA1120MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Comments must contain text. + /// + internal static string SA1120Title { + get { + return ResourceManager.GetString("SA1120Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Replace with built-in type. + /// + internal static string SA1121CodeFix { + get { + return ResourceManager.GetString("SA1121CodeFix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The code uses one of the basic C# types, but does not use the built-in alias for the type.. + /// + internal static string SA1121Description { + get { + return ResourceManager.GetString("SA1121Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use built-in type alias. + /// + internal static string SA1121MessageFormat { + get { + return ResourceManager.GetString("SA1121MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use built-in type alias. + /// + internal static string SA1121Title { + get { + return ResourceManager.GetString("SA1121Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Replace with string.Empty. + /// + internal static string SA1122CodeFix { + get { + return ResourceManager.GetString("SA1122CodeFix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The C# code includes an empty string, written as "".. + /// + internal static string SA1122Description { + get { + return ResourceManager.GetString("SA1122Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use string.Empty for empty strings. + /// + internal static string SA1122MessageFormat { + get { + return ResourceManager.GetString("SA1122MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use string.Empty for empty strings. + /// + internal static string SA1122Title { + get { + return ResourceManager.GetString("SA1122Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The C# code contains a region within the body of a code element.. + /// + internal static string SA1123Description { + get { + return ResourceManager.GetString("SA1123Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Region must not be located within a code element.. + /// + internal static string SA1123MessageFormat { + get { + return ResourceManager.GetString("SA1123MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Do not place regions within elements. + /// + internal static string SA1123Title { + get { + return ResourceManager.GetString("SA1123Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The C# code contains a region.. + /// + internal static string SA1124Description { + get { + return ResourceManager.GetString("SA1124Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Do not use regions. + /// + internal static string SA1124MessageFormat { + get { + return ResourceManager.GetString("SA1124MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Do not use regions. + /// + internal static string SA1124Title { + get { + return ResourceManager.GetString("SA1124Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The Nullable<T> type has been defined not using the C# shorthand. For example, Nullable<DateTime> has been used instead of the preferred DateTime?. + /// + internal static string SA1125Description { + get { + return ResourceManager.GetString("SA1125Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use shorthand for nullable types. + /// + internal static string SA1125MessageFormat { + get { + return ResourceManager.GetString("SA1125MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use shorthand for nullable types. + /// + internal static string SA1125Title { + get { + return ResourceManager.GetString("SA1125Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A call to a member is not prefixed with the 'this.', 'base.', 'object.' or 'typename.' prefix to indicate the intended method call, within a C# code file.. + /// + internal static string SA1126Description { + get { + return ResourceManager.GetString("SA1126Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to . + /// + internal static string SA1126MessageFormat { + get { + return ResourceManager.GetString("SA1126MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Prefix calls correctly. + /// + internal static string SA1126Title { + get { + return ResourceManager.GetString("SA1126Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Place each type constraint on a new line. + /// + internal static string SA1127CodeFix { + get { + return ResourceManager.GetString("SA1127CodeFix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Each type constraint clause for a generic type parameter should be listed on a line of code by itself.. + /// + internal static string SA1127Description { + get { + return ResourceManager.GetString("SA1127Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Generic type constraints must be on their own line. + /// + internal static string SA1127MessageFormat { + get { + return ResourceManager.GetString("SA1127MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Generic type constraints must be on their own line. + /// + internal static string SA1127Title { + get { + return ResourceManager.GetString("SA1127Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Place constructor initializer on own line. + /// + internal static string SA1128CodeFix { + get { + return ResourceManager.GetString("SA1128CodeFix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A constructor initializer, including the colon character, should be on its own line.. + /// + internal static string SA1128Description { + get { + return ResourceManager.GetString("SA1128Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Put constructor initializers on their own line. + /// + internal static string SA1128MessageFormat { + get { + return ResourceManager.GetString("SA1128MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Put constructor initializers on their own line. + /// + internal static string SA1128Title { + get { + return ResourceManager.GetString("SA1128Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Replace with default(T). + /// + internal static string SA1129CodeFix { + get { + return ResourceManager.GetString("SA1129CodeFix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to When creating a new instance of a value type T, the syntax 'default(T)' is functionally equivalent to the syntax 'new T()'. To avoid confusion regarding the behavior of the resulting instance, the first form is preferred.. + /// + internal static string SA1129Description { + get { + return ResourceManager.GetString("SA1129Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Do not use default value type constructor. + /// + internal static string SA1129MessageFormat { + get { + return ResourceManager.GetString("SA1129MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Do not use default value type constructor. + /// + internal static string SA1129Title { + get { + return ResourceManager.GetString("SA1129Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Replace with lambda.. + /// + internal static string SA1130CodeFix { + get { + return ResourceManager.GetString("SA1130CodeFix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Lambda expressions are more succinct and easier to read than anonymous methods, so they should are preferred whenever the two are functionally equivalent.. + /// + internal static string SA1130Description { + get { + return ResourceManager.GetString("SA1130Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use lambda syntax. + /// + internal static string SA1130MessageFormat { + get { + return ResourceManager.GetString("SA1130MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use lambda syntax. + /// + internal static string SA1130Title { + get { + return ResourceManager.GetString("SA1130Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Swap operands. + /// + internal static string SA1131CodeFix { + get { + return ResourceManager.GetString("SA1131CodeFix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to When a comparison is made between a variable and a literal, the variable should be placed on the left-hand-side to maximize readability.. + /// + internal static string SA1131Description { + get { + return ResourceManager.GetString("SA1131Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Constant values should appear on the right-hand side of comparisons. + /// + internal static string SA1131MessageFormat { + get { + return ResourceManager.GetString("SA1131MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use readable conditions. + /// + internal static string SA1131Title { + get { + return ResourceManager.GetString("SA1131Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Place each field on a new line. + /// + internal static string SA1132CodeFix { + get { + return ResourceManager.GetString("SA1132CodeFix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Each field should be declared on its own line, in order to clearly see each field of a type and allow for proper documentation of the behavior of each field.. + /// + internal static string SA1132Description { + get { + return ResourceManager.GetString("SA1132Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Each field should be declared on its own line. + /// + internal static string SA1132MessageFormat { + get { + return ResourceManager.GetString("SA1132MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Do not combine fields. + /// + internal static string SA1132Title { + get { + return ResourceManager.GetString("SA1132Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Give each attribute its own square brackets. + /// + internal static string SA1133CodeFix { + get { + return ResourceManager.GetString("SA1133CodeFix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Each attribute usage should be placed in its own set of square brackets for maximum readability.. + /// + internal static string SA1133Description { + get { + return ResourceManager.GetString("SA1133Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Each attribute should be placed in its own set of square brackets.. + /// + internal static string SA1133MessageFormat { + get { + return ResourceManager.GetString("SA1133MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Do not combine attributes. + /// + internal static string SA1133Title { + get { + return ResourceManager.GetString("SA1133Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Place attribute on own line.. + /// + internal static string SA1134CodeFix { + get { + return ResourceManager.GetString("SA1134CodeFix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Each attribute should be placed on its own line of code.. + /// + internal static string SA1134Description { + get { + return ResourceManager.GetString("SA1134Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Each attribute should be placed on its own line of code.. + /// + internal static string SA1134MessageFormat { + get { + return ResourceManager.GetString("SA1134MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attributes must not share line. + /// + internal static string SA1134Title { + get { + return ResourceManager.GetString("SA1134Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Place enum values own their own lines. + /// + internal static string SA1136CodeFix { + get { + return ResourceManager.GetString("SA1136CodeFix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Enum values should be placed on their own lines for maximum readability.. + /// + internal static string SA1136Description { + get { + return ResourceManager.GetString("SA1136Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Enum values should be on separate lines. + /// + internal static string SA1136MessageFormat { + get { + return ResourceManager.GetString("SA1136MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Enum values should be on separate lines. + /// + internal static string SA1136Title { + get { + return ResourceManager.GetString("SA1136Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Elements at the same level in the syntax tree should have the same indentation.. + /// + internal static string SA1137Description { + get { + return ResourceManager.GetString("SA1137Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Elements should have the same indentation. + /// + internal static string SA1137MessageFormat { + get { + return ResourceManager.GetString("SA1137MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Elements should have the same indentation. + /// + internal static string SA1137Title { + get { + return ResourceManager.GetString("SA1137Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use literal suffix notation instead of casting. + /// + internal static string SA1139CodeFix { + get { + return ResourceManager.GetString("SA1139CodeFix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use literal suffix notation instead of casting, in order to improve readability, avoid bugs related to illegal casts and ensure that optimal IL is produced.. + /// + internal static string SA1139Description { + get { + return ResourceManager.GetString("SA1139Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use literal suffix notation instead of casting. + /// + internal static string SA1139MessageFormat { + get { + return ResourceManager.GetString("SA1139MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use literal suffix notation instead of casting. + /// + internal static string SA1139Title { + get { + return ResourceManager.GetString("SA1139Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remove 'this.' prefix. + /// + internal static string SX1101CodeFix { + get { + return ResourceManager.GetString("SX1101CodeFix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A call to an instance member of the local class or a base class is prefixed with `this.`.. + /// + internal static string SX1101Description { + get { + return ResourceManager.GetString("SX1101Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Do not prefix local calls with 'this.'. + /// + internal static string SX1101MessageFormat { + get { + return ResourceManager.GetString("SX1101MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Do not prefix local calls with 'this.'. + /// + internal static string SX1101Title { + get { + return ResourceManager.GetString("SX1101Title", resourceCulture); + } + } + } +} diff --git a/src/Agoda.Analyzers/StyleCop/ReadabilityResources.resx b/src/Agoda.Analyzers/StyleCop/ReadabilityResources.resx new file mode 100644 index 0000000..649b28f --- /dev/null +++ b/src/Agoda.Analyzers/StyleCop/ReadabilityResources.resx @@ -0,0 +1,537 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Fix indentation + + + Remove region + + + Replace 'base.' with 'this.' + + + A call to a member from an inherited class begins with 'base.', and the local class does not contain an override or implementation of the member. + + + Do not prefix calls with base unless local implementation exists + + + Do not prefix calls with base unless local implementation exists + + + Prefix reference with 'this.' + + + A call to an instance member of the local class or a base class is not prefixed with 'this.', within a C# code file. + + + Prefix local calls with this + + + Prefix local calls with this + + + Remove separating lines + + + A C# query clause does not begin on the same line as the previous clause, or on the next line. + + + Query clause must follow previous clause. + + + Query clause must follow previous clause + + + Place on multiple lines + + + Place on single line + + + The clauses within a C# query expression are not all placed on the same line, and each clause is not placed on its own line. + + + Query clauses must be on separate lines or all on one line + + + Query clauses must be on separate lines or all on one line + + + A clause within a C# query expression begins on the same line as the previous clause, when the previous clause spans across multiple lines. + + + Query clause must begin on new line when previous clause spans multiple lines + + + Insert new line + + + Query clause must begin on new line when previous clause spans multiple lines + + + A clause within a C# query expression spans across multiple lines, and does not begin on its own line. + + + Query clauses spanning multiple lines must begin on own line + + + Query clauses spanning multiple lines must begin on own line + + + Remove empty statement + + + The C# code contains an extra semicolon. + + + Code must not contain empty statements + + + Code must not contain empty statements + + + Enter new line + + + The C# code contains more than one statement on a single line. + + + Code must not contain multiple statements on one line + + + Code must not contain multiple statements on one line + + + A C# statement contains a comment between the declaration of the statement and the opening brace of the statement. + + + Block statements must not contain embedded comments + + + Block statements must not contain embedded comments + + + A C# statement contains a region tag between the declaration of the statement and the opening brace of the statement. + + + + + + Block statements must not contain embedded regions + + + The opening parenthesis or bracket is not placed on the same line as the method/indexer/attribute/array name. + + + Opening parenthesis or bracket must be on declaration line. + + + Opening parenthesis or bracket must be on declaration line + + + The closing parenthesis or bracket in a call to or declaration of a C# method/indexer/attribute/array/constructor/delegate is not placed on the same line as the last parameter. + + + Closing parenthesis must be on line of last parameter + + + Closing parenthesis must be on line of last parameter + + + The closing parenthesis or bracket in a call to a C# method or indexer, or the declaration of a method or indexer, is not placed on the same line as the opening bracket when the element does not take any parameters. + + + Closing parenthesis must be on line of opening parenthesis + + + Closing parenthesis must be on line of opening parenthesis + + + A comma between two parameters in a call to a C# method or indexer, or in the declaration of a method or indexer, is not placed on the same line as the previous parameter. + + + Comma must be on the same line as previous parameter. + + + Comma must be on the same line as previous parameter + + + The start of the parameter list for a method/constructor/indexer/array/operator call or declaration does not begin on the same line as the opening bracket, or on the line after the opening bracket. + + + Parameter list must follow declaration + + + Parameter list must follow declaration + + + A parameter within a C# method or indexer call or declaration does not begin on the same line as the previous parameter, or on the next line. + + + The parameter must begin on the line after the previous parameter. + + + Parameter must follow comma + + + Move first argument to next line + + + The parameters to a C# method or indexer call or declaration span across multiple lines, but the first parameter does not start on the line after the opening bracket. + + + The parameters must begin on the line after the declaration, whenever the parameter span across multiple lines + + + Split parameters must start on line after declaration + + + The parameters to a C# method or indexer call or declaration are not all on the same line or each on a separate line. + + + The parameters must all be placed on the same line or each parameter must be placed on its own line. + + + Parameters must be on same line or separate lines + + + A parameter to a C# method/indexer/attribute/array, other than the first parameter, spans across multiple lines. If the parameter is short, place the entire parameter on a single line. Otherwise, save the contents of the parameter in a temporary variable and pass the temporary variable as a parameter. + + + The parameter spans multiple lines + + + Parameter must not span multiple lines + + + Remove empty comment + + + The C# comment does not contain any comment text. + + + Comments must contain text + + + Comments must contain text + + + Replace with built-in type + + + The code uses one of the basic C# types, but does not use the built-in alias for the type. + + + Use built-in type alias + + + Use built-in type alias + + + Replace with string.Empty + + + The C# code includes an empty string, written as "". + + + Use string.Empty for empty strings + + + Use string.Empty for empty strings + + + The C# code contains a region within the body of a code element. + + + Region must not be located within a code element. + + + Do not place regions within elements + + + The C# code contains a region. + + + Do not use regions + + + Do not use regions + + + The Nullable<T> type has been defined not using the C# shorthand. For example, Nullable<DateTime> has been used instead of the preferred DateTime? + + + Use shorthand for nullable types + + + Use shorthand for nullable types + + + A call to a member is not prefixed with the 'this.', 'base.', 'object.' or 'typename.' prefix to indicate the intended method call, within a C# code file. + + + + + + Prefix calls correctly + + + Place each type constraint on a new line + + + Each type constraint clause for a generic type parameter should be listed on a line of code by itself. + + + Generic type constraints must be on their own line + + + Generic type constraints must be on their own line + + + Place constructor initializer on own line + + + A constructor initializer, including the colon character, should be on its own line. + + + Put constructor initializers on their own line + + + Put constructor initializers on their own line + + + Replace with default(T) + + + When creating a new instance of a value type T, the syntax 'default(T)' is functionally equivalent to the syntax 'new T()'. To avoid confusion regarding the behavior of the resulting instance, the first form is preferred. + + + Do not use default value type constructor + + + Do not use default value type constructor + + + Replace with lambda. + + + Lambda expressions are more succinct and easier to read than anonymous methods, so they should are preferred whenever the two are functionally equivalent. + + + Use lambda syntax + + + Use lambda syntax + + + Swap operands + + + When a comparison is made between a variable and a literal, the variable should be placed on the left-hand-side to maximize readability. + + + Constant values should appear on the right-hand side of comparisons + + + Use readable conditions + + + Place each field on a new line + + + Each field should be declared on its own line, in order to clearly see each field of a type and allow for proper documentation of the behavior of each field. + + + Each field should be declared on its own line + + + Do not combine fields + + + Give each attribute its own square brackets + + + Each attribute usage should be placed in its own set of square brackets for maximum readability. + + + Each attribute should be placed in its own set of square brackets. + + + Do not combine attributes + + + Place attribute on own line. + + + Each attribute should be placed on its own line of code. + + + Each attribute should be placed on its own line of code. + + + Attributes must not share line + + + Place enum values own their own lines + + + Enum values should be placed on their own lines for maximum readability. + + + Enum values should be on separate lines + + + Enum values should be on separate lines + + + Elements at the same level in the syntax tree should have the same indentation. + + + Elements should have the same indentation + + + Elements should have the same indentation + + + Use literal suffix notation instead of casting, in order to improve readability, avoid bugs related to illegal casts and ensure that optimal IL is produced. + + + Use literal suffix notation instead of casting + + + Use literal suffix notation instead of casting + + + Use literal suffix notation instead of casting + + + Remove 'this.' prefix + + + A call to an instance member of the local class or a base class is prefixed with `this.`. + + + Do not prefix local calls with 'this.' + + + Do not prefix local calls with 'this.' + + \ No newline at end of file diff --git a/src/Agoda.Analyzers/StyleCop/SA1106CodeMustNotContainEmptyStatements.cs b/src/Agoda.Analyzers/StyleCop/SA1106CodeMustNotContainEmptyStatements.cs new file mode 100644 index 0000000..9efe1cc --- /dev/null +++ b/src/Agoda.Analyzers/StyleCop/SA1106CodeMustNotContainEmptyStatements.cs @@ -0,0 +1,109 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System; +using System.Collections.Immutable; +using Agoda.Analyzers.Helpers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using StyleCop.Analyzers; + +namespace Agoda.Analyzers.StyleCop +{ + /// + /// The C# code contains an extra semicolon. + /// + /// + /// A violation of this rule occurs when the code contain an extra semicolon. Syntactically, this results in + /// an extra, empty statement in the code. + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class SA1106CodeMustNotContainEmptyStatements : DiagnosticAnalyzer + { + /// + /// The ID for diagnostics produced by the analyzer. + /// + public const string DiagnosticId = "SA1106"; + private static readonly LocalizableString Title = new LocalizableResourceString(nameof(ReadabilityResources.SA1106Title), ReadabilityResources.ResourceManager, typeof(ReadabilityResources)); + private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(ReadabilityResources.SA1106MessageFormat), ReadabilityResources.ResourceManager, typeof(ReadabilityResources)); + private static readonly LocalizableString Description = new LocalizableResourceString(nameof(ReadabilityResources.SA1106Description), ReadabilityResources.ResourceManager, typeof(ReadabilityResources)); + private static readonly string HelpLink = "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1106.md"; + + private static readonly DiagnosticDescriptor Descriptor = + new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, AnalyzerCategory.ReadabilityRules, DiagnosticSeverity.Warning, AnalyzerConstants.EnabledByDefault, Description, HelpLink, WellKnownDiagnosticTags.Unnecessary); + + private static readonly Action EmptyStatementAction = HandleEmptyStatement; + private static readonly Action BaseTypeDeclarationAction = HandleBaseTypeDeclaration; + private static readonly Action NamespaceDeclarationAction = HandleNamespaceDeclaration; + + /// + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(Descriptor); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterSyntaxNodeAction(EmptyStatementAction, SyntaxKind.EmptyStatement); + context.RegisterSyntaxNodeAction(BaseTypeDeclarationAction, SyntaxKinds.BaseTypeDeclaration); + context.RegisterSyntaxNodeAction(NamespaceDeclarationAction, SyntaxKind.NamespaceDeclaration); + } + + private static void HandleBaseTypeDeclaration(SyntaxNodeAnalysisContext context) + { + var declaration = (BaseTypeDeclarationSyntax)context.Node; + + if (declaration.SemicolonToken.IsKind(SyntaxKind.SemicolonToken)) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptor, declaration.SemicolonToken.GetLocation())); + } + } + + private static void HandleNamespaceDeclaration(SyntaxNodeAnalysisContext context) + { + var declaration = (NamespaceDeclarationSyntax)context.Node; + + if (declaration.SemicolonToken.IsKind(SyntaxKind.SemicolonToken)) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptor, declaration.SemicolonToken.GetLocation())); + } + } + + private static void HandleEmptyStatement(SyntaxNodeAnalysisContext context) + { + EmptyStatementSyntax syntax = (EmptyStatementSyntax)context.Node; + + LabeledStatementSyntax labeledStatementSyntax = syntax.Parent as LabeledStatementSyntax; + if (labeledStatementSyntax != null) + { + BlockSyntax blockSyntax = labeledStatementSyntax.Parent as BlockSyntax; + if (blockSyntax != null) + { + for (int i = blockSyntax.Statements.Count - 1; i >= 0; i--) + { + StatementSyntax statement = blockSyntax.Statements[i]; + + // allow an empty statement to be used for a label, but only if no non-empty statements exist + // before the end of the block + if (blockSyntax.Statements[i] == labeledStatementSyntax) + { + return; + } + + if (!statement.IsKind(SyntaxKind.EmptyStatement)) + { + break; + } + } + } + } + + // Code must not contain empty statements + context.ReportDiagnostic(Diagnostic.Create(Descriptor, syntax.GetLocation())); + } + } +} diff --git a/src/Agoda.Analyzers/StyleCop/SA1107CodeMustNotContainMultipleStatementsOnOneLine.cs b/src/Agoda.Analyzers/StyleCop/SA1107CodeMustNotContainMultipleStatementsOnOneLine.cs new file mode 100644 index 0000000..188e5fb --- /dev/null +++ b/src/Agoda.Analyzers/StyleCop/SA1107CodeMustNotContainMultipleStatementsOnOneLine.cs @@ -0,0 +1,86 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System; +using System.Collections.Immutable; +using Agoda.Analyzers.Helpers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using StyleCop.Analyzers; + +namespace Agoda.Analyzers.StyleCop +{ + /// + /// The C# code contains more than one statement on a single line. + /// + /// + /// A violation of this rule occurs when the code contain more than one statement on the same line. Each + /// statement must begin on a new line. + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class SA1107CodeMustNotContainMultipleStatementsOnOneLine : DiagnosticAnalyzer + { + /// + /// The ID for diagnostics produced by the + /// analyzer. + /// + public const string DiagnosticId = "SA1107"; + private static readonly LocalizableString Title = new LocalizableResourceString(nameof(ReadabilityResources.SA1107Title), ReadabilityResources.ResourceManager, typeof(ReadabilityResources)); + private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(ReadabilityResources.SA1107MessageFormat), ReadabilityResources.ResourceManager, typeof(ReadabilityResources)); + private static readonly LocalizableString Description = new LocalizableResourceString(nameof(ReadabilityResources.SA1107Description), ReadabilityResources.ResourceManager, typeof(ReadabilityResources)); + private static readonly string HelpLink = "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1107.md"; + + private static readonly DiagnosticDescriptor Descriptor = + new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, AnalyzerCategory.ReadabilityRules, DiagnosticSeverity.Warning, AnalyzerConstants.EnabledByDefault, Description, HelpLink); + + private static readonly Action BlockAction = HandleBlock; + + /// + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(Descriptor); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterSyntaxNodeAction(BlockAction, SyntaxKind.Block); + } + + private static void HandleBlock(SyntaxNodeAnalysisContext context) + { + BlockSyntax block = context.Node as BlockSyntax; + + if (block != null && block.Statements.Any()) + { + var previousStatement = block.Statements[0]; + FileLinePositionSpan previousStatementLocation = previousStatement.GetLineSpan(); + FileLinePositionSpan currentStatementLocation; + + for (int i = 1; i < block.Statements.Count; i++) + { + var currentStatement = block.Statements[i]; + currentStatementLocation = currentStatement.GetLineSpan(); + + if (previousStatementLocation.EndLinePosition.Line + == currentStatementLocation.StartLinePosition.Line + && !IsLastTokenMissing(previousStatement)) + { + context.ReportDiagnostic(Diagnostic.Create(Descriptor, block.Statements[i].GetLocation())); + } + + previousStatementLocation = currentStatementLocation; + previousStatement = currentStatement; + } + } + } + + private static bool IsLastTokenMissing(StatementSyntax previousStatement) + { + return previousStatement.GetLastToken(includeZeroWidth: true, includeSkipped: true).IsMissing; + } + } +} diff --git a/src/Agoda.Analyzers/StyleCop/SA1123DoNotPlaceRegionsWithinElements.cs b/src/Agoda.Analyzers/StyleCop/SA1123DoNotPlaceRegionsWithinElements.cs new file mode 100644 index 0000000..db2de85 --- /dev/null +++ b/src/Agoda.Analyzers/StyleCop/SA1123DoNotPlaceRegionsWithinElements.cs @@ -0,0 +1,103 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using StyleCop.Analyzers; + +namespace Agoda.Analyzers.StyleCop +{ + /// + /// The C# code contains a region within the body of a code element. + /// + /// + /// A violation of this rule occurs whenever a region is placed within the body of a code element. In many + /// editors, including Visual Studio, the region will appear collapsed by default, hiding the code within the + /// region. It is generally a bad practice to hide code within the body of an element, as this can lead to bad + /// decisions as the code is maintained over time. + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class SA1123DoNotPlaceRegionsWithinElements : DiagnosticAnalyzer + { + /// + /// The ID for diagnostics produced by the analyzer. + /// + public const string DiagnosticId = "SA1123"; + private static readonly LocalizableString Title = new LocalizableResourceString(nameof(ReadabilityResources.SA1123Title), ReadabilityResources.ResourceManager, typeof(ReadabilityResources)); + private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(ReadabilityResources.SA1123MessageFormat), ReadabilityResources.ResourceManager, typeof(ReadabilityResources)); + private static readonly LocalizableString Description = new LocalizableResourceString(nameof(ReadabilityResources.SA1123Description), ReadabilityResources.ResourceManager, typeof(ReadabilityResources)); + private static readonly string HelpLink = "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1123.md"; + + private static readonly DiagnosticDescriptor Descriptor = + new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, AnalyzerCategory.ReadabilityRules, DiagnosticSeverity.Warning, AnalyzerConstants.EnabledByDefault, Description, HelpLink); + + private static readonly Action RegionDirectiveTriviaAction = HandleRegionDirectiveTrivia; + + /// + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(Descriptor); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterSyntaxNodeAction(RegionDirectiveTriviaAction, SyntaxKind.RegionDirectiveTrivia); + } + + /// + /// Checks if a region is completely part of a body. That means that the #region and #endregion + /// tags both have to have a common as one of their ancestors. + /// + /// The that should be analyzed. + /// , if both tags have a common as one of their + /// ancestors; otherwise, . + /// + /// If is . + /// + internal static bool IsCompletelyContainedInBody(RegionDirectiveTriviaSyntax regionSyntax) + { + if (regionSyntax == null) + { + throw new ArgumentNullException(nameof(regionSyntax)); + } + + BlockSyntax syntax = null; + foreach (var directive in regionSyntax.GetRelatedDirectives()) + { + BlockSyntax blockSyntax = directive.AncestorsAndSelf().OfType().LastOrDefault(); + if (blockSyntax == null) + { + return false; + } + else if (syntax == null) + { + syntax = blockSyntax; + } + else if (blockSyntax != syntax) + { + return false; + } + } + + return true; + } + + private static void HandleRegionDirectiveTrivia(SyntaxNodeAnalysisContext context) + { + RegionDirectiveTriviaSyntax regionSyntax = (RegionDirectiveTriviaSyntax)context.Node; + + if (IsCompletelyContainedInBody(regionSyntax)) + { + // Region must not be located within a code element. + context.ReportDiagnostic(Diagnostic.Create(Descriptor, regionSyntax.GetLocation())); + } + } + } +} diff --git a/src/Agoda.Analyzers/StyleCop/Settings/DeserializationFailureBehavior.cs b/src/Agoda.Analyzers/StyleCop/Settings/DeserializationFailureBehavior.cs new file mode 100644 index 0000000..702c823 --- /dev/null +++ b/src/Agoda.Analyzers/StyleCop/Settings/DeserializationFailureBehavior.cs @@ -0,0 +1,24 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using Agoda.Analyzers.StyleCop.Settings.ObjectModel; +using Newtonsoft.Json; + +namespace Agoda.Analyzers.StyleCop.Settings +{ + /// + /// Defines the behavior of various methods in the event of a deserialization error. + /// + internal enum DeserializationFailureBehavior + { + /// + /// When deserialization fails, return a default instance. + /// + ReturnDefaultSettings, + + /// + /// When deserialization fails, throw a containing details about the error. + /// + ThrowException + } +} diff --git a/src/Agoda.Analyzers/StyleCop/Settings/ObjectModel/IndentationSettings.cs b/src/Agoda.Analyzers/StyleCop/Settings/ObjectModel/IndentationSettings.cs new file mode 100644 index 0000000..35d65f5 --- /dev/null +++ b/src/Agoda.Analyzers/StyleCop/Settings/ObjectModel/IndentationSettings.cs @@ -0,0 +1,49 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using Newtonsoft.Json; + +namespace Agoda.Analyzers.StyleCop.Settings.ObjectModel +{ + [JsonObject(MemberSerialization.OptIn)] + public class IndentationSettings + { + /// + /// This is the backing field for the property. + /// + [JsonProperty("indentationSize", DefaultValueHandling = DefaultValueHandling.Include)] + private int indentationSize; + + /// + /// This is the backing field for the property. + /// + [JsonProperty("tabSize", DefaultValueHandling = DefaultValueHandling.Include)] + private int tabSize; + + /// + /// This is the backing field for the property. + /// + [JsonProperty("useTabs", DefaultValueHandling = DefaultValueHandling.Include)] + private bool useTabs; + + /// + /// Initializes a new instance of the class during JSON deserialization. + /// + [JsonConstructor] + protected internal IndentationSettings() + { + this.indentationSize = 4; + this.tabSize = 4; + this.useTabs = false; + } + + public int IndentationSize => + this.indentationSize; + + public int TabSize => + this.tabSize; + + public bool UseTabs => + this.useTabs; + } +} diff --git a/src/Agoda.Analyzers/StyleCop/Settings/ObjectModel/SettingsFile.cs b/src/Agoda.Analyzers/StyleCop/Settings/ObjectModel/SettingsFile.cs new file mode 100644 index 0000000..ead424b --- /dev/null +++ b/src/Agoda.Analyzers/StyleCop/Settings/ObjectModel/SettingsFile.cs @@ -0,0 +1,35 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using Newtonsoft.Json; + +namespace Agoda.Analyzers.StyleCop.Settings.ObjectModel +{ + [JsonObject(MemberSerialization.OptIn)] + internal class SettingsFile + { + /// + /// This is the backing field for the property. + /// + [JsonProperty("settings", DefaultValueHandling = DefaultValueHandling.Ignore)] + private StyleCopSettings settings; + + /// + /// Initializes a new instance of the class + /// during JSON deserialization. + /// + [JsonConstructor] + protected SettingsFile() + { + this.settings = new StyleCopSettings(); + } + + public StyleCopSettings Settings + { + get + { + return this.settings; + } + } + } +} diff --git a/src/Agoda.Analyzers/StyleCop/Settings/ObjectModel/StyleCopSettings.cs b/src/Agoda.Analyzers/StyleCop/Settings/ObjectModel/StyleCopSettings.cs new file mode 100644 index 0000000..ff1209d --- /dev/null +++ b/src/Agoda.Analyzers/StyleCop/Settings/ObjectModel/StyleCopSettings.cs @@ -0,0 +1,31 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using Newtonsoft.Json; + +namespace Agoda.Analyzers.StyleCop.Settings.ObjectModel +{ + [JsonObject(MemberSerialization.OptIn)] + public class StyleCopSettings + { + /// + /// This is the backing field for the property. + /// + [JsonProperty("indentation", DefaultValueHandling = DefaultValueHandling.Ignore)] + private IndentationSettings indentation; + + /// + /// Initializes a new instance of the class during JSON deserialization. + /// + [JsonConstructor] + public StyleCopSettings() + { + this.indentation = new IndentationSettings(); + + } + + public IndentationSettings Indentation => + this.indentation; + + } +} diff --git a/src/Agoda.Analyzers/StyleCop/Settings/SettingsHelper.cs b/src/Agoda.Analyzers/StyleCop/Settings/SettingsHelper.cs new file mode 100644 index 0000000..c50c99e --- /dev/null +++ b/src/Agoda.Analyzers/StyleCop/Settings/SettingsHelper.cs @@ -0,0 +1,187 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Collections.Immutable; +using System.IO; +using System.Threading; +using Agoda.Analyzers.StyleCop.Settings.ObjectModel; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Text; +using Newtonsoft.Json; + +namespace Agoda.Analyzers.StyleCop.Settings +{ + /// + /// Class that manages the settings files for StyleCopAnalyzers. + /// + public static class SettingsHelper + { + public const string SettingsFileName = "stylecop.json"; + + private static readonly SourceTextValueProvider SettingsValueProvider = + new SourceTextValueProvider( + text => GetStyleCopSettings(text, DeserializationFailureBehavior.ReturnDefaultSettings)); + + /// + /// Gets the StyleCop settings. + /// + /// + /// If a occurs while deserializing the settings file, a default settings + /// instance is returned. + /// + /// The context that will be used to determine the StyleCop settings. + /// The cancellation token that the operation will observe. + /// A instance that represents the StyleCop settings for the given context. + internal static StyleCopSettings GetStyleCopSettings(this SyntaxTreeAnalysisContext context, CancellationToken cancellationToken) + { + return context.Options.GetStyleCopSettings(cancellationToken); + } + + /// + /// Gets the StyleCop settings. + /// + /// + /// If a occurs while deserializing the settings file, a default settings + /// instance is returned. + /// + /// The analyzer options that will be used to determine the StyleCop settings. + /// The cancellation token that the operation will observe. + /// A instance that represents the StyleCop settings for the given context. + internal static StyleCopSettings GetStyleCopSettings(this AnalyzerOptions options, CancellationToken cancellationToken) + { + return GetStyleCopSettings(options, DeserializationFailureBehavior.ReturnDefaultSettings, cancellationToken); + } + + /// + /// Gets the StyleCop settings. + /// + /// The analyzer options that will be used to determine the StyleCop settings. + /// The behavior of the method when a occurs while + /// deserializing the settings file. + /// The cancellation token that the operation will observe. + /// A instance that represents the StyleCop settings for the given context. + internal static StyleCopSettings GetStyleCopSettings(this AnalyzerOptions options, DeserializationFailureBehavior failureBehavior, CancellationToken cancellationToken) + { + return GetStyleCopSettings(options != null ? options.AdditionalFiles : ImmutableArray.Create(), failureBehavior, cancellationToken); + } + + internal static StyleCopSettings GetStyleCopSettings(this AnalysisContext context, AnalyzerOptions options, CancellationToken cancellationToken) + { + return GetStyleCopSettings(context, options, DeserializationFailureBehavior.ReturnDefaultSettings, cancellationToken); + } + + internal static StyleCopSettings GetStyleCopSettings(this AnalysisContext context, AnalyzerOptions options, DeserializationFailureBehavior failureBehavior, CancellationToken cancellationToken) + { + SourceText text = TryGetStyleCopSettingsText(options, cancellationToken); + if (text == null) + { + return new StyleCopSettings(); + } + + if (failureBehavior == DeserializationFailureBehavior.ReturnDefaultSettings) + { + StyleCopSettings settings; + if (!context.TryGetValue(text, SettingsValueProvider, out settings)) + { + return new StyleCopSettings(); + } + + return settings; + } + + return JsonConvert.DeserializeObject(text.ToString()).Settings; + } + + internal static StyleCopSettings GetStyleCopSettings(this CompilationStartAnalysisContext context, AnalyzerOptions options, CancellationToken cancellationToken) + { + return GetStyleCopSettings(context, options, DeserializationFailureBehavior.ReturnDefaultSettings, cancellationToken); + } + +#pragma warning disable RS1012 // Start action has no registered actions. + internal static StyleCopSettings GetStyleCopSettings(this CompilationStartAnalysisContext context, AnalyzerOptions options, DeserializationFailureBehavior failureBehavior, CancellationToken cancellationToken) +#pragma warning restore RS1012 // Start action has no registered actions. + { + SourceText text = TryGetStyleCopSettingsText(options, cancellationToken); + if (text == null) + { + return new StyleCopSettings(); + } + + if (failureBehavior == DeserializationFailureBehavior.ReturnDefaultSettings) + { + StyleCopSettings settings; + if (!context.TryGetValue(text, SettingsValueProvider, out settings)) + { + return new StyleCopSettings(); + } + + return settings; + } + + return JsonConvert.DeserializeObject(text.ToString()).Settings; + } + + private static StyleCopSettings GetStyleCopSettings(SourceText text, DeserializationFailureBehavior failureBehavior) + { + try + { + var root = JsonConvert.DeserializeObject(text.ToString()); + + if (root == null) + { + throw new JsonException($"Settings file was missing or empty."); + } + + return root.Settings; + } + catch (JsonException) when (failureBehavior == DeserializationFailureBehavior.ReturnDefaultSettings) + { + // The settings file is invalid -> return the default settings. + } + + return new StyleCopSettings(); + } + + private static SourceText TryGetStyleCopSettingsText(this AnalyzerOptions options, CancellationToken cancellationToken) + { + foreach (var additionalFile in options.AdditionalFiles) + { + if (Path.GetFileName(additionalFile.Path).ToLowerInvariant() == SettingsFileName) + { + return additionalFile.GetText(cancellationToken); + } + } + + return null; + } + + private static StyleCopSettings GetStyleCopSettings(ImmutableArray additionalFiles, DeserializationFailureBehavior failureBehavior, CancellationToken cancellationToken) + { + try + { + foreach (var additionalFile in additionalFiles) + { + if (Path.GetFileName(additionalFile.Path).ToLowerInvariant() == SettingsFileName) + { + SourceText additionalTextContent = additionalFile.GetText(cancellationToken); + var root = JsonConvert.DeserializeObject(additionalTextContent.ToString()); + + if (root == null) + { + throw new JsonException($"Settings file at '{Path.GetFileName(additionalFile.Path)}' was missing or empty."); + } + + return root.Settings; + } + } + } + catch (JsonException) when (failureBehavior == DeserializationFailureBehavior.ReturnDefaultSettings) + { + // The settings file is invalid -> return the default settings. + } + + return new StyleCopSettings(); + } + } +} diff --git a/src/Agoda.Analyzers/app.config b/src/Agoda.Analyzers/app.config new file mode 100644 index 0000000..9541974 --- /dev/null +++ b/src/Agoda.Analyzers/app.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/Agoda.Analyzers/packages.config b/src/Agoda.Analyzers/packages.config new file mode 100644 index 0000000..ebb8db9 --- /dev/null +++ b/src/Agoda.Analyzers/packages.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/AgodaAnalyzers.sln b/src/AgodaAnalyzers.sln new file mode 100644 index 0000000..3ab13b4 --- /dev/null +++ b/src/AgodaAnalyzers.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.25420.1 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Agoda.Analyzers", "Agoda.Analyzers\Agoda.Analyzers.csproj", "{4F934D25-9BFF-4153-8965-F12F52BA41DF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Agoda.Analyzers.CodeFixes", "Agoda.Analyzers.CodeFixes\Agoda.Analyzers.CodeFixes.csproj", "{A5863784-CD41-4419-9C8F-53D89D509FE9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Agoda.Analyzers.Test", "Agoda.Analyzers.Test\Agoda.Analyzers.Test.csproj", "{756B9DD8-2FE7-485D-8640-6E2755514EAE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4F934D25-9BFF-4153-8965-F12F52BA41DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F934D25-9BFF-4153-8965-F12F52BA41DF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F934D25-9BFF-4153-8965-F12F52BA41DF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F934D25-9BFF-4153-8965-F12F52BA41DF}.Release|Any CPU.Build.0 = Release|Any CPU + {A5863784-CD41-4419-9C8F-53D89D509FE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A5863784-CD41-4419-9C8F-53D89D509FE9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A5863784-CD41-4419-9C8F-53D89D509FE9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A5863784-CD41-4419-9C8F-53D89D509FE9}.Release|Any CPU.Build.0 = Release|Any CPU + {756B9DD8-2FE7-485D-8640-6E2755514EAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {756B9DD8-2FE7-485D-8640-6E2755514EAE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {756B9DD8-2FE7-485D-8640-6E2755514EAE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {756B9DD8-2FE7-485D-8640-6E2755514EAE}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal