| File: Language\RazorCodeDocumentExtensions.cs | Web Access |
| Project: src\roslyn\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.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using Microsoft.AspNetCore.Mvc.Razor.Extensions; using Microsoft.AspNetCore.Razor.Language.Extensions; using Microsoft.AspNetCore.Razor.Language.Legacy; using Microsoft.AspNetCore.Razor.Language.Syntax; namespace Microsoft.AspNetCore.Razor.Language; public static class RazorCodeDocumentExtensions { public static bool TryComputeClassName(this RazorCodeDocument codeDocument, [NotNullWhen(true)] out string? className) { var filePath = codeDocument.Source.RelativePath ?? codeDocument.Source.FilePath; if (filePath.IsNullOrEmpty()) { className = null; return false; } className = CSharpIdentifier.GetClassNameFromPath(filePath); return className is not null; } public static bool TryGetNamespace( this RazorCodeDocument codeDocument, bool fallbackToRootNamespace, [NotNullWhen(true)] out string? @namespace) => codeDocument.TryGetNamespace(fallbackToRootNamespace, out @namespace, out _); public static bool TryGetNamespace( this RazorCodeDocument codeDocument, bool fallbackToRootNamespace, [NotNullWhen(true)] out string? @namespace, out SourceSpan? namespaceSpan) => codeDocument.TryGetNamespace(fallbackToRootNamespace, considerImports: true, out @namespace, out namespaceSpan); internal static bool IsImportsFile(this RazorCodeDocument codeDocument) => codeDocument.FileKind.IsComponentImport() || (codeDocument.FileKind.IsLegacy() && string.Equals(Path.GetFileName(codeDocument.Source.FilePath), MvcImportProjectFeature.ImportsFileName, StringComparison.OrdinalIgnoreCase)); /// <summary> /// Returns the content of the <c>@inherits</c> directive if present in a legacy <c>.cshtml</c> /// document's syntax tree, or <see langword="null"/> for non-legacy files or when absent. /// </summary> internal static string? GetInheritsDirectiveValue(this RazorCodeDocument codeDocument) { if (!codeDocument.FileKind.IsLegacy()) { return null; } var syntaxTree = codeDocument.GetSyntaxTree(); if (syntaxTree is null) { return null; } // Check the main document first -- its @inherits overrides any from imports. var inheritsValue = FindInheritsDirective(syntaxTree); if (inheritsValue is not null) { return inheritsValue; } // Check import syntax trees. The last import's @inherits wins (most specific scope). if (codeDocument.TryGetImportSyntaxTrees(out var importSyntaxTrees)) { for (var i = importSyntaxTrees.Length - 1; i >= 0; i--) { inheritsValue = FindInheritsDirective(importSyntaxTrees[i]); if (inheritsValue is not null) { return inheritsValue; } } } return null; static string? FindInheritsDirective(RazorSyntaxTree tree) { foreach (var node in tree.Root.DescendantNodes()) { if (node is RazorDirectiveSyntax { DirectiveDescriptor: var descriptor, Body: RazorDirectiveBodySyntax { CSharpCode: { } csharpCode } } && descriptor == InheritsDirective.Directive) { return csharpCode.GetContent()?.Trim(); } } return null; } } /// <summary> /// Returns all <c>@using</c> directives from the document and its import files, in the /// same order as <see cref="DefaultRazorIntermediateNodeLoweringPhase"/> emits them /// during code generation: imports first (outer to inner), then the page's own usings. /// </summary> internal static ImmutableArray<string> GetUsingDirectives(this RazorCodeDocument codeDocument) { var syntaxTree = codeDocument.GetSyntaxTree(); if (syntaxTree is null) { return []; } var usings = new List<string>(); if (codeDocument.TryGetImportSyntaxTrees(out var importSyntaxTrees)) { foreach (var importTree in importSyntaxTrees) { CollectUsings(importTree, usings); } } CollectUsings(syntaxTree, usings); return [.. usings]; static void CollectUsings(RazorSyntaxTree tree, List<string> usings) { foreach (var node in tree.Root.DescendantNodes()) { if (node is RazorUsingDirectiveSyntax usingDirective) { var content = usingDirective.Body?.GetContent()?.Trim(); if (content is not null && content.StartsWith("using ", StringComparison.Ordinal)) { var ns = content.Substring("using ".Length).TrimEnd(';').Trim(); if (ns.Length > 0) { usings.Add(ns); } } } } } } /// <summary> /// Returns whether the directive specified was involved in tag helper binding /// </summary> /// <remarks> /// If passed a directive that has no effect on tag helper binding at all, like `@if` or `@code`, /// this method will return false, correctly identifying that the tag helper didn't contribute. /// </remarks> internal static bool IsDirectiveUsed(this RazorCodeDocument codeDocument, BaseRazorDirectiveSyntax directive) { Debug.Assert(directive is RazorUsingDirectiveSyntax || directive.DirectiveBody.Keyword.GetContent() == SyntaxConstants.CSharp.AddTagHelperKeyword); // In imports files, all directives are considered used as usage tracking is only for source documents. if (codeDocument.IsImportsFile()) { return true; } var contributions = codeDocument.GetDirectiveTagHelperContributions(); // No contributions, means no directives contributed, so no directives are used if (contributions.IsDefaultOrEmpty) { return false; } // No tag helpers referenced, so no directives are used if (!codeDocument.TryGetReferencedTagHelpers(out var referencedTagHelpers)) { return false; } foreach (var contribution in contributions) { if (contribution.DirectiveSpanStart == directive.SpanStart) { return AnyContributedTagHelperIsReferenced(contribution.ContributedTagHelpers, referencedTagHelpers); } } return false; static bool AnyContributedTagHelperIsReferenced( TagHelperCollection contributedTagHelpers, TagHelperCollection referencedTagHelpers) { foreach (var contributed in contributedTagHelpers) { if (referencedTagHelpers.Contains(contributed)) { return true; } } return false; } } }