File: Language\RazorProjectEngine.cs
Web Access
Project: src\src\Razor\src\Compiler\Microsoft.CodeAnalysis.Razor.Compiler\src\Microsoft.CodeAnalysis.Razor.Compiler.csproj (Microsoft.CodeAnalysis.Razor.Compiler)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language.Components;
using Microsoft.AspNetCore.Razor.Language.Extensions;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
using Microsoft.AspNetCore.Razor.PooledObjects;
 
namespace Microsoft.AspNetCore.Razor.Language;
 
public sealed class RazorProjectEngine
{
    public RazorConfiguration Configuration { get; }
    public RazorProjectFileSystem FileSystem { get; }
    public RazorEngine Engine { get; }
    public ImmutableArray<IRazorEnginePhase> Phases => Engine.Phases;
    public ImmutableArray<IRazorProjectEngineFeature> Features { get; }
 
    private readonly FeatureCache<IRazorProjectEngineFeature> _featureCache;
 
    private readonly ImmutableArray<IConfigureRazorParserOptionsFeature> _configureParserOptionsFeatures;
    private readonly ImmutableArray<IConfigureRazorCodeGenerationOptionsFeature> _configureCodeGenerationOptionsFeatures;
 
    internal RazorProjectEngine(
        RazorConfiguration configuration,
        RazorEngine engine,
        RazorProjectFileSystem fileSystem,
        ImmutableArray<IRazorProjectEngineFeature> features)
    {
        Configuration = configuration;
        Engine = engine;
        FileSystem = fileSystem;
        Features = features;
 
        _featureCache = new(features);
 
        foreach (var projectFeature in features)
        {
            projectFeature.Initialize(this);
        }
 
        _configureParserOptionsFeatures = Engine
            .GetFeatures<IConfigureRazorParserOptionsFeature>()
            .OrderByAsArray(static x => x.Order);
 
        _configureCodeGenerationOptionsFeatures = Engine
            .GetFeatures<IConfigureRazorCodeGenerationOptionsFeature>()
            .OrderByAsArray(static x => x.Order);
    }
 
    public ImmutableArray<TFeature> GetFeatures<TFeature>()
        where TFeature : class, IRazorProjectEngineFeature
        => _featureCache.GetFeatures<TFeature>();
 
    public RazorCodeDocument Process(RazorProjectItem projectItem, CancellationToken cancellationToken = default)
    {
        ArgHelper.ThrowIfNull(projectItem);
 
        var codeDocument = CreateCodeDocumentCore(projectItem);
        return ProcessCore(codeDocument, cancellationToken);
    }
 
    public RazorCodeDocument Process(
        RazorSourceDocument source,
        RazorFileKind fileKind,
        ImmutableArray<RazorSourceDocument> importSources,
        TagHelperCollection? tagHelpers,
        CancellationToken cancellationToken = default)
    {
        ArgHelper.ThrowIfNull(source);
        ArgHelper.ThrowIfNull(fileKind);
 
        var codeDocument = CreateCodeDocumentCore(source, fileKind, importSources, tagHelpers, cssScope: null, configureParser: null, configureCodeGeneration: null);
        return ProcessCore(codeDocument, cancellationToken);
    }
 
    public RazorCodeDocument ProcessDeclarationOnly(RazorProjectItem projectItem, CancellationToken cancellationToken = default)
    {
        ArgHelper.ThrowIfNull(projectItem);
 
        var codeDocument = CreateCodeDocumentCore(projectItem, configureParser: null, configureCodeGeneration: (builder) =>
        {
            builder.SuppressPrimaryMethodBody = true;
        });
 
        return ProcessCore(codeDocument, cancellationToken);
    }
 
    public RazorCodeDocument ProcessDeclarationOnly(
        RazorSourceDocument source,
        RazorFileKind fileKind,
        ImmutableArray<RazorSourceDocument> importSources,
        TagHelperCollection? tagHelpers,
        CancellationToken cancellationToken = default)
    {
        ArgHelper.ThrowIfNull(source);
        ArgHelper.ThrowIfNull(fileKind);
 
        var codeDocument = CreateCodeDocumentCore(source, fileKind, importSources, tagHelpers, cssScope: null, configureParser: null, configureCodeGeneration: (builder) =>
        {
            builder.SuppressPrimaryMethodBody = true;
        });
 
        return ProcessCore(codeDocument, cancellationToken);
    }
 
    public RazorCodeDocument ProcessDesignTime(RazorProjectItem projectItem, CancellationToken cancellationToken = default)
    {
        ArgHelper.ThrowIfNull(projectItem);
 
        var codeDocument = CreateCodeDocumentDesignTimeCore(projectItem);
        return ProcessCore(codeDocument, cancellationToken);
    }
 
    public RazorCodeDocument ProcessDesignTime(
        RazorSourceDocument source,
        RazorFileKind fileKind,
        ImmutableArray<RazorSourceDocument> importSources,
        TagHelperCollection? tagHelpers,
        CancellationToken cancellationToken = default)
    {
        ArgHelper.ThrowIfNull(source);
        ArgHelper.ThrowIfNull(fileKind);
 
        var codeDocument = CreateCodeDocumentDesignTimeCore(source, fileKind, importSources, tagHelpers, configureParser: null, configureCodeGeneration: null);
        return ProcessCore(codeDocument, cancellationToken);
    }
 
    internal RazorCodeDocument CreateCodeDocument(RazorProjectItem projectItem)
    {
        ArgHelper.ThrowIfNull(projectItem);
 
        return CreateCodeDocumentCore(projectItem);
    }
 
    internal RazorCodeDocument CreateCodeDocument(
        RazorSourceDocument source,
        RazorFileKind fileKind,
        ImmutableArray<RazorSourceDocument> importSources,
        TagHelperCollection? tagHelpers,
        string? cssScope)
    {
        ArgHelper.ThrowIfNull(source);
 
        return CreateCodeDocumentCore(source, fileKind, importSources, tagHelpers, cssScope, configureParser: null, configureCodeGeneration: null);
    }
 
    internal RazorCodeDocument CreateDesignTimeCodeDocument(
        RazorSourceDocument source,
        RazorFileKind fileKind,
        ImmutableArray<RazorSourceDocument> importSources,
        TagHelperCollection? tagHelpers)
    {
        ArgHelper.ThrowIfNull(source);
 
        return CreateCodeDocumentDesignTimeCore(source, fileKind, importSources, tagHelpers, configureParser: null, configureCodeGeneration: null);
    }
 
    private RazorCodeDocument CreateCodeDocumentCore(
        RazorProjectItem projectItem,
        Action<RazorParserOptions.Builder>? configureParser = null,
        Action<RazorCodeGenerationOptions.Builder>? configureCodeGeneration = null)
    {
        var source = projectItem.GetSource();
        var importSources = GetImportSources(projectItem, designTime: false);
 
        return CreateCodeDocumentCore(
            source, projectItem.FileKind, importSources, tagHelpers: null, cssScope: projectItem.CssScope, configureParser, configureCodeGeneration);
    }
 
    private RazorCodeDocument CreateCodeDocumentCore(
        RazorSourceDocument source,
        RazorFileKind fileKind,
        ImmutableArray<RazorSourceDocument> importSources,
        TagHelperCollection? tagHelpers,
        string? cssScope,
        Action<RazorParserOptions.Builder>? configureParser,
        Action<RazorCodeGenerationOptions.Builder>? configureCodeGeneration)
    {
        var parserOptions = ComputeParserOptions(fileKind, configureParser);
        var codeGenerationOptions = ComputeCodeGenerationOptions(cssScope, configureCodeGeneration);
 
        var codeDocument = RazorCodeDocument.Create(source, importSources, parserOptions, codeGenerationOptions);
 
        return tagHelpers != null ? codeDocument.WithTagHelpers(tagHelpers) : codeDocument;
    }
 
    private RazorCodeDocument CreateCodeDocumentDesignTimeCore(
        RazorProjectItem projectItem,
        Action<RazorParserOptions.Builder>? configureParser = null,
        Action<RazorCodeGenerationOptions.Builder>? configureCodeGeneration = null)
    {
        var source = projectItem.GetSource();
        var importSources = GetImportSources(projectItem, designTime: true);
 
        return CreateCodeDocumentDesignTimeCore(source, projectItem.FileKind, importSources, tagHelpers: null, configureParser, configureCodeGeneration);
    }
 
    private RazorCodeDocument CreateCodeDocumentDesignTimeCore(
        RazorSourceDocument sourceDocument,
        RazorFileKind fileKind,
        ImmutableArray<RazorSourceDocument> importSources,
        TagHelperCollection? tagHelpers,
        Action<RazorParserOptions.Builder>? configureParser,
        Action<RazorCodeGenerationOptions.Builder>? configureCodeGeneration)
    {
        ArgHelper.ThrowIfNull(sourceDocument);
 
        var parserOptions = ComputeParserOptions(fileKind, builder =>
        {
            builder.DesignTime = true;
 
            configureParser?.Invoke(builder);
        });
 
        var codeGenerationOptions = ComputeCodeGenerationOptions(cssScope: null, builder =>
        {
            builder.DesignTime = true;
            builder.SuppressChecksum = true;
            builder.SuppressMetadataAttributes = true;
 
            configureCodeGeneration?.Invoke(builder);
        });
 
        var codeDocument = RazorCodeDocument.Create(sourceDocument, importSources, parserOptions, codeGenerationOptions);
 
        return tagHelpers != null ? codeDocument.WithTagHelpers(tagHelpers) : codeDocument;
    }
 
    private RazorParserOptions ComputeParserOptions(RazorFileKind fileKind, Action<RazorParserOptions.Builder>? configure)
    {
        var builder = new RazorParserOptions.Builder(Configuration.LanguageVersion, fileKind);
 
        configure?.Invoke(builder);
 
        foreach (var feature in _configureParserOptionsFeatures)
        {
            feature.Configure(builder);
        }
 
        return builder.ToOptions();
    }
 
    private RazorCodeGenerationOptions ComputeCodeGenerationOptions(string? cssScope, Action<RazorCodeGenerationOptions.Builder>? configure)
    {
        var configuration = Configuration;
        var builder = new RazorCodeGenerationOptions.Builder()
        {
            CssScope = cssScope,
            SuppressAddComponentParameter = configuration.SuppressAddComponentParameter,
            RazorWarningLevel = configuration.RazorWarningLevel
        };
 
        configure?.Invoke(builder);
 
        foreach (var feature in _configureCodeGenerationOptionsFeatures)
        {
            feature.Configure(builder);
        }
 
        return builder.ToOptions();
    }
 
    private RazorCodeDocument ProcessCore(RazorCodeDocument codeDocument, CancellationToken cancellationToken)
    {
        ArgHelper.ThrowIfNull(codeDocument);
 
        return Engine.Process(codeDocument, cancellationToken);
    }
 
    internal static RazorProjectEngine CreateEmpty(Action<RazorProjectEngineBuilder>? configure = null)
    {
        var builder = new RazorProjectEngineBuilder(RazorConfiguration.Default, RazorProjectFileSystem.Empty);
 
        configure?.Invoke(builder);
 
        return builder.Build();
    }
 
    internal static RazorProjectEngine Create(Action<RazorProjectEngineBuilder> configure)
        => Create(RazorConfiguration.Default, RazorProjectFileSystem.Empty, configure);
 
    public static RazorProjectEngine Create(RazorConfiguration configuration, RazorProjectFileSystem fileSystem)
        => Create(configuration, fileSystem, configure: null);
 
    public static RazorProjectEngine Create(
        RazorConfiguration configuration,
        RazorProjectFileSystem fileSystem,
        Action<RazorProjectEngineBuilder>? configure)
    {
        ArgHelper.ThrowIfNull(configuration);
        ArgHelper.ThrowIfNull(fileSystem);
 
        var builder = new RazorProjectEngineBuilder(configuration, fileSystem);
 
        // The initialization order is somewhat important.
        //
        // Defaults -> Extensions -> Additional customization
        //
        // This allows extensions to rely on default features, and customizations to override choices made by
        // extensions.
        AddDefaultPhases(builder.Phases);
        AddDefaultFeatures(builder.Features);
 
        if (configuration.LanguageVersion >= RazorLanguageVersion.Version_5_0)
        {
            builder.Features.Add(new ViewCssScopePass());
        }
 
        if (configuration.LanguageVersion >= RazorLanguageVersion.Version_3_0)
        {
            FunctionsDirective.Register(builder);
            ImplementsDirective.Register(builder);
            InheritsDirective.Register(builder);
            NamespaceDirective.Register(builder);
            AttributeDirective.Register(builder);
 
            AddComponentFeatures(builder, configuration.LanguageVersion);
        }
 
        configure?.Invoke(builder);
 
        return builder.Build();
    }
 
    private static void AddDefaultPhases(ImmutableArray<IRazorEnginePhase>.Builder phases)
    {
        phases.Add(new DefaultRazorParsingPhase());
        phases.Add(new DefaultRazorSyntaxTreePhase());
        phases.Add(new DefaultRazorTagHelperContextDiscoveryPhase());
        phases.Add(new DefaultRazorIntermediateNodeLoweringPhase());
        phases.Add(new DefaultTagHelperResolutionPhase());
        phases.Add(new DefaultRazorTagHelperRewritePhase());
        phases.Add(new DefaultRazorDocumentClassifierPhase());
        phases.Add(new DefaultRazorDirectiveClassifierPhase());
        phases.Add(new DefaultRazorOptimizationPhase());
        phases.Add(new DefaultRazorCSharpLoweringPhase());
    }
 
    private static void AddDefaultFeatures(ImmutableArray<IRazorFeature>.Builder features)
    {
        features.Add(new DefaultImportProjectFeature());
        features.Add(new TagHelperDiscoveryService());
 
        // General extensibility
        features.Add(new ConfigureDirectivesFeature());
        features.Add(new DefaultMetadataIdentifierFeature());
 
        // Syntax Tree passes
        features.Add(new DefaultDirectiveSyntaxTreePass());
        features.Add(new HtmlNodeOptimizationPass());
 
        // Intermediate Node Passes
        features.Add(new DefaultDocumentClassifierPass());
        features.Add(new MetadataAttributePass());
        features.Add(new DesignTimeDirectivePass());
        features.Add(new DirectiveRemovalOptimizationPass());
        features.Add(new DefaultTagHelperOptimizationPass());
        features.Add(new PreallocatedTagHelperAttributeOptimizationPass());
        features.Add(new EliminateMethodBodyPass());
 
        // Default Code Target Extensions
        var targetExtensionFeature = new DefaultRazorTargetExtensionFeature();
        features.Add(targetExtensionFeature);
        targetExtensionFeature.TargetExtensions.Add(new MetadataAttributeTargetExtension());
        targetExtensionFeature.TargetExtensions.Add(new DefaultTagHelperTargetExtension());
        targetExtensionFeature.TargetExtensions.Add(new PreallocatedAttributeTargetExtension());
        targetExtensionFeature.TargetExtensions.Add(new DesignTimeDirectiveTargetExtension());
 
        // Default configuration
        var configurationFeature = new DefaultDocumentClassifierPassFeature();
        features.Add(configurationFeature);
        configurationFeature.ConfigureClass.Add((document, @class) =>
        {
            @class.Name = "Template";
            @class.Modifiers = CommonModifiers.Public;
        });
 
        configurationFeature.ConfigureNamespace.Add((document, @namespace) =>
        {
            @namespace.Name = "Razor";
        });
 
        configurationFeature.ConfigureMethod.Add((document, method) =>
        {
            method.Name = "ExecuteAsync";
            method.ReturnType = $"global::{typeof(Task).FullName}";
            method.Modifiers = CommonModifiers.PublicAsyncOverride;
        });
    }
 
    private static void AddComponentFeatures(RazorProjectEngineBuilder builder, RazorLanguageVersion razorLanguageVersion)
    {
        // Project Engine Features
        builder.Features.Add(new ComponentImportProjectFeature());
 
        // Directives (conditional on file kind)
        ComponentCodeDirective.Register(builder);
        ComponentInjectDirective.Register(builder);
        ComponentLayoutDirective.Register(builder);
        ComponentPageDirective.Register(builder);
 
        if (razorLanguageVersion >= RazorLanguageVersion.Version_6_0)
        {
            ComponentConstrainedTypeParamDirective.Register(builder);
        }
        else
        {
            ComponentTypeParamDirective.Register(builder);
        }
 
        if (razorLanguageVersion >= RazorLanguageVersion.Version_5_0)
        {
            ComponentPreserveWhitespaceDirective.Register(builder);
        }
 
        if (razorLanguageVersion >= RazorLanguageVersion.Version_8_0)
        {
            ComponentRenderModeDirective.Register(builder);
        }
 
        // Document Classifier
        builder.Features.Add(new ComponentDocumentClassifierPass());
 
        // Directive Classifier
        builder.Features.Add(new ComponentWhitespacePass());
 
        // Optimization
        builder.Features.Add(new ComponentComplexAttributeContentPass());
        builder.Features.Add(new ComponentLoweringPass());
        builder.Features.Add(new ComponentEventHandlerLoweringPass());
        builder.Features.Add(new ComponentKeyLoweringPass());
        builder.Features.Add(new ComponentReferenceCaptureLoweringPass());
        builder.Features.Add(new ComponentSplatLoweringPass());
        builder.Features.Add(new ComponentFormNameLoweringPass());
        builder.Features.Add(new ComponentBindLoweringPass());
        builder.Features.Add(new ComponentRenderModeLoweringPass());
        builder.Features.Add(new ComponentCssScopePass());
        builder.Features.Add(new ComponentTemplateDiagnosticPass());
        builder.Features.Add(new ComponentGenericTypePass());
        builder.Features.Add(new ComponentChildContentDiagnosticPass());
        builder.Features.Add(new ComponentMarkupDiagnosticPass());
        builder.Features.Add(new ComponentMarkupBlockPass(razorLanguageVersion));
        builder.Features.Add(new ComponentMarkupEncodingPass(razorLanguageVersion));
    }
 
    internal void CollectImports(RazorProjectItem projectItem, ref PooledArrayBuilder<RazorProjectItem> importItems)
    {
        foreach (var importProjectFeature in GetFeatures<IImportProjectFeature>())
        {
            importProjectFeature.CollectImports(projectItem, ref importItems);
        }
    }
 
    internal ImmutableArray<RazorProjectItem> GetImports(RazorProjectItem projectItem)
    {
        using var imports = new PooledArrayBuilder<RazorProjectItem>();
        CollectImports(projectItem, ref imports.AsRef());
 
        return imports.ToImmutable();
    }
 
    internal ImmutableArray<RazorProjectItem> GetImports(RazorProjectItem projectItem, Func<RazorProjectItem, bool> predicate)
    {
        using var imports = new PooledArrayBuilder<RazorProjectItem>();
        CollectImports(projectItem, ref imports.AsRef());
 
        if (imports.Count == 0)
        {
            return [];
        }
 
        using var result = new PooledArrayBuilder<RazorProjectItem>(capacity: imports.Count);
 
        foreach (var import in imports)
        {
            if (predicate(import))
            {
                result.Add(import);
            }
        }
 
        return result.ToImmutableAndClear();
    }
 
    private ImmutableArray<RazorSourceDocument> GetImportSources(RazorProjectItem projectItem, bool designTime)
    {
        using var importItems = new PooledArrayBuilder<RazorProjectItem>();
        CollectImports(projectItem, ref importItems.AsRef());
 
        if (importItems.Count == 0)
        {
            return [];
        }
 
        // Suppress exceptions for design-time requests.
        return GetImportSourceDocuments(in importItems, suppressExceptions: designTime);
    }
 
    // Internal for testing
    internal static ImmutableArray<RazorSourceDocument> GetImportSourceDocuments(
        ref readonly PooledArrayBuilder<RazorProjectItem> importItems,
        bool suppressExceptions = false)
    {
        using var imports = new PooledArrayBuilder<RazorSourceDocument>(importItems.Count);
 
        foreach (var importItem in importItems)
        {
            if (importItem.Exists)
            {
                try
                {
                    // Normal import, has file paths, content etc.
                    var sourceDocument = importItem.GetSource();
                    imports.Add(sourceDocument);
                }
                catch (IOException) when (suppressExceptions)
                {
                    // Something happened when trying to read the item from disk.
                    // Catch the exception so we don't crash the editor.
                }
            }
        }
 
        return imports.ToImmutableAndClear();
    }
}