Skip to content

Commit

Permalink
Implement location mapping for razor files
Browse files Browse the repository at this point in the history
  • Loading branch information
costin-zaharia-sonarsource committed Aug 30, 2023
1 parent 858a1da commit e7695b5
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 41 deletions.
@@ -0,0 +1,29 @@
/*
* SonarAnalyzer for .NET
* Copyright (C) 2015-2023 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.
*/

namespace SonarAnalyzer.Extensions;

public static class LocationExtensions
{
public static FileLinePositionSpan GetMappedLineSpanIfAvailable(this Location location) =>
GeneratedCodeRecognizer.IsRazorGeneratedFile(location.SourceTree)
? location.GetMappedLineSpan()
: location.GetLineSpan();
}
Expand Up @@ -38,7 +38,7 @@ public static bool IsGenerated(this SyntaxTree tree, GeneratedCodeRecognizer gen
return false;
}
var cache = GeneratedCodeCache.GetOrCreateValue(compilation);
// Hot path: Don't use cache.GetOrAdd that takes a factory method. It allocates a delegate which causes GC preasure.
// Hot path: Don't use cache.GetOrAdd that takes a factory method. It allocates a delegate which causes GC pressure.
return cache.TryGetValue(tree, out var isGenerated)
? isGenerated
: cache.GetOrAdd(tree, generatedCodeRecognizer.IsGenerated(tree));
Expand Down
Expand Up @@ -18,7 +18,6 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

using Microsoft.CodeAnalysis.Text;
using SonarAnalyzer.Protobuf;

namespace SonarAnalyzer.Rules
Expand All @@ -42,22 +41,21 @@ public abstract class SymbolReferenceAnalyzerBase<TSyntaxKind> : UtilityAnalyzer

protected sealed override SymbolReferenceInfo CreateMessage(SyntaxTree syntaxTree, SemanticModel semanticModel)
{
var symbolReferenceInfo = new SymbolReferenceInfo { FilePath = syntaxTree.FilePath };
var filePath = GetFilePath(syntaxTree);
var symbolReferenceInfo = new SymbolReferenceInfo { FilePath = filePath };
var references = GetReferences(syntaxTree.GetRoot(), semanticModel);

foreach (var symbol in references.Keys)
{
if (GetSymbolReference(references[symbol], syntaxTree) is { } reference)
if (GetSymbolReference(references[symbol], filePath) is { } reference)
{
symbolReferenceInfo.Reference.Add(reference);
}
}

return symbolReferenceInfo;
}

protected override bool ShouldGenerateMetrics(SyntaxTree tree) =>
base.ShouldGenerateMetrics(tree)
protected override bool ShouldGenerateMetrics(SyntaxTree tree, Compilation compilation) =>
base.ShouldGenerateMetrics(tree, compilation)
&& !HasTooManyTokens(tree);

private Dictionary<ISymbol, List<ReferenceInfo>> GetReferences(SyntaxNode root, SemanticModel model)
Expand All @@ -80,7 +78,8 @@ protected sealed override SymbolReferenceInfo CreateMessage(SyntaxTree syntaxTre
var currentDeclaration = declarationReferences[j];
if (currentDeclaration.Symbol != null)
{
references.GetOrAdd(currentDeclaration.Symbol, _ => new List<ReferenceInfo>())
references
.GetOrAdd(currentDeclaration.Symbol, _ => new List<ReferenceInfo>())
.Add(currentDeclaration);
knownNodes.Add(currentDeclaration.Node);
knownIdentifiers.Add(currentDeclaration.Identifier.ValueText);
Expand Down Expand Up @@ -112,33 +111,37 @@ protected sealed override SymbolReferenceInfo CreateMessage(SyntaxTree syntaxTre
var symbol => symbol
};

private static SymbolReferenceInfo.Types.SymbolReference GetSymbolReference(List<ReferenceInfo> references, SyntaxTree tree)
private static SymbolReferenceInfo.Types.SymbolReference GetSymbolReference(IReadOnlyList<ReferenceInfo> references, string filePath)
{
var declarationSpan = GetDeclarationSpan(references);
var declarationSpan = GetDeclarationSpan(references, filePath);
if (!declarationSpan.HasValue)
{
return null;
}

var symbolReference = new SymbolReferenceInfo.Types.SymbolReference { Declaration = GetTextRange(Location.Create(tree, declarationSpan.Value).GetLineSpan()) };
var symbolReference = new SymbolReferenceInfo.Types.SymbolReference { Declaration = GetTextRange(declarationSpan.Value) };
for (var i = 0; i < references.Count; i++)
{
var reference = references[i];
if (!reference.IsDeclaration)
if (!reference.IsDeclaration
&& reference.Identifier.GetLocation().GetMappedLineSpanIfAvailable() is var mappedLineSpan
&& string.Equals(mappedLineSpan.Path, filePath, StringComparison.OrdinalIgnoreCase))
{
symbolReference.Reference.Add(GetTextRange(Location.Create(tree, reference.Identifier.Span).GetLineSpan()));
symbolReference.Reference.Add(GetTextRange(mappedLineSpan));
}
}
return symbolReference;
}

private static TextSpan? GetDeclarationSpan(List<ReferenceInfo> references)
private static FileLinePositionSpan? GetDeclarationSpan(IReadOnlyList<ReferenceInfo> references, string filePath)
{
for (var i = 0; i < references.Count; i++)
{
if (references[i].IsDeclaration)
if (references[i].IsDeclaration
&& references[i].Identifier.GetLocation().GetMappedLineSpanIfAvailable() is var mappedLineSpan
&& string.Equals(mappedLineSpan.Path, filePath, StringComparison.OrdinalIgnoreCase))
{
return references[i].Identifier.Span;
return mappedLineSpan;
}
}
return null;
Expand Down
Expand Up @@ -110,14 +110,20 @@ public abstract class UtilityAnalyzerBase<TSyntaxKind, TMessage> : UtilityAnalyz
}
});

protected virtual bool ShouldGenerateMetrics(SyntaxTree tree) =>
protected virtual bool ShouldGenerateMetrics(SyntaxTree tree, Compilation compilation) =>
// The results of Metrics and CopyPasteToken analyzers are not needed for Test projects yet the plugin side expects the protobuf files, so we create empty ones.
(AnalyzeTestProjects || !IsTestProject)
&& FileExtensionWhitelist.Contains(Path.GetExtension(tree.FilePath))
&& (AnalyzeGeneratedCode || !Language.GeneratedCodeRecognizer.IsGenerated(tree));
&& (AnalyzeGeneratedCode || !tree.IsConsideredGenerated(Language.GeneratedCodeRecognizer, compilation));

protected static string GetFilePath(SyntaxTree syntaxTree) =>
// If the syntax tree is constructed for a razor generated file, we need to provide the original file path.
GeneratedCodeRecognizer.IsRazorGeneratedFile(syntaxTree) && syntaxTree.GetRoot() is var root && root.ContainsDirectives
? root.GetMappedFilePathFromRoot()
: syntaxTree.FilePath;

private bool ShouldGenerateMetrics(SonarCompilationReportingContext context, SyntaxTree tree) =>
(AnalyzeUnchangedFiles || !context.IsUnchanged(tree))
&& ShouldGenerateMetrics(tree);
&& ShouldGenerateMetrics(tree, context.Compilation);
}
}
Expand Up @@ -21,6 +21,7 @@
using System.IO;
using SonarAnalyzer.Protobuf;
using SonarAnalyzer.Rules;
using SonarAnalyzer.UnitTest.Helpers;
using CS = SonarAnalyzer.Rules.CSharp;
using VB = SonarAnalyzer.Rules.VisualBasic;

Expand Down Expand Up @@ -190,34 +191,36 @@ public void Verify_UnchangedFiles(string unchangedFileName, bool expectedProtobu
#if NET

[TestMethod]
public void Verify_Razor() =>
public void Verify_Razor()
{
using var scope = new EnvironmentVariableScope(false) { EnableRazorAnalysis = true };

// Currently only the symbols from .cs files are computed since .razor support is not yet implemented.
CreateBuilder(ProjectType.Product, "Razor.razor", "ToDo.cs")
.WithConcurrentAnalysis(false)
.VerifyUtilityAnalyzer<SymbolReferenceInfo>(symbols =>
{
symbols.Should().ContainSingle();
symbols[0].FilePath.Should().EndWith("ToDo.cs");
symbols[0].Reference
.Select(x => x.Declaration)
.Should()
.BeEquivalentTo(new[]
{
new TextRange { StartLine = 3, EndLine = 3, StartOffset = 13, EndOffset = 17 },
new TextRange { StartLine = 5, EndLine = 5, StartOffset = 19, EndOffset = 24 },
new TextRange { StartLine = 6, EndLine = 6, StartOffset = 16, EndOffset = 22 }
});
});
{
var orderedSymbols = symbols.OrderBy(x => x.FilePath).ToArray();
orderedSymbols.Select(x => Path.GetFileName(x.FilePath)).Should().Contain("_Imports.razor", "Razor.razor", "ToDo.cs");
orderedSymbols[0].FilePath.Should().EndWith("_Imports.razor");
orderedSymbols[1].FilePath.Should().EndWith("Razor.razor");
VerifyReferences(orderedSymbols[1].Reference, 9, 13, 4, 6, 20); // currentCount
VerifyReferences(orderedSymbols[1].Reference, 9, 16, 10, 20, 21); // IncrementAmount
VerifyReferences(orderedSymbols[1].Reference, 9, 18, 8); // IncrementCount
VerifyReferences(orderedSymbols[1].Reference, 9, 34, 34); // x
VerifyReferences(orderedSymbols[1].Reference, 9, 37, 28, 34); // todos
VerifyReferences(orderedSymbols[1].Reference, 9, 39, 25); // AddTodo
VerifyReferences(orderedSymbols[1].Reference, 9, 41); // x
VerifyReferences(orderedSymbols[1].Reference, 9, 42); // y
VerifyReferences(orderedSymbols[1].Reference, 9, 44, 41); // LocalMethod
});
}

#endif

private void Verify(string fileName, ProjectType projectType, int expectedDeclarationCount, int assertedDeclarationLine, params int[] assertedDeclarationLineReferences) =>
Verify(fileName, projectType, references =>
{
references.Where(x => x.Declaration != null).Should().HaveCount(expectedDeclarationCount);
var declarationReferences = references.Single(x => x.Declaration.StartLine == assertedDeclarationLine).Reference;
declarationReferences.Select(x => x.StartLine).Should().BeEquivalentTo(assertedDeclarationLineReferences);
});
Verify(fileName, projectType, references => VerifyReferences(references, expectedDeclarationCount, assertedDeclarationLine, assertedDeclarationLineReferences));

private void Verify(string fileName,
ProjectType projectType,
Expand Down Expand Up @@ -255,6 +258,16 @@ private VerifierBuilder CreateBuilder(ProjectType projectType, params string[] f
.WithProtobufPath(@$"{testRoot}\symrefs.pb");
}

private static void VerifyReferences(IReadOnlyList<SymbolReferenceInfo.Types.SymbolReference> references,
int expectedDeclarationCount,
int assertedDeclarationLine,
params int[] assertedDeclarationLineReferences)
{
references.Where(x => x.Declaration != null).Should().HaveCount(expectedDeclarationCount);
var declarationReferences = references.Single(x => x.Declaration.StartLine == assertedDeclarationLine).Reference;
declarationReferences.Select(x => x.StartLine).Should().BeEquivalentTo(assertedDeclarationLineReferences);
}

// We need to set protected properties and this class exists just to enable the analyzer without bothering with additional files with parameters
private sealed class TestSymbolReferenceAnalyzer_CS : CS.SymbolReferenceAnalyzer
{
Expand Down
Expand Up @@ -31,7 +31,7 @@
}
</ul>

<h3>Todo (@todos.Count(todo => !todo.IsDone))</h3>
<h3>Todo (@todos.Count(x => !x.IsDone))</h3>

@code {
private List<ToDo> todos = new();
Expand Down

0 comments on commit e7695b5

Please sign in to comment.