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

Add file scoped extensible directives. #1457

Merged
merged 2 commits into from
Jun 22, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ public static class ModelDirective
public static readonly DirectiveDescriptor Directive = DirectiveDescriptor.CreateDirective(
"model",
DirectiveKind.SingleLine,
builder => builder.AddTypeToken());
builder =>
{
builder.AddTypeToken();
builder.Usage = DirectiveUsage.FileScopedSinglyOccurring;
});

public static IRazorEngineBuilder Register(IRazorEngineBuilder builder)
{
Expand Down Expand Up @@ -55,7 +59,7 @@ private static string GetModelType(DocumentIntermediateNode document, Visitor vi
}
else
{
return "dynamic";
return "dynamic";
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ public static class NamespaceDirective
public static readonly DirectiveDescriptor Directive = DirectiveDescriptor.CreateDirective(
"namespace",
DirectiveKind.SingleLine,
builder => builder.AddNamespaceToken());
builder =>
{
builder.AddNamespaceToken();
builder.Usage = DirectiveUsage.FileScopedSinglyOccurring;
});

public static void Register(IRazorEngineBuilder builder)
{
Expand Down Expand Up @@ -58,7 +62,7 @@ protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentInte
// No namespace node. Skip.
return;
}

if (TryComputeNamespace(codeDocument.Source.FilePath, directive, out var computedNamespace))
{
// Beautify the class name since we're using a hierarchy for namespaces.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ public class PageDirective
public static readonly DirectiveDescriptor Directive = DirectiveDescriptor.CreateDirective(
"page",
DirectiveKind.SingleLine,
builder => builder.AddOptionalStringToken());
builder =>
{
builder.AddOptionalStringToken();
builder.Usage = DirectiveUsage.FileScopedSinglyOccurring;
});

private PageDirective(string routeTemplate)
{
Expand Down
13 changes: 12 additions & 1 deletion src/Microsoft.AspNetCore.Razor.Language/DirectiveDescriptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ public abstract class DirectiveDescriptor
/// </summary>
public abstract DirectiveKind Kind { get; }

/// <summary>
/// Gets the way a directive can be used. The usage determines how many, and where directives can exist per document.
/// </summary>
public abstract DirectiveUsage Usage { get; }

/// <summary>
/// Gets the list of directive tokens that can follow the directive keyword.
/// </summary>
Expand Down Expand Up @@ -189,6 +194,8 @@ public DefaultDirectiveDescriptorBuilder(string name, DirectiveKind kind)

public DirectiveKind Kind { get; }

public DirectiveUsage Usage { get; set; }

public IList<DirectiveTokenDescriptor> Tokens { get; }

public DirectiveDescriptor Build()
Expand Down Expand Up @@ -218,7 +225,7 @@ public DirectiveDescriptor Build()
}
}

return new DefaultDirectiveDescriptor(Directive, Kind, Tokens.ToArray(), DisplayName, Description);
return new DefaultDirectiveDescriptor(Directive, Kind, Usage, Tokens.ToArray(), DisplayName, Description);
}
}

Expand All @@ -227,12 +234,14 @@ private class DefaultDirectiveDescriptor : DirectiveDescriptor
public DefaultDirectiveDescriptor(
string directive,
DirectiveKind kind,
DirectiveUsage usage,
DirectiveTokenDescriptor[] tokens,
string displayName,
string description)
{
Directive = directive;
Kind = kind;
Usage = usage;
Tokens = tokens;
DisplayName = displayName;
Description = description;
Expand All @@ -246,6 +255,8 @@ public DefaultDirectiveDescriptor(

public override DirectiveKind Kind { get; }

public override DirectiveUsage Usage { get; }

public override IReadOnlyList<DirectiveTokenDescriptor> Tokens { get; }
}
}
Expand Down
11 changes: 11 additions & 0 deletions src/Microsoft.AspNetCore.Razor.Language/DirectiveUsage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// 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.

namespace Microsoft.AspNetCore.Razor.Language
{
public enum DirectiveUsage
{
Unrestricted,
FileScopedSinglyOccurring,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ public static class InheritsDirective
public static readonly DirectiveDescriptor Directive = DirectiveDescriptor.CreateDirective(
SyntaxConstants.CSharp.InheritsKeyword,
DirectiveKind.SingleLine,
builder => builder.AddTypeToken());
builder =>
{
builder.AddTypeToken();
builder.Usage = DirectiveUsage.FileScopedSinglyOccurring;
});

public static void Register(IRazorEngineBuilder builder)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ public interface IDirectiveDescriptorBuilder
/// </summary>
DirectiveKind Kind { get; }

/// <summary>
/// Gets or sets the directive usage. The usage determines how many, and where directives can exist per document.
/// </summary>
DirectiveUsage Usage { get; set; }

/// <summary>
/// Gets a list of the directive tokens.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,10 @@ internal enum BlockKindInternal
// Code
Statement,
Directive,
Functions,
Expression,
Helper,

// Markup
Markup,
Section,
Template,

// Special
Expand Down
63 changes: 61 additions & 2 deletions src/Microsoft.AspNetCore.Razor.Language/Legacy/CSharpCodeParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ internal class CSharpCodeParser : TokenizerBackedParser<CSharpTokenizer, CSharpS

private Dictionary<string, Action> _directiveParsers = new Dictionary<string, Action>(StringComparer.Ordinal);
private Dictionary<CSharpKeyword, Action<bool>> _keywordParsers = new Dictionary<CSharpKeyword, Action<bool>>();
private HashSet<string> _seenDirectives = new HashSet<string>(StringComparer.Ordinal);

public CSharpCodeParser(ParserContext context)
: this(directives: Enumerable.Empty<DirectiveDescriptor>(), context: context)
Expand Down Expand Up @@ -91,7 +92,12 @@ protected void MapDirectives(Action handler, params string[] directives)
{
foreach (var directive in directives)
{
_directiveParsers.Add(directive, handler);
_directiveParsers.Add(directive, () =>
{
handler();
_seenDirectives.Add(directive);
});

Keywords.Add(directive);

// These C# keywords are reserved for use in directives. It's an error to use them outside of
Expand Down Expand Up @@ -1589,10 +1595,13 @@ private void HandleDirective(DirectiveDescriptor descriptor)
AcceptAndMoveNext();
Output(SpanKindInternal.MetaCode, AcceptedCharactersInternal.None);

// Even if an error was logged do not bail out early. If a directive was used incorrectly it doesn't mean it can't be parsed.
ValidateDirectiveUsage(descriptor);

for (var i = 0; i < descriptor.Tokens.Count; i++)
{
if (!At(CSharpSymbolType.WhiteSpace) &&
!At(CSharpSymbolType.NewLine) &&
!At(CSharpSymbolType.NewLine) &&
!EndOfFile)
{
Context.ErrorSink.OnError(
Expand Down Expand Up @@ -1767,6 +1776,56 @@ private void HandleDirective(DirectiveDescriptor descriptor)
}
}


private void ValidateDirectiveUsage(DirectiveDescriptor descriptor)
{
if (descriptor.Usage == DirectiveUsage.FileScopedSinglyOccurring)
{
if (_seenDirectives.Contains(descriptor.Directive))
{
UsageError(Resources.FormatDuplicateDirective(descriptor.Directive));
return;
}

var root = Context.Builder.ActiveBlocks.Last();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ajaybhargavb this is the magic that determines if a directive exists prior to code, html etc.

for (var i = 0; i < root.Children.Count; i++)
{
// Directives, comments and whitespace are valid prior to an unnested directive.

var child = root.Children[i];
if (child is Legacy.Block block)
{
if (block.Type == BlockKindInternal.Directive || block.Type == BlockKindInternal.Comment)
{
continue;
}
}
else if (child is Span span)
{
if (span.Length == 0 ||
span.Kind == SpanKindInternal.Comment ||
span.Symbols.All(symbol => string.IsNullOrWhiteSpace(symbol.Content)))
{
continue;
}
}

UsageError(Resources.FormatDirectiveMustExistBeforeMarkupOrCode(descriptor.Directive));
return;
}
}

return;

void UsageError(string message)
{
// There wil always be at least 1 child because of the `@` transition.
var directiveStart = Context.Builder.CurrentBlock.Children.First().Start;
var errorLength = /* @ */ 1 + descriptor.Directive.Length;
Context.ErrorSink.OnError(directiveStart, message, errorLength);
}
}

private void ParseDirectiveBlock(DirectiveDescriptor descriptor, Action<SourceLocation> parseChildren)
{
if (EndOfFile)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public SyntaxTreeBuilder()
_endBlock = EndBlock;
}

public IEnumerable<BlockBuilder> ActiveBlocks => _blockStack;
public IReadOnlyCollection<BlockBuilder> ActiveBlocks => _blockStack;

public BlockBuilder CurrentBlock => _blockStack.Peek();

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions src/Microsoft.AspNetCore.Razor.Language/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -225,4 +225,10 @@
<data name="IntermediateNodeReference_CollectionIsReadOnly" xml:space="preserve">
<value>The node '{0}' has a read-only child collection and cannot be modified.</value>
</data>
<data name="DuplicateDirective" xml:space="preserve">
<value>The '{0}' directive may only occur once per document.</value>
</data>
<data name="DirectiveMustExistBeforeMarkupOrCode" xml:space="preserve">
<value>The '{0}' directive must exist prior to markup or code.</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions
public class ModelDirectiveTest
{
[Fact]
public void ModelDirective_GetModelType_GetsTypeFromLastWellFormedDirective()
public void ModelDirective_GetModelType_GetsTypeFromFirstWellFormedDirective()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means that @model existing multiple times in a single document results in the first valid @model being used. If you have a valid model in an import file that carries over and properly overrides proper @model directives due to how our ModelDirective code passes over the IR.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what you're saying

{
// Arrange
var codeDocument = CreateDocument(@"
Expand All @@ -31,7 +31,7 @@ @model Type2
var result = ModelDirective.GetModelType(irDocument);

// Assert
Assert.Equal("Type2", result);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is fine.

Assert.Equal("Type1", result);
}

[Fact]
Expand Down Expand Up @@ -101,7 +101,7 @@ @model Type2
// Assert
var @class = FindClassNode(irDocument);
Assert.NotNull(@class);
Assert.Equal("BaseType<Type2>", @class.BaseType);
Assert.Equal("BaseType<Type1>", @class.BaseType);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml(4,1): Error RZ9999: The 'page' directive may only occur once per document.
TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml(5,1): Error RZ9999: The 'page' directive may only occur once per document.
TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml(5,7): Error RZ9999: The 'page' directive expects a string surrounded by double quotes.
TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml(7,1): Error RZ9999: The 'model' directive must exist prior to markup or code.
TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml(7,7): Error RZ9999: The 'model' directive expects a type name.
TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml(8,1): Error RZ9999: The 'model' directive may only occur once per document.
TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml(8,8): Error RZ9999: The 'model' directive expects a type name.
TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml(10,8): Error RZ9999: The 'inject' directive expects a type name.
TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml(11,9): Error RZ9999: The 'inject' directive expects a type name.
TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml(12,26): Error RZ9999: The 'inject' directive expects an identifier.
TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml(14,1): Error RZ9999: The 'namespace' directive must exist prior to markup or code.
TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml(14,11): Error RZ9999: The 'namespace' directive expects a namespace name.
TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml(15,1): Error RZ9999: The 'namespace' directive may only occur once per document.
TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml(15,12): Error RZ9999: The 'namespace' directive expects a namespace name.
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Document -
MethodDeclaration - - public async override - global::System.Threading.Tasks.Task - ExecuteAsync
HtmlContent - (83:0,83 [4] IncompleteDirectives.cshtml)
IntermediateToken - (83:0,83 [4] IncompleteDirectives.cshtml) - Html - \n\n
MalformedDirective - (94:3,0 [8] IncompleteDirectives.cshtml)
MalformedDirective - (102:4,0 [6] IncompleteDirectives.cshtml)
HtmlContent - (108:4,6 [5] IncompleteDirectives.cshtml)
IntermediateToken - (108:4,6 [5] IncompleteDirectives.cshtml) - Html - "\n\n
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml(4,1): Error RZ9999: The 'page' directive may only occur once per document.
TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml(5,1): Error RZ9999: The 'page' directive may only occur once per document.
TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml(5,7): Error RZ9999: The 'page' directive expects a string surrounded by double quotes.
TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml(7,1): Error RZ9999: The 'model' directive must exist prior to markup or code.
TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml(7,7): Error RZ9999: The 'model' directive expects a type name.
TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml(8,1): Error RZ9999: The 'model' directive may only occur once per document.
TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml(8,8): Error RZ9999: The 'model' directive expects a type name.
TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml(10,8): Error RZ9999: The 'inject' directive expects a type name.
TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml(11,9): Error RZ9999: The 'inject' directive expects a type name.
TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml(12,26): Error RZ9999: The 'inject' directive expects an identifier.
TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml(14,1): Error RZ9999: The 'namespace' directive must exist prior to markup or code.
TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml(14,11): Error RZ9999: The 'namespace' directive expects a namespace name.
TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml(15,1): Error RZ9999: The 'namespace' directive may only occur once per document.
TestFiles/IntegrationTests/CodeGenerationIntegrationTest/IncompleteDirectives.cshtml(15,12): Error RZ9999: The 'namespace' directive expects a namespace name.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Document -
IntermediateToken - (83:0,83 [4] IncompleteDirectives.cshtml) - Html - \n\n
CSharpCode -
IntermediateToken - - CSharp - EndContext();
MalformedDirective - (94:3,0 [8] IncompleteDirectives.cshtml)
MalformedDirective - (102:4,0 [6] IncompleteDirectives.cshtml)
CSharpCode -
IntermediateToken - - CSharp - BeginContext(108, 5, true);
Expand Down
Loading