diff --git a/analyzers/src/SonarAnalyzer.CSharp.Styling/packages.lock.json b/analyzers/src/SonarAnalyzer.CSharp.Styling/packages.lock.json index c3e93bf8f34..9b84e8e2ff6 100644 --- a/analyzers/src/SonarAnalyzer.CSharp.Styling/packages.lock.json +++ b/analyzers/src/SonarAnalyzer.CSharp.Styling/packages.lock.json @@ -71,6 +71,11 @@ "resolved": "2.14.1", "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==" }, + "Microsoft.AspNetCore.Razor.Language": { + "type": "Transitive", + "resolved": "6.0.29", + "contentHash": "GqbAfFQEv9XKi+QRlhzoYJvbi9EkFo0rkzvaP1xGvn2Hjn1DrhGK93UuJ8zLw8FQ9jmW1GOMLXLSPJoID4lLbg==" + }, "Microsoft.Bcl.AsyncInterfaces": { "type": "Transitive", "resolved": "8.0.0", @@ -282,6 +287,7 @@ "sonaranalyzer.csharp": { "type": "Project", "dependencies": { + "Microsoft.AspNetCore.Razor.Language": "[6.0.29, )", "Microsoft.CodeAnalysis.CSharp.Workspaces": "[1.3.2, )", "SonarAnalyzer": "[1.0.0, )", "System.Collections.Immutable": "[1.1.37, )" diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/FrameworkViewCompiler.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/FrameworkViewCompiler.cs new file mode 100644 index 00000000000..ae6ea80041f --- /dev/null +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/FrameworkViewCompiler.cs @@ -0,0 +1,107 @@ +/* + * SonarAnalyzer for .NET + * Copyright (C) 2015-2024 SonarSource SA + * mailto: contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +using System.IO; +using Microsoft.AspNetCore.Razor.Language; + +namespace SonarAnalyzer.Rules.CSharp; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class FrameworkViewCompiler : SonarDiagnosticAnalyzer +{ + private readonly HashSet BadBoys = new() + { + "S3904", // AssemblyVersion attribute + "S3990", // CLSCompliant attribute + "S3992", // ComVisible attribute + "S1451", // License header + "S103", // Lines too long + "S104", // Files too many lines + "S109", // Magic number + "S113", // Files without newline + "S1147", // Exit methods + "S1192", // String literals duplicated + "S1944", // Invalid cast + "S1905", // Redundant cast + "S1116", // Empty statements + }; + + private ImmutableArray Rules; + + public override ImmutableArray SupportedDiagnostics { get; } + + protected override bool EnableConcurrentExecution => false; + + public FrameworkViewCompiler() + { + Rules = RuleFinder2.CreateAnalyzers(AnalyzerLanguage.CSharp, false) + .Where(x => x.SupportedDiagnostics.All(d => !BadBoys.Contains(d.Id))) + .ToImmutableArray(); + + SupportedDiagnostics = Rules.SelectMany(x => x.SupportedDiagnostics).ToImmutableArray(); + } + + protected override void Initialize(SonarAnalysisContext context) => + + context.RegisterCompilationAction( + c => + { + // TODO: Maybe there is a better check, maybe use References(KnownAssembly for System.Web.Mvc) + if (c.Compilation.GetTypeByMetadataName(KnownType.System_Web_Mvc_Controller) is null) + { + return; + } + + var projectConfiguration = c.ProjectConfiguration(); + var root = Path.GetDirectoryName(projectConfiguration.ProjectPath); + var dummy = CompileViews(c.Compilation, root).WithAnalyzers(Rules, c.Options); + var diagnostics = dummy.GetAnalyzerDiagnosticsAsync().Result; + foreach (var diagnostic in diagnostics) + { + c.ReportIssue(diagnostic); + } + }); + + Compilation CompileViews(Compilation compilation, string rootDir) + { + FilesToAnalyzeProvider filesProvider = new(Directory.GetFiles(rootDir, "*.*", SearchOption.AllDirectories)); + var razorCompiler = new RazorCompiler(rootDir, filesProvider); + var dummyCompilation = compilation; + + var documents = razorCompiler.CompileAll(); + var razorTrees = new List(); + + var i = 0; + + foreach (var razorDocument in documents) + { + if (razorDocument.GetCSharpDocument()?.GeneratedCode is { } csharpCode) + { + var razorTree = CSharpSyntaxTree.ParseText( + csharpCode, + new CSharpParseOptions(compilation.GetLanguageVersion()), + path: $"x_{i++}.cshtml.g.cs"); + razorTrees.Add(razorTree); + } + } + dummyCompilation = dummyCompilation.AddSyntaxTrees(razorTrees); + return dummyCompilation; + } +} diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/RazorCompiler.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/RazorCompiler.cs new file mode 100644 index 00000000000..5d0b8590e34 --- /dev/null +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/RazorCompiler.cs @@ -0,0 +1,195 @@ +using System.IO; +using System.Text.RegularExpressions; +using System.Xml.Linq; +using System.Xml.XPath; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.CodeGeneration; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace SonarAnalyzer.Rules; + +// Copied from sonar-security. + +/// +/// Compiler for manual Razor Views cross-compilation to C# code for .NET Framework projects. +/// We don't need to manually compile .NET Core Razor projects. They are compiled during build time and analyzed directly with Roslyn analyzers. +/// +internal sealed class RazorCompiler +{ + public const string SonarRazorCompiledItemAttribute = nameof(SonarRazorCompiledItemAttribute); + + private static readonly Regex CshtmlFileRegex = new(@"\.cshtml$", RegexOptions.IgnoreCase, TimeSpan.FromSeconds(2000)); + + private readonly string rootDirectory; + private readonly string[] viewPaths; + private readonly Dictionary engines = new(); // Cache for Razor engines based on the View and web.config directory. + + public RazorCompiler(string rootDirectory, FilesToAnalyzeProvider filesToAnalyzeProvider) + { + this.rootDirectory = rootDirectory; + viewPaths = filesToAnalyzeProvider.FindFiles(CshtmlFileRegex).ToArray(); + if (viewPaths.Length != 0) + { + BuildEngines(filesToAnalyzeProvider.FindFiles("web.config")); + } + } + + public IEnumerable CompileAll() + { + foreach (var viewPath in viewPaths) + { + if (FindEngine(Path.GetDirectoryName(viewPath)) is { } engine) + { + yield return engine.Compile(viewPath); + } + } + } + + private void BuildEngines(IEnumerable webConfigs) + { + // Each web.config can change the list of imported namespace and we need a separate engine for it + var razorFileSystem = RazorProjectFileSystem.Create(rootDirectory); + foreach (var webConfigPath in webConfigs.OrderBy(x => x.Length)) + { + var webConfig = File.ReadAllText(webConfigPath); + if (webConfig.Contains("()); + } + // Fill cache with values for each View directory + foreach (var directory in viewPaths.Select(Path.GetDirectoryName).Distinct().OrderBy(x => x.Length)) + { + if (!engines.ContainsKey(directory)) + { + engines.Add(directory, FindEngine(directory) ?? rootEngine); + } + } + } + + private string[] ReadNamespaces(string webConfigDir, IEnumerable xmlElements) + { + // web.config structure is hierarchical. Every level inherits parent's configuration and modifies it with Add/Clear operations. + var ret = FindEngine(webConfigDir)?.Namespaces.ToList() ?? new List(); + foreach (var element in xmlElements) + { + switch (element.Name.LocalName) + { + case "add": + var ns = element.Attribute("namespace")?.Value; + if (!string.IsNullOrEmpty(ns)) + { + ret.Add(ns); + } + break; + + case "clear": + ret.Clear(); + break; + } + } + return ret.Distinct().ToArray(); + } + + private Engine FindEngine(string directory) + { + while (directory.StartsWith(rootDirectory, StringComparison.OrdinalIgnoreCase)) + { + if (engines.ContainsKey(directory)) + { + return engines[directory]; + } + directory = Path.GetDirectoryName(directory); + } + return null; + } + + private static XDocument ParseXDocument(string text) + { + try + { + return XDocument.Parse(text); + } + catch + { + return null; + } + } + + private sealed class Engine + { + public readonly string[] Namespaces; + private readonly RazorProjectEngine razorProjectEngine; + + public Engine(RazorProjectFileSystem razorFileSystem, string[] namespaces) + { + Namespaces = namespaces; + razorProjectEngine = RazorProjectEngine.Create(RazorConfiguration.Default, razorFileSystem, builder => + { + builder.AddDefaultImports(Namespaces.Select(x => $"@using {x}").ToArray()); + builder.AddDirective(ModificationPass.Directive); + builder.Features.Add(new ModificationPass()); + var targetExtension = builder.Features.OfType().Single(); + UpdateCompiledItemAttributeName(targetExtension.TargetExtensions.Single(x => x.GetType().Name == "MetadataAttributeTargetExtension")); + }); + } + + public RazorCodeDocument Compile(string viewPath) => + razorProjectEngine.Process(razorProjectEngine.FileSystem.GetItem(viewPath, FileKinds.Legacy)); + + private static void UpdateCompiledItemAttributeName(ICodeTargetExtension metadataAttributeTargetExtension) => + metadataAttributeTargetExtension.GetType().GetProperty("CompiledItemAttributeName").SetValue(metadataAttributeTargetExtension, "global::" + SonarRazorCompiledItemAttribute); + } + + private sealed class ModificationPass : IRazorOptimizationPass + { + // This represents the '@model' directive in Razor syntax tree. AddTypeToken() ensures that there will be exactly one (space separated) type token following the directive. + // i.e.: @model ThisIs.Single.TypeToken + public static readonly DirectiveDescriptor Directive = DirectiveDescriptor.CreateDirective("model", DirectiveKind.SingleLine, builder => builder.AddTypeToken()); + + public int Order => 1; // Must be higher than MetadataAttributePass.Order == 0, that generates the attribute nodes. + public RazorEngine Engine { get; set; } + + public void Execute(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) + { + var namespaceNode = FindNode(documentNode); + var classNode = FindNode(namespaceNode); + var methodNode = FindNode(classNode); + var modelNode = FindNode(methodNode, x => x.Directive == Directive); + documentNode.Children.Add(new SonarAttributeIntermediateNode()); + namespaceNode.Children.Remove(namespaceNode.Children.Single(x => x.GetType().Name == "RazorSourceChecksumAttributeIntermediateNode")); + classNode.BaseType = $"global::System.Web.Mvc.WebViewPage<{modelNode?.Tokens.Single().Content ?? "dynamic"}>"; + methodNode.Modifiers.Remove("async"); + methodNode.ReturnType = "void"; + methodNode.MethodName = "Execute"; + } + + private static TNode FindNode(IntermediateNode parent, Func predicate = null) where TNode : IntermediateNode => + parent.Children.OfType().FirstOrDefault(x => predicate is null || predicate(x)); + } + + private sealed class SonarAttributeIntermediateNode : ExtensionIntermediateNode + { + public override IntermediateNodeCollection Children => IntermediateNodeCollection.ReadOnly; + + public override void Accept(IntermediateNodeVisitor visitor) => + AcceptExtensionNode(this, visitor); + + public override void WriteNode(CodeTarget target, CodeRenderingContext context) => + // The constructor signature must be same as the original Microsoft.AspNetCore.Razor.Hosting.RazorCompiledItemAttribute + context.CodeWriter.WriteLine($$""" + public sealed class {{SonarRazorCompiledItemAttribute}} : System.Attribute + { + public SonarRazorCompiledItemAttribute(System.Type type, string kind, string identifier) { } + } + """); + } +} diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/RuleFinder2.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/RuleFinder2.cs new file mode 100644 index 00000000000..3497e0ce2ab --- /dev/null +++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/RuleFinder2.cs @@ -0,0 +1,114 @@ +using System.Reflection; +using System.Reflection.Metadata; +using SonarAnalyzer.Rules.CSharp; + +namespace SonarAnalyzer.Rules; + +// Copied from the test framework. +// ToDo: rework both this class and the RuleFinder from the test framework. +internal static class RuleFinder2 +{ + private const string DotDelimiterString = "."; + + public static IEnumerable AllAnalyzerTypes { get; } // Rules and Utility analyzers + public static IEnumerable RuleAnalyzerTypes { get; } // Rules-only, without Utility analyzers + public static IEnumerable UtilityAnalyzerTypes { get; } + + static RuleFinder2() + { + var executingAssembly = Assembly.GetExecutingAssembly(); + List allTypes; + using (var assemblyMetadata = AssemblyMetadata.CreateFromFile(executingAssembly.Location)) + { + allTypes = assemblyMetadata + .GetModules() + .SelectMany(GetFullyQualifiedNames) + .Select(executingAssembly.GetType) + .ToList(); + } + + AllAnalyzerTypes = allTypes.Where(x => x.IsSubclassOf(typeof(DiagnosticAnalyzer)) && x.GetCustomAttributes().Any()).ToArray(); + UtilityAnalyzerTypes = AllAnalyzerTypes.Where(x => typeof(UtilityAnalyzerBase).IsAssignableFrom(x)).ToList(); + RuleAnalyzerTypes = AllAnalyzerTypes.Except(UtilityAnalyzerTypes).ToList(); + } + + public static IEnumerable GetAnalyzerTypes(AnalyzerLanguage language) => + RuleAnalyzerTypes.Where(x => TargetLanguage(x) == language); + + public static IEnumerable CreateAnalyzers(AnalyzerLanguage language, bool includeUtilityAnalyzers) + { + var types = GetAnalyzerTypes(language); + if (includeUtilityAnalyzers) + { + types = types.Concat(UtilityAnalyzerTypes.Where(x => TargetLanguage(x) == language)); + } + + // EXCLUDE ourselves, or FrameworkViewCompiler will try to instantiate itself indefinitely, until StackOverflowException. + types = types.Where(x => x != typeof(FrameworkViewCompiler)); + foreach (var type in types) + { + yield return typeof(HotspotDiagnosticAnalyzer).IsAssignableFrom(type) && type.GetConstructor([typeof(IAnalyzerConfiguration)]) is not null + ? (DiagnosticAnalyzer)Activator.CreateInstance(type, AnalyzerConfiguration.AlwaysEnabled) + : (DiagnosticAnalyzer)Activator.CreateInstance(type); + } + } + + public static bool IsParameterized(Type analyzerType) => + analyzerType.GetProperties().Any(x => x.GetCustomAttributes().Any()); + + private static AnalyzerLanguage TargetLanguage(MemberInfo analyzerType) + { + var languages = analyzerType.GetCustomAttributes().SingleOrDefault()?.Languages + ?? throw new NotSupportedException($"Can not find any language for the given type {analyzerType.Name}!"); + return languages.Length == 1 + ? AnalyzerLanguage.FromName(languages.Single()) + : throw new NotSupportedException($"Analyzer can not have multiple languages: {analyzerType.Name}"); + } + + private static IEnumerable GetFullyQualifiedNames(ModuleMetadata module) + { + var metadataReader = module.GetMetadataReader(); + return metadataReader.TypeDefinitions + .Select(definitionHandle => metadataReader.GetTypeDefinition(definitionHandle)) + .Where(definition => definition.GetCustomAttributes().Any(attributeHandle => IsDiagnosticAnalyzerAttribute(attributeHandle, metadataReader))) + .Select(definition => GetFullyQualifiedTypeName(definition, metadataReader)); + } + + private static bool IsDiagnosticAnalyzerAttribute(CustomAttributeHandle attributeHandle, MetadataReader metadataReader) + { + var attribute = metadataReader.GetCustomAttribute(attributeHandle); + var ctor = attribute.Constructor; + if (ctor.Kind == HandleKind.MemberReference) + { + var memberRef = metadataReader.GetMemberReference((MemberReferenceHandle)ctor); + var ctorType = memberRef.Parent; + if (metadataReader.StringComparer.Equals(memberRef.Name, WellKnownMemberNames.InstanceConstructorName) + && ctorType.Kind == HandleKind.TypeReference + && metadataReader.GetString(metadataReader.GetTypeReference((TypeReferenceHandle)ctorType).Name) == "DiagnosticAnalyzerAttribute" + && metadataReader.GetString(metadataReader.GetTypeReference((TypeReferenceHandle)ctorType).Namespace) == "Microsoft.CodeAnalysis.Diagnostics") + { + return true; + } + } + return false; + } + + private static string GetFullyQualifiedTypeName(TypeDefinition typeDef, MetadataReader metadataReader) + { + var declaringType = typeDef.GetDeclaringType(); + if (declaringType.IsNil) // Non nested type - simply get the full name + { + return BuildQualifiedName(metadataReader.GetString(typeDef.Namespace), metadataReader.GetString(typeDef.Name)); + } + else + { + var declaringTypeDef = metadataReader.GetTypeDefinition(declaringType); + return GetFullyQualifiedTypeName(declaringTypeDef, metadataReader) + "+" + metadataReader.GetString(typeDef.Name); + } + } + + private static string BuildQualifiedName(string qualifier, string name) => + string.IsNullOrEmpty(qualifier) + ? name + : string.Concat(qualifier, DotDelimiterString, name); +} diff --git a/analyzers/src/SonarAnalyzer.CSharp/SonarAnalyzer.CSharp.csproj b/analyzers/src/SonarAnalyzer.CSharp/SonarAnalyzer.CSharp.csproj index bbe8145a243..7a687f9232b 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/SonarAnalyzer.CSharp.csproj +++ b/analyzers/src/SonarAnalyzer.CSharp/SonarAnalyzer.CSharp.csproj @@ -15,6 +15,7 @@ For instance, System.ValueTuple is not available in 4.6.1 and must be added to the final packaging if we add it here --> + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -55,6 +56,7 @@ + diff --git a/analyzers/src/SonarAnalyzer.CSharp/packages.lock.json b/analyzers/src/SonarAnalyzer.CSharp/packages.lock.json index 41dd7bc332b..9ab21778462 100644 --- a/analyzers/src/SonarAnalyzer.CSharp/packages.lock.json +++ b/analyzers/src/SonarAnalyzer.CSharp/packages.lock.json @@ -2,6 +2,12 @@ "version": 1, "dependencies": { ".NETStandard,Version=v2.0": { + "Microsoft.AspNetCore.Razor.Language": { + "type": "Direct", + "requested": "[6.0.29, )", + "resolved": "6.0.29", + "contentHash": "GqbAfFQEv9XKi+QRlhzoYJvbi9EkFo0rkzvaP1xGvn2Hjn1DrhGK93UuJ8zLw8FQ9jmW1GOMLXLSPJoID4lLbg==" + }, "Microsoft.CodeAnalysis.CSharp.Workspaces": { "type": "Direct", "requested": "[1.3.2, )", diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/AnalysisConfigReader.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/AnalysisConfigReader.cs index 675ebfe0573..bc6bccf88ed 100644 --- a/analyzers/src/SonarAnalyzer.Common/Helpers/AnalysisConfigReader.cs +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/AnalysisConfigReader.cs @@ -45,7 +45,10 @@ public AnalysisConfigReader(string analysisConfigPath) public string[] UnchangedFiles() => ConfigValue("UnchangedFilesPath") is { } unchangedFilesPath ? File.ReadAllLines(unchangedFilesPath) - : Array.Empty(); + : []; + + public string SonarScannerWorkingDirectory() => + ConfigValue("SonarScannerWorkingDirectory"); private string ConfigValue(string id) => analysisConfig.AdditionalConfig.FirstOrDefault(x => x.Id == id)?.Value; diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/FilesToAnalyzeProvider.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/FilesToAnalyzeProvider.cs index 006dfce9018..bd9d8aa4cd8 100644 --- a/analyzers/src/SonarAnalyzer.Common/Helpers/FilesToAnalyzeProvider.cs +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/FilesToAnalyzeProvider.cs @@ -30,6 +30,9 @@ public class FilesToAnalyzeProvider public FilesToAnalyzeProvider(string filePath) => allFiles = ReadLines(filePath); + public FilesToAnalyzeProvider(IEnumerable fileList) => + allFiles = fileList; + public IEnumerable FindFiles(string fileName, bool onlyExistingFiles = true) => allFiles.Where(x => FilterByFileName(x, fileName) && (!onlyExistingFiles || File.Exists(x))); diff --git a/analyzers/tests/SonarAnalyzer.CSharp.Styling.Test/packages.lock.json b/analyzers/tests/SonarAnalyzer.CSharp.Styling.Test/packages.lock.json index a6c9eb5135e..7d2e96989db 100644 --- a/analyzers/tests/SonarAnalyzer.CSharp.Styling.Test/packages.lock.json +++ b/analyzers/tests/SonarAnalyzer.CSharp.Styling.Test/packages.lock.json @@ -79,6 +79,11 @@ "resolved": "2.14.1", "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==" }, + "Microsoft.AspNetCore.Razor.Language": { + "type": "Transitive", + "resolved": "6.0.29", + "contentHash": "GqbAfFQEv9XKi+QRlhzoYJvbi9EkFo0rkzvaP1xGvn2Hjn1DrhGK93UuJ8zLw8FQ9jmW1GOMLXLSPJoID4lLbg==" + }, "Microsoft.Build": { "type": "Transitive", "resolved": "17.3.2", @@ -568,6 +573,7 @@ "sonaranalyzer.csharp": { "type": "Project", "dependencies": { + "Microsoft.AspNetCore.Razor.Language": "[6.0.29, )", "Microsoft.CodeAnalysis.CSharp.Workspaces": "[1.3.2, )", "SonarAnalyzer": "[1.0.0, )", "System.Collections.Immutable": "[1.1.37, )" diff --git a/analyzers/tests/SonarAnalyzer.Test/Rules/AspNet/BackslashShouldBeAvoidedInAspNetRoutesTest.cs b/analyzers/tests/SonarAnalyzer.Test/Rules/AspNet/BackslashShouldBeAvoidedInAspNetRoutesTest.cs index aebeb6cf5b9..1d6b2b4c663 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Rules/AspNet/BackslashShouldBeAvoidedInAspNetRoutesTest.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Rules/AspNet/BackslashShouldBeAvoidedInAspNetRoutesTest.cs @@ -63,7 +63,7 @@ public static string AttributesWithAllTypesOfStringsDisplayNameProvider(MethodIn public static IEnumerable AspNet4xMvcVersionsUnderTest => [["5.2.7"] /* Most used */, [Constants.NuGetLatestVersion]]; - private static IEnumerable AspNet4xReferences(string aspNetMvcVersion) => + public static IEnumerable AspNet4xReferences(string aspNetMvcVersion) => MetadataReferenceFacade.SystemWeb // For HttpAttributeMethod and derived attributes .Concat(NuGetMetadataReference.MicrosoftAspNetMvc(aspNetMvcVersion)); // For Controller diff --git a/analyzers/tests/SonarAnalyzer.Test/Rules/IfCollapsibleTest.cs b/analyzers/tests/SonarAnalyzer.Test/Rules/IfCollapsibleTest.cs index a7abd3e8c75..8f58e8186aa 100644 --- a/analyzers/tests/SonarAnalyzer.Test/Rules/IfCollapsibleTest.cs +++ b/analyzers/tests/SonarAnalyzer.Test/Rules/IfCollapsibleTest.cs @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +using System.IO; using CS = SonarAnalyzer.Rules.CSharp; using VB = SonarAnalyzer.Rules.VisualBasic; @@ -26,6 +27,29 @@ namespace SonarAnalyzer.Test.Rules [TestClass] public class IfCollapsibleTest { + +#if NETFRAMEWORK + public TestContext TestContext { get; set; } + + public static IEnumerable AspNet4xReferences(string aspNetMvcVersion) => + MetadataReferenceFacade.SystemWeb // For HttpAttributeMethod and derived attributes + .Concat(NuGetMetadataReference.MicrosoftAspNetMvc(aspNetMvcVersion)); // For Controller + + [TestMethod] + public void FrameworkViewCompiler_CS() + { + var rootProjectPath = Path.Combine(Paths.CurrentTestCases(), "Razor", "CSHTMLFiles", "Project.csproj"); + var analysisConfigPath = AnalysisScaffolding.CreateAnalysisConfig(TestContext, "SonarScannerWorkingDirectory", Path.Combine(Paths.CurrentTestCases(), "Razor", "CSHTMLFiles")); + + new VerifierBuilder() + .WithAdditionalFilePath(AnalysisScaffolding.CreateSonarProjectConfig(TestContext, "ProjectPath", rootProjectPath, true, analysisConfigPath)) + .AddReferences(AspNet4xReferences("5.2.7")) + .AddSnippet(string.Empty, + "Thingy.cs") + .Verify(); + } +#endif + [TestMethod] public void IfCollapsible_CS() => new VerifierBuilder().AddPaths("IfCollapsible.cs").Verify(); diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/Razor/CSHTMLFiles/Contact.cshtml b/analyzers/tests/SonarAnalyzer.Test/TestCases/Razor/CSHTMLFiles/Contact.cshtml new file mode 100644 index 00000000000..4bdba071c9d --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.Test/TestCases/Razor/CSHTMLFiles/Contact.cshtml @@ -0,0 +1,12 @@ +@{ + var uselessInView = 42; // Noncompliant + // Noncompliant @-1 + + void DoWorkView() // Noncompliant + { + DoWorkView(); + } +} + +
+
\ No newline at end of file diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/Razor/CSHTMLFiles/Costin.cshtml b/analyzers/tests/SonarAnalyzer.Test/TestCases/Razor/CSHTMLFiles/Costin.cshtml new file mode 100644 index 00000000000..e8660dc5b01 --- /dev/null +++ b/analyzers/tests/SonarAnalyzer.Test/TestCases/Razor/CSHTMLFiles/Costin.cshtml @@ -0,0 +1,16 @@ +@{ + var uselessInView2 = 42; // Noncompliant + // Noncompliant @-1 + + int Factorial(int n, int uselessInView3 = 0) // Noncompliant + { + if (n == 0) + { + return 1; + } + return n * Factorial(n - 1); + } +} + +
+
\ No newline at end of file diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/Razor/EmptyProject/EmptyProject.csproj b/analyzers/tests/SonarAnalyzer.Test/TestCases/Razor/EmptyProject/EmptyProject.csproj index e665be6db1f..0379cbbd00a 100644 --- a/analyzers/tests/SonarAnalyzer.Test/TestCases/Razor/EmptyProject/EmptyProject.csproj +++ b/analyzers/tests/SonarAnalyzer.Test/TestCases/Razor/EmptyProject/EmptyProject.csproj @@ -1,11 +1,165 @@ - + + + + + + Debug + AnyCPU + + + 2.0 + {8704E7D7-B73F-4648-B750-2D084F84B673} + {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} + Library + Properties + Lol + Lol + v4.8.1 + true + + 44346 + + + + + + + + + true + . + + + true + full + false + bin\ + DEBUG;TRACE + prompt + 4 + + + true + pdbonly + true + bin\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + True + ..\packages\Microsoft.Web.Infrastructure.2.0.1\lib\net40\Microsoft.Web.Infrastructure.dll + + + + + + + True + ..\packages\Microsoft.AspNet.WebPages.3.2.9\lib\net45\System.Web.Helpers.dll + + + True + ..\packages\Microsoft.AspNet.Mvc.5.2.9\lib\net45\System.Web.Mvc.dll + + + ..\packages\Microsoft.AspNet.Web.Optimization.1.1.3\lib\net40\System.Web.Optimization.dll + + + True + ..\packages\Microsoft.AspNet.Razor.3.2.9\lib\net45\System.Web.Razor.dll + + + True + ..\packages\Microsoft.AspNet.WebPages.3.2.9\lib\net45\System.Web.WebPages.dll + + + True + ..\packages\Microsoft.AspNet.WebPages.3.2.9\lib\net45\System.Web.WebPages.Deployment.dll + + + True + ..\packages\Microsoft.AspNet.WebPages.3.2.9\lib\net45\System.Web.WebPages.Razor.dll + + + ..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll + + + True + ..\packages\WebGrease.1.6.0\lib\WebGrease.dll + + + True + ..\packages\Antlr.3.5.0.2\lib\Antlr3.Runtime.dll + + + + + ..\packages\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.2.0.1\lib\net45\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.dll + + + + + + + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + + + + + + + + True + True + 61091 + / + https://localhost:44346/ + False + False + + + False + + + + + - net7.0 - latest - enable - enable - Library + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - + + + \ No newline at end of file diff --git a/analyzers/tests/SonarAnalyzer.Test/packages.lock.json b/analyzers/tests/SonarAnalyzer.Test/packages.lock.json index bfde5d134da..cf7a3c34f38 100644 --- a/analyzers/tests/SonarAnalyzer.Test/packages.lock.json +++ b/analyzers/tests/SonarAnalyzer.Test/packages.lock.json @@ -82,6 +82,11 @@ "resolved": "2.14.1", "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==" }, + "Microsoft.AspNetCore.Razor.Language": { + "type": "Transitive", + "resolved": "6.0.29", + "contentHash": "GqbAfFQEv9XKi+QRlhzoYJvbi9EkFo0rkzvaP1xGvn2Hjn1DrhGK93UuJ8zLw8FQ9jmW1GOMLXLSPJoID4lLbg==" + }, "Microsoft.Bcl.AsyncInterfaces": { "type": "Transitive", "resolved": "8.0.0", @@ -560,6 +565,7 @@ "sonaranalyzer.csharp": { "type": "Project", "dependencies": { + "Microsoft.AspNetCore.Razor.Language": "[6.0.29, )", "Microsoft.CodeAnalysis.CSharp.Workspaces": "[1.3.2, )", "SonarAnalyzer": "[1.0.0, )", "System.Collections.Immutable": "[1.1.37, )" @@ -676,6 +682,11 @@ "resolved": "2.14.1", "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==" }, + "Microsoft.AspNetCore.Razor.Language": { + "type": "Transitive", + "resolved": "6.0.29", + "contentHash": "GqbAfFQEv9XKi+QRlhzoYJvbi9EkFo0rkzvaP1xGvn2Hjn1DrhGK93UuJ8zLw8FQ9jmW1GOMLXLSPJoID4lLbg==" + }, "Microsoft.Build": { "type": "Transitive", "resolved": "17.3.2", @@ -1149,6 +1160,7 @@ "sonaranalyzer.csharp": { "type": "Project", "dependencies": { + "Microsoft.AspNetCore.Razor.Language": "[6.0.29, )", "Microsoft.CodeAnalysis.CSharp.Workspaces": "[1.3.2, )", "SonarAnalyzer": "[1.0.0, )", "System.Collections.Immutable": "[1.1.37, )" diff --git a/analyzers/tests/SonarAnalyzer.TestFramework.Test/packages.lock.json b/analyzers/tests/SonarAnalyzer.TestFramework.Test/packages.lock.json index bb3dce94898..a0459b31577 100644 --- a/analyzers/tests/SonarAnalyzer.TestFramework.Test/packages.lock.json +++ b/analyzers/tests/SonarAnalyzer.TestFramework.Test/packages.lock.json @@ -79,6 +79,11 @@ "resolved": "2.14.1", "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==" }, + "Microsoft.AspNetCore.Razor.Language": { + "type": "Transitive", + "resolved": "6.0.29", + "contentHash": "GqbAfFQEv9XKi+QRlhzoYJvbi9EkFo0rkzvaP1xGvn2Hjn1DrhGK93UuJ8zLw8FQ9jmW1GOMLXLSPJoID4lLbg==" + }, "Microsoft.Build": { "type": "Transitive", "resolved": "17.3.2", @@ -560,6 +565,7 @@ "sonaranalyzer.csharp": { "type": "Project", "dependencies": { + "Microsoft.AspNetCore.Razor.Language": "[6.0.29, )", "Microsoft.CodeAnalysis.CSharp.Workspaces": "[1.3.2, )", "SonarAnalyzer": "[1.0.0, )", "System.Collections.Immutable": "[1.1.37, )" diff --git a/analyzers/tests/SonarAnalyzer.TestFramework/Common/AnalysisScaffolding.cs b/analyzers/tests/SonarAnalyzer.TestFramework/Common/AnalysisScaffolding.cs index 39fadee7d18..9aee280b36f 100644 --- a/analyzers/tests/SonarAnalyzer.TestFramework/Common/AnalysisScaffolding.cs +++ b/analyzers/tests/SonarAnalyzer.TestFramework/Common/AnalysisScaffolding.cs @@ -75,6 +75,9 @@ public static string CreateSonarProjectConfigWithFilesToAnalyze(TestContext cont return CreateSonarProjectConfig(context, "FilesToAnalyzePath", filesToAnalyzePath, true); } + public static string CreateSonarProjectConfig(TestContext context, string analysisConfigPath) => + CreateSonarProjectConfig(context, "NotImportant", null, true, analysisConfigPath); + public static string CreateSonarProjectConfigWithUnchangedFiles(TestContext context, params string[] unchangedFiles) => CreateSonarProjectConfig(context, "NotImportant", null, true, CreateAnalysisConfig(context, unchangedFiles)); @@ -150,7 +153,7 @@ private static XElement CreateKeyValuePair(string containerName, string key, str private static string ConcatenateStringArray(string[] array) => string.Join(",", array ?? Array.Empty()); - private static string CreateSonarProjectConfig(TestContext context, string element, string value, bool isScannerRun, string analysisConfigPath = null) + public static string CreateSonarProjectConfig(TestContext context, string element, string value, bool isScannerRun, string analysisConfigPath = null) { var sonarProjectConfigPath = TestHelper.TestPath(context, "SonarProjectConfig.xml"); var outPath = isScannerRun ? Path.GetDirectoryName(sonarProjectConfigPath) : null; diff --git a/analyzers/tests/SonarAnalyzer.TestFramework/Verification/DiagnosticVerifier.cs b/analyzers/tests/SonarAnalyzer.TestFramework/Verification/DiagnosticVerifier.cs index 24659711965..431b9b2cc49 100644 --- a/analyzers/tests/SonarAnalyzer.TestFramework/Verification/DiagnosticVerifier.cs +++ b/analyzers/tests/SonarAnalyzer.TestFramework/Verification/DiagnosticVerifier.cs @@ -46,10 +46,21 @@ public static void Verify( SuppressionHandler.HookSuppression(); try { + var diagnostics = DiagnosticsAndErrors(compilation, analyzers, checkMode, additionalFilePath, onlyDiagnostics).ToArray(); + + var externalFilePaths = diagnostics + .Where(x => x.Location.Kind == LocationKind.ExternalFile) + .Select(x => x.Location.GetLineSpan().Path) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Distinct() + .Select(x => new FileContent(x)) + .ToList(); + var sources = compilation.SyntaxTrees.ExceptRazorGeneratedFiles() .Select(x => new FileContent(x)) - .Concat((additionalSourceFiles ?? Array.Empty()).Select(x => new FileContent(x))); - var diagnostics = DiagnosticsAndErrors(compilation, analyzers, checkMode, additionalFilePath, onlyDiagnostics).ToArray(); + .Concat((additionalSourceFiles ?? Array.Empty()).Select(x => new FileContent(x))) + .Concat(externalFilePaths); + var expected = new CompilationIssues(sources); VerifyNoExceptionThrown(diagnostics); Compare(compilation.LanguageVersionString(), new(diagnostics), expected); diff --git a/analyzers/tests/SonarAnalyzer.TestFramework/Verification/Verifier.cs b/analyzers/tests/SonarAnalyzer.TestFramework/Verification/Verifier.cs index d32daa9dd4f..ea8e2e0ee69 100644 --- a/analyzers/tests/SonarAnalyzer.TestFramework/Verification/Verifier.cs +++ b/analyzers/tests/SonarAnalyzer.TestFramework/Verification/Verifier.cs @@ -231,12 +231,12 @@ private string PrepareRazorProject(string projectRoot, string langVersion) } var csProjPath = Path.Combine(projectRoot, "EmptyProject.csproj"); var xml = XElement.Load(csProjPath); - xml.Descendants("LangVersion").Single().Value = langVersion; - var references = xml.Descendants("ItemGroup").Single(); - foreach (var reference in builder.References) - { - references.Add(new XElement("Reference", new XAttribute("Include", reference.Display))); - } + //xml.Descendants("LangVersion").Single().Value = langVersion; + //var references = xml.Descendants("ItemGroup").Single(); + //foreach (var reference in builder.References) + //{ + // references.Add(new XElement("Reference", new XAttribute("Include", reference.Display))); + //} xml.Save(csProjPath); return csProjPath; }