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

Add extensible directive abstractions #869

Merged
merged 1 commit into from
Nov 24, 2016
Merged

Conversation

NTaylorMullen
Copy link
Contributor

  • Based generic directive implementation off of descriptors.
  • Added parsing logic to consume descriptors and parse content that's expected.
  • Added parsing errors to automagically detect unexpected directive pieces.
  • Updated visitor implementations to understand the directive bits.
  • Added a builder abstraction to easily create descriptors. Had to maintain the ability to manually construct a descriptor to enable convenient serialization/deserialization.

#853

Will add tests once design looks good.

/cc @rynowak

var directiveNode = (DirectiveIRNode)Builder.Pop();

var tokens = directiveNode.Children.OfType<DirectiveTokenIRNode>().ToList();
var exceptTokens = directiveNode.Children.Except(tokens).ToList();
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 is pretty crappy but for the IR passes to come that will light up directives it'll be important to have easy access to the Tokens.

Copy link
Member

Choose a reason for hiding this comment

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

I get why you're doing this, but wouldn't it be better to just leave the Children as is and make Tokens do something like:

public IEnumerable<DirectiveTokenIRNode> Tokens => Children.OfType<DirectiveTokenIRNode>();

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, sure we can do that 😄

@@ -39,11 +39,16 @@ internal class CSharpCodeParser : TokenizerBackedParser<CSharpTokenizer, CSharpS
private Dictionary<CSharpKeyword, Action<bool>> _keywordParsers = new Dictionary<CSharpKeyword, Action<bool>>();

public CSharpCodeParser(ParserContext context)
: this (directiveDescriptors: Enumerable.Empty<DirectiveDescriptor>(), context: context)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should talk about how we want to replace existing directives with the directive descriptors. Aka:

  1. Added in the CSharpCodeParser as "additional" directives?
  2. Added in the person who constructs CSharpCodeParser in a manner that enables users to remove default directives from the system.

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 think there's any value in making the "built in" directives unparsable by removing them. Users have the ability to make "built in" directives invalid in the IR if they don't want them to be supported.

{
MapDirectives(() => HandleDirective(directiveDescriptor), directiveDescriptor.Name);
}

MapDirectives(TagHelperPrefixDirective, SyntaxConstants.CSharp.TagHelperPrefixKeyword);
Copy link
Contributor Author

@NTaylorMullen NTaylorMullen Nov 23, 2016

Choose a reason for hiding this comment

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

These will be able to go away once they're implemented ontop of the directive descriptors. Issue..


IDirectiveDescriptorBuilder AddString();

IDirectiveDescriptorBuilder AddLiteral(string literal, bool optional);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Don't love the "optional" part of this but this comes into play when doing @addTagHelper "..." where the quotes are optional. Also works for ending optional semicolons which exist on many of our directives.

If you can think of something better I'm all ears 😄

Copy link
Member

Choose a reason for hiding this comment

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

My two ideas

  1. Semicolons are always optional in directives (you don't have a choice)
  2. Don't build @addTagHelper on top of extensible directives unless we want the "optional" part to become popular

Copy link
Member

@rynowak rynowak left a comment

Choose a reason for hiding this comment

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

The design of this looks right on the whole! Bring on the tests.

var directiveNode = (DirectiveIRNode)Builder.Pop();

var tokens = directiveNode.Children.OfType<DirectiveTokenIRNode>().ToList();
var exceptTokens = directiveNode.Children.Except(tokens).ToList();
Copy link
Member

Choose a reason for hiding this comment

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

I get why you're doing this, but wouldn't it be better to just leave the Children as is and make Tokens do something like:

public IEnumerable<DirectiveTokenIRNode> Tokens => Children.OfType<DirectiveTokenIRNode>();


namespace Microsoft.AspNetCore.Razor.Evolution
{
public class DirectiveDescriptor
Copy link
Member

Choose a reason for hiding this comment

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

Add a private constructor

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That'd break deserialization


public DirectiveDescriptorType Type { get; set; }

public IList<DirectiveTokenDescriptor> Tokens { get; set; }
Copy link
Member

Choose a reason for hiding this comment

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

Wait, what? Is this an implementation of the builder or a factory of the builder?

Copy link
Member

Choose a reason for hiding this comment

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

Then how about putting these factory methods on another type - static class DirectiveDescriptorBuilder


namespace Microsoft.AspNetCore.Razor.Evolution
{
public enum DirectiveDescriptorType
Copy link
Member

Choose a reason for hiding this comment

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

IMO we should suffix enums like this with Kind instead of Type. It's easy to overload the meaning of type.


IDirectiveDescriptorBuilder AddString();

IDirectiveDescriptorBuilder AddLiteral(string literal, bool optional);
Copy link
Member

Choose a reason for hiding this comment

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

My two ideas

  1. Semicolons are always optional in directives (you don't have a choice)
  2. Don't build @addTagHelper on top of extensible directives unless we want the "optional" part to become popular

@@ -39,11 +39,16 @@ internal class CSharpCodeParser : TokenizerBackedParser<CSharpTokenizer, CSharpS
private Dictionary<CSharpKeyword, Action<bool>> _keywordParsers = new Dictionary<CSharpKeyword, Action<bool>>();

public CSharpCodeParser(ParserContext context)
: this (directiveDescriptors: Enumerable.Empty<DirectiveDescriptor>(), context: context)
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 think there's any value in making the "built in" directives unparsable by removing them. Users have the ability to make "built in" directives invalid in the IR if they don't want them to be supported.

@NTaylorMullen NTaylorMullen force-pushed the nimullen/directives.853 branch from 30388a5 to b513709 Compare November 23, 2016 18:18
@NTaylorMullen NTaylorMullen changed the title [Design] Add extensible directive abstractions Add extensible directive abstractions Nov 24, 2016
@NTaylorMullen
Copy link
Contributor Author

🆙 📅 added tests

@NTaylorMullen NTaylorMullen force-pushed the nimullen/directives.853 branch from 749866d to cc0ae6a Compare November 24, 2016 01:18
Copy link
Member

@rynowak rynowak left a comment

Choose a reason for hiding this comment

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

:shipit:

}

return descriptorX != null &&
string.Equals(descriptorX.Value, descriptorY.Value, StringComparison.Ordinal) &&
Copy link
Member

Choose a reason for hiding this comment

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

This can nullref if x != null && y == null

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nope. The if above prevents that 😄

if (descriptorX == descriptorY)

Copy link
Member

Choose a reason for hiding this comment

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

The previous if is a short-circuit for the case where descriptorX and descriptorY are references to the same instance or both null. Need something like the following before this expression:

if (descriptorX == null || descriptorY == null)
{
   // Not reachable if both descriptors are null.
   return false;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If descriptorX != null then descriptorY != null because the == statement above would have short-circuited.

What you're suggesting is that we return false instead of true if both the descriptors being compared are null. Probably smart. We should do this for our other comparers because they fall into the same trap.

Copy link
Member

Choose a reason for hiding this comment

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

Huh?

If descriptorX != descriptorY and descriptorX != null, descriptorY could be anything except the same reference as descriptoryX -- including null. That is, the code currently excludes only a single non-null value for descriptorY. The code you checked in can easily null ref.

Second, two null references should compare equal. I was suggesting an addition:

if (descriptorX == descriptorY)
{
    return true;
}

if (descriptorX == null || descriptorY == null)
{
    return false;
}

return (string.Equals(descriptorX.Name, descriptorY.Name, StringComparison.Ordinal) &&
    descriptorX.Kind == descriptorY.Kind &&
    Enumerable.SequenceEqual( ... );

I had a look at the other comparers in Razor. Primarily the test ones are null-safe; they often Assert.NotNull() for both parameters.

Copy link
Member

Choose a reason for hiding this comment

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

What he said

Copy link
Member

Choose a reason for hiding this comment

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

And by that I mean, that I agree with my countryman 🇨🇦 🏒 🍁 🇨🇦

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I be dumb 👍

- Based generic directive implementation off of descriptors.
- Added parsing logic to consume descriptors and parse content that's expected.
- Added parsing errors to automagically detect unexpected directive pieces.
- Updated visitor implementations to understand the directive bits.
- Added a builder abstraction to easily create descriptors. Had to maintain the ability to manually construct a descriptor to enable convenient serialization/deserialization.
- Added tests/comparers to verify correctness of parsing.

#853
@NTaylorMullen NTaylorMullen force-pushed the nimullen/directives.853 branch from cc0ae6a to 518378f Compare November 24, 2016 03:56
@NTaylorMullen NTaylorMullen merged commit 518378f into dev Nov 24, 2016
@NTaylorMullen NTaylorMullen deleted the nimullen/directives.853 branch November 24, 2016 03:58
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants