Skip to content
This repository has been archived by the owner on Dec 19, 2018. It is now read-only.

Commit

Permalink
Implement @namespace
Browse files Browse the repository at this point in the history
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
rynowak committed Apr 11, 2017
1 parent b4b4a19 commit e5cac9f
Show file tree
Hide file tree
Showing 17 changed files with 898 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions
{
public class MvcViewDocumentClassifierPass : DocumentClassifierPassBase
{
public readonly string MvcViewDocumentKind = "mvc.1.0.view";
public static readonly string MvcViewDocumentKind = "mvc.1.0.view";

protected override string DocumentKind => MvcViewDocumentKind;

Expand Down
191 changes: 191 additions & 0 deletions src/Microsoft.AspNetCore.Mvc.Razor.Extensions/NamespaceDirective.cs
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);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public static void Register(IRazorEngineBuilder builder)
{
InjectDirective.Register(builder);
ModelDirective.Register(builder);
NamespaceDirective.Register(builder);
PageDirective.Register(builder);

builder.AddTargetExtension(new InjectDirectiveTargetExtension());
Expand Down
31 changes: 25 additions & 6 deletions src/Microsoft.AspNetCore.Razor.Language/Legacy/CSharpCodeParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,17 @@ protected void MapDirectives(Action handler, params string[] directives)
{
_directiveParsers.Add(directive, handler);
Keywords.Add(directive);

// These C# keywords are reserved for use in directives. It's an error to use them outside of
// a directive. This code removes the error generation if the directive *is* registered.
if (string.Equals(directive, "class", StringComparison.OrdinalIgnoreCase))
{
_keywordParsers.Remove(CSharpKeyword.Class);
}
else if (string.Equals(directive, "namespace", StringComparison.OrdinalIgnoreCase))
{
_keywordParsers.Remove(CSharpKeyword.Namespace);
}
}
}

Expand Down Expand Up @@ -266,8 +277,7 @@ private void AfterTransition()
}
else if (CurrentSymbol.Type == CSharpSymbolType.Identifier)
{
Action handler;
if (TryGetDirectiveHandler(CurrentSymbol.Content, out handler))
if (TryGetDirectiveHandler(CurrentSymbol.Content, out var handler))
{
Span.ChunkGenerator = SpanChunkGenerator.Null;
handler();
Expand Down Expand Up @@ -295,8 +305,17 @@ private void AfterTransition()
}
else if (CurrentSymbol.Type == CSharpSymbolType.Keyword)
{
KeywordBlock(topLevel: true);
return;
if (TryGetDirectiveHandler(CurrentSymbol.Content, out var handler))
{
Span.ChunkGenerator = SpanChunkGenerator.Null;
handler();
return;
}
else
{
KeywordBlock(topLevel: true);
return;
}
}
else if (CurrentSymbol.Type == CSharpSymbolType.LeftBrace)
{
Expand Down Expand Up @@ -737,7 +756,7 @@ private void SetUpKeywords()
MapKeywords(TryStatement, CSharpKeyword.Try);
MapKeywords(UsingKeyword, CSharpKeyword.Using);
MapKeywords(DoStatement, CSharpKeyword.Do);
MapKeywords(ReservedDirective, CSharpKeyword.Namespace, CSharpKeyword.Class);
MapKeywords(ReservedDirective, CSharpKeyword.Class, CSharpKeyword.Namespace);
}

protected virtual void ReservedDirective(bool topLevel)
Expand Down Expand Up @@ -1748,7 +1767,7 @@ protected virtual void RemoveTagHelperDirective()
[Conditional("DEBUG")]
protected void AssertDirective(string directive)
{
Assert(CSharpSymbolType.Identifier);
Debug.Assert(CurrentSymbol.Type == CSharpSymbolType.Identifier || CurrentSymbol.Type == CSharpSymbolType.Keyword);
Debug.Assert(string.Equals(CurrentSymbol.Content, directive, StringComparison.Ordinal));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,36 @@ public void RazorPagesWithoutModel_Runtime()
AssertIRMatchesBaseline(document.GetIRDocument());
AssertCSharpDocumentMatchesBaseline(document.GetCSharpDocument());
}

[Fact]
public void PageWithNamespace_Runtime()
{
// Arrange
var engine = CreateRuntimeEngine();
var document = CreateCodeDocument();

// Act
engine.Process(document);

// Assert
AssertIRMatchesBaseline(document.GetIRDocument());
AssertCSharpDocumentMatchesBaseline(document.GetCSharpDocument());
}

[Fact]
public void ViewWithNamespace_Runtime()
{
// Arrange
var engine = CreateRuntimeEngine();
var document = CreateCodeDocument();

// Act
engine.Process(document);

// Assert
AssertIRMatchesBaseline(document.GetIRDocument());
AssertCSharpDocumentMatchesBaseline(document.GetCSharpDocument());
}
#endregion

#region DesignTime
Expand Down Expand Up @@ -311,6 +341,36 @@ public void RazorPagesWithoutModel_DesignTime()
AssertIRMatchesBaseline(document.GetIRDocument());
AssertCSharpDocumentMatchesBaseline(document.GetCSharpDocument());
}

[Fact]
public void PageWithNamespace_DesignTime()
{
// Arrange
var engine = CreateDesignTimeEngine();
var document = CreateCodeDocument();

// Act
engine.Process(document);

// Assert
AssertIRMatchesBaseline(document.GetIRDocument());
AssertCSharpDocumentMatchesBaseline(document.GetCSharpDocument());
}

[Fact]
public void ViewWithNamespace_DesignTime()
{
// Arrange
var engine = CreateDesignTimeEngine();
var document = CreateCodeDocument();

// Act
engine.Process(document);

// Assert
AssertIRMatchesBaseline(document.GetIRDocument());
AssertCSharpDocumentMatchesBaseline(document.GetCSharpDocument());
}
#endregion

protected RazorEngine CreateDesignTimeEngine(IEnumerable<TagHelperDescriptor> descriptors = null)
Expand Down

0 comments on commit e5cac9f

Please sign in to comment.