Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DO NOT MERGE] Make a POC around in-memory compilation of .NET Framework views #9244

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from

Conversation

gregory-paidis-sonarsource
Copy link
Contributor

No description provided.

namespace SonarAnalyzer.Rules.CSharp;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class FrameworkViewCompiler : SonarDiagnosticAnalyzer
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the entrypoint.
The goal of this analyzer is to make one dummy compilation (or one dummy compilation per view file) and slap our analyzers on top of it. Then we can ask for diagnostics and filter accordingly.

{
// TODO: Read this from context.ProjectConfiguration().ProjectPath.
// Not sure how to set it up in the UTs.
private const string ROOT_DIR = """C:\dev\personal\aspnet\Lol""";
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check TODO.

If you want to debug locally, make a boilerplate ASP.NET Framework project and put the fullpath here.
Optimally, we should use additional files, just like we do from the ProjectReader.

context.RegisterCompilationAction(
c =>
{
var mightBeUseful = c.ProjectConfiguration();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure how to mock this in UTs, but it would be useful.

foreach (var diagnostic in diagnostics.Where(x => supportedDiagnostics.Keys.Contains(x.Id)))
{
// TODO: This breaks when raising on out-of-compilation files. Find a replacement, maybe writing directly to the SARIF.
c.ReportIssue(diagnostic);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See this thread.

The main problem is that:

  • On .NET Core, the Views are part of the compilation...but we do not care, as they are compiled anyways and we get analysis (almost) for free.
  • On .NET Framework the Views are NOT part of the compilation, so if we propagate the diagnostic with ReportIssue, roslyn throws an AD0001.


protected override void Initialize(SonarAnalysisContext context) =>

context.RegisterCompilationAction(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to optionally register.

  • Opt-in only for .NET FM projects that contain cshtml files.
  • Opt-out on SonarLint.

...probably more filtering required.

var razorCompiler = new RazorCompiler(ROOT_DIR, filesProvider);
var dummyCompilation = compilation;

foreach (var razorDocument in razorCompiler.CompileAll().Where(x => x.Source.FilePath.Contains("Contact")))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .Where(x => x.Source.FilePath.Contains("Contact")) is a hacky patch to check specific file locally, feel free to alter/remove it.

{
if (razorDocument.GetCSharpDocument()?.GeneratedCode is { } csharpCode)
{
var fp = dummyCompilation.SyntaxTrees.FirstOrDefault(x => x.FilePath.Contains("Contact")).FilePath;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a hack so that the syntax tree we pass on AddSyntaxTrees is not name-less, to avoid AD0001s.

}
#region RULE_FINDER
// Copied from the test framework.
internal static class RuleFinder2
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TLDR: This class uses reflection and fetches all of our rules from the same assembly.

{
types = types.Concat(UtilityAnalyzerTypes.Where(x => TargetLanguage(x) == language));
}
foreach (var type in types.Where(x => x == typeof(VariableUnused))) // TODO: Remove ".Where(x => x == typeof(VariableUnused))" and see error. Locally it breaks for me with StackOverflow, at a compiled Regex.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check TODO.

We will probably need to exclude some of our rules.
For now, we can add one or two just to test. I chose S1481 because it's really braindead and easy to add issues for.

/// 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.
/// </summary>
internal sealed class RazorCompiler
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the magic/hack part.
This package cross-compiles cshtml -> c#, but only for .NET Core.

So we do a lot of dirty things to make it play a bit better with the .NET FM type system.

.AddReferences(AspNet4xReferences("5.2.7"))
.AddSnippet("""
@{
var uselessInView = 42;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, we would like to raise here.

@@ -1,11 +1,167 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<?xml version="1.0" encoding="utf-8"?>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made this a .NET FM project, to test a bit more realistically.

I think a better way is by attaching a debugger, especially in the initial stages, where we do not want to find the UT framework.

<ItemGroup>
<Compile Include="Contact.cshtml" />
<!--OR-->
<Content Include="Contact.cshtml" />
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HEY HEY! This is an important bit.

Line 123: Uses Compile. What happens:

  • The file is added to the compilation by MSBuild.
  • Roslyn does not understand what is going on, so it just makes an AST where the root node is the entire file 🤣
  • BECAUSE it is part of the compilation, we raise successfully issues.

Line 125: Uses Content. What happens:

  • The file is added as an additional, content file.
  • Roslyn does not have it in the compilation context.
  • If we raise inside it, we get an AD0001.

...unfortunately, the default behavior for views is line 125.

I think this concludes the fact that we CANNOT propagate the issue by using .ReportIssue on the analyzer.
But we could write the issues on disk, probably.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update:
After looking more into this, Content is fine.
If we name the generated file cshtml.g.cs and the location mappings work, the cshtml file is considered an ExternalFile, and raising there is accepted, so we can use ReportIssue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants