This repository has been archived by the owner on Dec 19, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 228
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This change adds support for @namespace, and introduces a set of changes that are needed to support @namespace in the parser. @namespace and @Class have always been treated as reserved words by Razor, with the intent that someday they would be allowed as directives. This changes makes that possible. You will still get an error about @namespace being a reserved word if you don't have the directive.
- Loading branch information
Showing
17 changed files
with
898 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
191 changes: 191 additions & 0 deletions
191
src/Microsoft.AspNetCore.Mvc.Razor.Extensions/NamespaceDirective.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
// 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.IO; | ||
using System.Linq; | ||
using System.Text; | ||
using Microsoft.AspNetCore.Razor.Language; | ||
using Microsoft.AspNetCore.Razor.Language.Intermediate; | ||
|
||
namespace Microsoft.AspNetCore.Mvc.Razor.Extensions | ||
{ | ||
public static class NamespaceDirective | ||
{ | ||
private static readonly char[] Separators = new char[] { '\\', '/' }; | ||
|
||
public static readonly DirectiveDescriptor Directive = DirectiveDescriptorBuilder.Create("namespace").AddNamespace().Build(); | ||
|
||
public static void Register(IRazorEngineBuilder builder) | ||
{ | ||
if (builder == null) | ||
{ | ||
throw new ArgumentNullException(); | ||
} | ||
|
||
builder.AddDirective(Directive); | ||
builder.Features.Add(new Pass()); | ||
} | ||
|
||
// internal for testing | ||
internal class Pass : RazorIRPassBase, IRazorDirectiveClassifierPass | ||
{ | ||
public override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIRNode irDocument) | ||
{ | ||
if (irDocument.DocumentKind != RazorPageDocumentClassifierPass.RazorPageDocumentKind && | ||
irDocument.DocumentKind != MvcViewDocumentClassifierPass.MvcViewDocumentKind) | ||
{ | ||
// Not a page. Skip. | ||
return; | ||
} | ||
|
||
var visitor = new Visitor(); | ||
visitor.Visit(irDocument); | ||
|
||
var directive = visitor.LastNamespaceDirective; | ||
if (directive == null) | ||
{ | ||
// No namespace set. Skip. | ||
return; | ||
} | ||
|
||
var @namespace = visitor.FirstNamespace; | ||
if (@namespace == null) | ||
{ | ||
// No namespace node. Skip. | ||
return; | ||
} | ||
|
||
if (TryComputeNamespace(codeDocument.Source.FileName, directive, out var computedNamespace)) | ||
{ | ||
// Beautify the class name since we're using a hierarchy for namespaces. | ||
var @class = visitor.FirstClass; | ||
if (@class != null && irDocument.DocumentKind == RazorPageDocumentClassifierPass.RazorPageDocumentKind) | ||
{ | ||
@class.Name = Path.GetFileNameWithoutExtension(codeDocument.Source.FileName) + "_Page"; | ||
} | ||
else if (@class != null && irDocument.DocumentKind == MvcViewDocumentClassifierPass.MvcViewDocumentKind) | ||
{ | ||
@class.Name = Path.GetFileNameWithoutExtension(codeDocument.Source.FileName) + "_View"; | ||
} | ||
} | ||
|
||
@namespace.Content = computedNamespace; | ||
} | ||
} | ||
|
||
// internal for testing. | ||
// | ||
// This code does a best-effort attempt to compute a namespace 'suffix' - the path difference between | ||
// where the @namespace directive appears and where the current document is on disk. | ||
// | ||
// In the event that these two source either don't have filenames set or don't follow a coherent hierarchy, | ||
// we will just use the namespace verbatim. | ||
internal static bool TryComputeNamespace(string source, DirectiveIRNode directive, out string @namespace) | ||
{ | ||
var directiveSource = NormalizeDirectory(directive.Source?.FilePath); | ||
|
||
var baseNamespace = directive.Tokens.First().Content; | ||
if (string.IsNullOrEmpty(source) || directiveSource == null) | ||
{ | ||
// No sources, can't compute a suffix. | ||
@namespace = baseNamespace; | ||
return false; | ||
} | ||
|
||
// We're specifically using OrdinalIgnoreCase here because Razor treats all paths as case-insensitive. | ||
if (!source.StartsWith(directiveSource, StringComparison.OrdinalIgnoreCase) || | ||
source.Length <= directiveSource.Length) | ||
{ | ||
// The imports are not from the directory hierarchy, can't compute a suffix. | ||
@namespace = baseNamespace; | ||
return false; | ||
} | ||
|
||
// OK so that this point we know that the 'imports' file containing this directive is in the directory | ||
// hierarchy of this soure file. This is the case where we can append a suffix to the baseNamespace. | ||
// | ||
// Everything so far has just been defensiveness on our part. | ||
|
||
var builder = new StringBuilder(baseNamespace); | ||
|
||
var segments = source.Substring(directiveSource.Length).Split(Separators); | ||
|
||
// Skip the last segment because it's the filename. | ||
for (var i = 0; i < segments.Length - 1; i++) | ||
{ | ||
builder.Append('.'); | ||
builder.Append(segments[i]); | ||
} | ||
|
||
@namespace = builder.ToString(); | ||
return true; | ||
} | ||
|
||
// We want to normalize the path of the file containing the '@namespace' directive to just the containing | ||
// directory with a trailing separator. | ||
// | ||
// Not using Path.GetDirectoryName here because it doesn't meet these requirements, and we want to handle | ||
// both 'view engine' style paths and absolute paths. | ||
// | ||
// We also don't normalize the separators here. We expect that all documents are using a consistent style of path. | ||
// | ||
// If we can't normalize the path, we just return null so it will be ignored. | ||
private static string NormalizeDirectory(string path) | ||
{ | ||
if (string.IsNullOrEmpty(path)) | ||
{ | ||
return null; | ||
} | ||
|
||
var lastSeparator = path.LastIndexOfAny(Separators); | ||
if (lastSeparator == -1) | ||
{ | ||
return null; | ||
} | ||
|
||
// Includes the separator | ||
return path.Substring(0, lastSeparator + 1); | ||
} | ||
|
||
private class Visitor : RazorIRNodeWalker | ||
{ | ||
public ClassDeclarationIRNode FirstClass { get; private set; } | ||
|
||
public NamespaceDeclarationIRNode FirstNamespace { get; private set; } | ||
|
||
// We want the last one, so get them all and then . | ||
public DirectiveIRNode LastNamespaceDirective { get; private set; } | ||
|
||
public override void VisitNamespace(NamespaceDeclarationIRNode node) | ||
{ | ||
if (FirstNamespace == null) | ||
{ | ||
FirstNamespace = node; | ||
} | ||
|
||
base.VisitNamespace(node); | ||
} | ||
|
||
public override void VisitClass(ClassDeclarationIRNode node) | ||
{ | ||
if (FirstClass == null) | ||
{ | ||
FirstClass = node; | ||
} | ||
|
||
base.VisitClass(node); | ||
} | ||
|
||
public override void VisitDirective(DirectiveIRNode node) | ||
{ | ||
if (node.Descriptor == Directive) | ||
{ | ||
LastNamespaceDirective = node; | ||
} | ||
|
||
base.VisitDirective(node); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.