diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/CSharpCodeParser.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/CSharpCodeParser.cs
index 621d7f666e..6d45b45404 100644
--- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/CSharpCodeParser.cs
+++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/CSharpCodeParser.cs
@@ -101,7 +101,11 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
{
foreach (var directive in directives)
{
- _directiveParsers.Add(directive, handler);
+ _directiveParsers.Add(directive, () =>
+ {
+ EnsureDirectiveIsAtStartOfLine();
+ handler();
+ });
Keywords.Add(directive);
// These C# keywords are reserved for use in directives. It's an error to use them outside of
@@ -1519,6 +1523,30 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
MapDirectives(RemoveTagHelperDirective, SyntaxConstants.CSharp.RemoveTagHelperKeyword);
}
+ private void EnsureDirectiveIsAtStartOfLine()
+ {
+ // 1 is the offset of the @ transition for the directive.
+ if (CurrentStart.CharacterIndex > 1)
+ {
+ var index = CurrentStart.AbsoluteIndex - 1;
+ var lineStart = CurrentStart.AbsoluteIndex - CurrentStart.CharacterIndex;
+ while (--index >= lineStart)
+ {
+ var @char = Context.SourceDocument[index];
+
+ if (!char.IsWhiteSpace(@char))
+ {
+ var currentDirective = CurrentSymbol.Content;
+ Context.ErrorSink.OnError(
+ CurrentStart,
+ Resources.FormatDirectiveMustAppearAtStartOfLine(currentDirective),
+ length: currentDirective.Length);
+ break;
+ }
+ }
+ }
+ }
+
private void HandleDirective(DirectiveDescriptor descriptor)
{
Context.Builder.CurrentBlock.Type = BlockKindInternal.Directive;
diff --git a/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs
index 1f41dd4ab7..89459d415c 100644
--- a/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs
+++ b/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs
@@ -430,6 +430,20 @@ namespace Microsoft.AspNetCore.Razor.Language
internal static string FormatDiagnostic_CodeTarget_UnsupportedExtension(object p0, object p1)
=> string.Format(CultureInfo.CurrentCulture, GetString("Diagnostic_CodeTarget_UnsupportedExtension"), p0, p1);
+ ///
+ /// The '{0}` directive must appear at the start of the line.
+ ///
+ internal static string DirectiveMustAppearAtStartOfLine
+ {
+ get => GetString("DirectiveMustAppearAtStartOfLine");
+ }
+
+ ///
+ /// The '{0}` directive must appear at the start of the line.
+ ///
+ internal static string FormatDirectiveMustAppearAtStartOfLine(object p0)
+ => string.Format(CultureInfo.CurrentCulture, GetString("DirectiveMustAppearAtStartOfLine"), p0);
+
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);
diff --git a/src/Microsoft.AspNetCore.Razor.Language/Resources.resx b/src/Microsoft.AspNetCore.Razor.Language/Resources.resx
index 85d8d679b3..b05f1016c4 100644
--- a/src/Microsoft.AspNetCore.Razor.Language/Resources.resx
+++ b/src/Microsoft.AspNetCore.Razor.Language/Resources.resx
@@ -207,4 +207,7 @@
The document type '{0}' does not support the extension '{1}'.
+
+ The '{0}` directive must appear at the start of the line.
+
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/CSharpDirectivesTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/CSharpDirectivesTest.cs
index d9897cfba5..f51d8e33b1 100644
--- a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/CSharpDirectivesTest.cs
+++ b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/CSharpDirectivesTest.cs
@@ -10,6 +10,104 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
{
public class CSharpDirectivesTest : CsHtmlCodeParserTestBase
{
+ [Fact]
+ public void ExtensibleDirectiveDoesNotErorrIfNotAtStartOfLineBecauseOfWhitespace()
+ {
+ // Arrange
+ var descriptor = DirectiveDescriptor.CreateDirective(
+ "custom",
+ DirectiveKind.SingleLine,
+ b => b.AddTypeToken());
+
+ // Act & Assert
+ ParseCodeBlockTest(Environment.NewLine + " @custom System.Text.Encoding.ASCIIEncoding",
+ new[] { descriptor },
+ new DirectiveBlock(
+ new DirectiveChunkGenerator(descriptor),
+ Factory.Code(Environment.NewLine + " ").AsStatement(),
+ Factory.CodeTransition(),
+ Factory.MetaCode("custom").Accepts(AcceptedCharactersInternal.None),
+ Factory.Span(SpanKindInternal.Code, " ", markup: false).Accepts(AcceptedCharactersInternal.WhiteSpace),
+ Factory.Span(SpanKindInternal.Code, "System.Text.Encoding.ASCIIEncoding", markup: false)
+ .With(new DirectiveTokenChunkGenerator(descriptor.Tokens[0]))
+ .Accepts(AcceptedCharactersInternal.NonWhiteSpace)));
+ }
+
+ [Fact]
+ public void BuiltInDirectiveDoesNotErorrIfNotAtStartOfLineBecauseOfWhitespace()
+ {
+ // Act & Assert
+ ParseCodeBlockTest(Environment.NewLine + " @addTagHelper \"*, Foo\"",
+ Enumerable.Empty(),
+ new DirectiveBlock(
+ Factory.Code(Environment.NewLine + " ").AsStatement(),
+ Factory.CodeTransition(),
+ Factory.MetaCode(SyntaxConstants.CSharp.AddTagHelperKeyword + " ")
+ .Accepts(AcceptedCharactersInternal.None),
+ Factory.Code("\"*, Foo\"")
+ .AsAddTagHelper("\"*, Foo\"")));
+ }
+
+ [Fact]
+ public void BuiltInDirectiveErorrsIfNotAtStartOfLine()
+ {
+ // Act & Assert
+ ParseCodeBlockTest("{ @addTagHelper \"*, Foo\"" + Environment.NewLine + "}",
+ Enumerable.Empty(),
+ new StatementBlock(
+ Factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None),
+ Factory.Code(" ")
+ .AsStatement()
+ .AutoCompleteWith(autoCompleteString: null, atEndOfSpan: false),
+ new DirectiveBlock(
+ Factory.CodeTransition(),
+ Factory.MetaCode(SyntaxConstants.CSharp.AddTagHelperKeyword + " ")
+ .Accepts(AcceptedCharactersInternal.None),
+ Factory.Code("\"*, Foo\"")
+ .AsAddTagHelper("\"*, Foo\"")),
+ Factory.Code(Environment.NewLine).AsStatement(),
+ Factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)),
+ new RazorError(
+ Resources.FormatDirectiveMustAppearAtStartOfLine("addTagHelper"),
+ new SourceLocation(4, 0, 4),
+ 12));
+ }
+
+ [Fact]
+ public void ExtensibleDirectiveErorrsIfNotAtStartOfLine()
+ {
+ // Arrange
+ var descriptor = DirectiveDescriptor.CreateDirective(
+ "custom",
+ DirectiveKind.SingleLine,
+ b => b.AddTypeToken());
+
+ // Act & Assert
+ ParseCodeBlockTest(
+ "{ @custom System.Text.Encoding.ASCIIEncoding" + Environment.NewLine + "}",
+ new[] { descriptor },
+ new StatementBlock(
+ Factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None),
+ Factory.Code(" ")
+ .AsStatement()
+ .AutoCompleteWith(autoCompleteString: null, atEndOfSpan: false),
+ new DirectiveBlock(
+ new DirectiveChunkGenerator(descriptor),
+ Factory.CodeTransition(),
+ Factory.MetaCode("custom").Accepts(AcceptedCharactersInternal.None),
+ Factory.Span(SpanKindInternal.Code, " ", markup: false).Accepts(AcceptedCharactersInternal.WhiteSpace),
+ Factory.Span(SpanKindInternal.Code, "System.Text.Encoding.ASCIIEncoding", markup: false)
+ .With(new DirectiveTokenChunkGenerator(descriptor.Tokens[0]))
+ .Accepts(AcceptedCharactersInternal.NonWhiteSpace),
+ Factory.Span(SpanKindInternal.Markup, Environment.NewLine, markup: false).Accepts(AcceptedCharactersInternal.WhiteSpace)),
+ Factory.EmptyCSharp().AsStatement(),
+ Factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)),
+ new RazorError(
+ Resources.FormatDirectiveMustAppearAtStartOfLine("custom"),
+ new SourceLocation(4, 0, 4),
+ 6));
+ }
+
[Fact]
public void DirectiveDescriptor_UnderstandsTypeTokens()
{
diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/CSharpSectionTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/CSharpSectionTest.cs
index a05f37dd26..3f4511525a 100644
--- a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/CSharpSectionTest.cs
+++ b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/CSharpSectionTest.cs
@@ -164,6 +164,10 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
Factory.Markup(" ")),
Factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)),
Factory.EmptyHtml()),
+ new RazorError(
+ Resources.FormatDirectiveMustAppearAtStartOfLine("section"),
+ new SourceLocation(16, 0, 16),
+ 7),
new RazorError(
LegacyResources.FormatParseError_Sections_Cannot_Be_Nested(LegacyResources.SectionExample_CS),
new SourceLocation(15, 0, 15),