File: SourceGenerators\RazorSourceGenerator.RazorProviders.cs
Web Access
Project: src\src\Razor\src\Compiler\Microsoft.CodeAnalysis.Razor.Compiler\src\Microsoft.CodeAnalysis.Razor.Compiler.csproj (Microsoft.CodeAnalysis.Razor.Compiler)
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
using System;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Razor.Compiler.CSharp;
 
namespace Microsoft.NET.Sdk.Razor.SourceGenerators
{
    public partial class RazorSourceGenerator
    {
        private (RazorSourceGenerationOptions?, ImmutableArray<Diagnostic>) ComputeRazorSourceGeneratorOptions(((AnalyzerConfigOptionsProvider, ParseOptions), ImmutableArray<MetadataReference>) pair, CancellationToken ct)
        {
            var ((options, parseOptions), references) = pair;
            var globalOptions = options.GlobalOptions;
 
            Log.ComputeRazorSourceGeneratorOptions();
 
            globalOptions.TryGetValue("build_property.RazorConfiguration", out var configurationName);
            globalOptions.TryGetValue("build_property.RootNamespace", out var rootNamespace);
            globalOptions.TryGetValue("build_property.SupportLocalizedComponentNames", out var supportLocalizedComponentNames);
            globalOptions.TryGetValue("build_property.GenerateRazorMetadataSourceChecksumAttributes", out var generateMetadataSourceChecksumAttributes);
 
            using var diagnostics = new PooledArrayBuilder<Diagnostic>(capacity: 2);
 
            var razorLanguageVersion = ParseRazorLanguageVersion(globalOptions, ref diagnostics.AsRef());
            var razorWarningLevel = ParseRazorWarningLevel(globalOptions, razorLanguageVersion, ref diagnostics.AsRef());
 
            var minimalReferences = references
                .Where(r => r.Display is { } display && display.EndsWith("Microsoft.AspNetCore.Components.dll", StringComparison.Ordinal))
                .ToImmutableArray();
 
            var isComponentParameterSupported = minimalReferences.Length == 0
                ? false
                : CSharpCompilation.Create("components", references: minimalReferences).HasAddComponentParameter();
 
            var razorConfiguration = new RazorConfiguration(razorLanguageVersion, configurationName ?? "default", Extensions: [], UseConsolidatedMvcViews: true, SuppressAddComponentParameter: !isComponentParameterSupported, RazorWarningLevel: razorWarningLevel);
 
            // We use the new tokenizer only when requested for now.
            var useRoslynTokenizer = parseOptions.UseRoslynTokenizer();
 
            var razorSourceGenerationOptions = new RazorSourceGenerationOptions()
            {
                Configuration = razorConfiguration,
                GenerateMetadataSourceChecksumAttributes = generateMetadataSourceChecksumAttributes == "true",
                RootNamespace = rootNamespace ?? "ASP",
                SupportLocalizedComponentNames = supportLocalizedComponentNames == "true",
                CSharpParseOptions = (CSharpParseOptions)parseOptions,
                TestSuppressUniqueIds = _testSuppressUniqueIds,
                UseRoslynTokenizer = useRoslynTokenizer,
            };
 
            return (razorSourceGenerationOptions, diagnostics.ToImmutableAndClear());
        }
 
        private static RazorLanguageVersion ParseRazorLanguageVersion(AnalyzerConfigOptions globalOptions, ref PooledArrayBuilder<Diagnostic> diagnostics)
        {
            if (!globalOptions.TryGetValue("build_property.RazorLangVersion", out var razorLanguageVersionString) ||
                !RazorLanguageVersion.TryParse(razorLanguageVersionString, out var razorLanguageVersion))
            {
                diagnostics.Add(Diagnostic.Create(
                    RazorDiagnostics.InvalidRazorLangVersionDescriptor,
                    Location.None,
                    razorLanguageVersionString,
                    RazorLanguageVersion.Preview.ToString()));
                return RazorLanguageVersion.Latest;
            }
 
            return razorLanguageVersion;
        }
 
        private static int ParseRazorWarningLevel(AnalyzerConfigOptions globalOptions, RazorLanguageVersion razorLanguageVersion, ref PooledArrayBuilder<Diagnostic> diagnostics)
        {
            if (!globalOptions.TryGetValue("build_property.RazorWarningLevel", out var razorWarningLevelString))
            {
                // Property not registered - old SDK that doesn't know about warning waves.
                // Default to 0 so no wave-gated warnings are reported.
                return 0;
            }
 
            if (string.IsNullOrEmpty(razorWarningLevelString))
            {
                // Property registered but not set - new SDK, use language version default.
                return razorLanguageVersion.GetDefaultWarningLevel();
            }
 
            if (int.TryParse(razorWarningLevelString, out var parsedLevel) && parsedLevel >= 0)
            {
                return parsedLevel;
            }
 
            diagnostics.Add(Diagnostic.Create(
                RazorDiagnostics.InvalidRazorWarningLevelDescriptor,
                Location.None,
                razorWarningLevelString));
            return razorLanguageVersion.GetDefaultWarningLevel();
        }
 
        private static (SourceGeneratorProjectItem?, Diagnostic?) ComputeProjectItems((AdditionalText, AnalyzerConfigOptionsProvider) pair, CancellationToken ct)
        {
            var (additionalText, globalOptions) = pair;
            var options = globalOptions.GetOptions(additionalText);
 
            string? relativePath = null;
            var hasTargetPath = options.TryGetValue("build_metadata.AdditionalFiles.TargetPath", out var encodedRelativePath);
            if (hasTargetPath && !string.IsNullOrWhiteSpace(encodedRelativePath))
            {
                relativePath = Encoding.UTF8.GetString(Convert.FromBase64String(encodedRelativePath));
            }
            else if (globalOptions.GlobalOptions.TryGetValue("build_property.MSBuildProjectDirectory", out var projectPath) &&
                projectPath is { Length: > 0 } &&
                additionalText.Path.StartsWith(projectPath, StringComparison.OrdinalIgnoreCase))
            {
                // Fallback, when TargetPath isn't specified but we know about the project directory, we can do our own calulation of
                // the project relative path, and use that as the target path. This is an easy way for a project that isn't using the
                // Razor SDK to still get TargetPath functionality without the complexity of specifying metadata on every item.
                relativePath = additionalText.Path[projectPath.Length..].TrimStart(['/', '\\']);
            }
            else if (!hasTargetPath)
            {
                // If the TargetPath is not provided, it could be a Misc Files situation, or just a project that isn't using the
                // Web or Razor SDK. In this case, we just use the physical path.
                relativePath = additionalText.Path;
            }
 
            if (relativePath is null)
            {
                // If we had a TargetPath but it was empty or whitespace, and we couldn't fall back to computing it from the project path
                // that's an error.
                var diagnostic = Diagnostic.Create(
                    RazorDiagnostics.TargetPathNotProvided,
                    Location.None,
                    additionalText.Path);
                return (null, diagnostic);
            }
 
            options.TryGetValue("build_metadata.AdditionalFiles.CssScope", out var cssScope);
 
            var projectItem = new SourceGeneratorProjectItem(
                basePath: "/",
                filePath: '/' + relativePath
                    .Replace(Path.DirectorySeparatorChar, '/')
                    .Replace("//", "/"),
                relativePhysicalPath: relativePath,
                fileKind: FileKinds.GetFileKindFromPath(additionalText.Path),
                additionalText: additionalText,
                cssScope: cssScope);
            return (projectItem, null);
        }
    }
}