Skip to content

Implement subset-based ordering for naming rules#36366

Merged
sharwell merged 7 commits intodotnet:masterfrom
sharwell:order-naming-rules
Jun 19, 2019
Merged

Implement subset-based ordering for naming rules#36366
sharwell merged 7 commits intodotnet:masterfrom
sharwell:order-naming-rules

Conversation

@sharwell
Copy link
Copy Markdown
Contributor

@sharwell sharwell commented Jun 12, 2019

Fixes #36355

From my previous investigation:

I reviewed some naming rules and found that many cases will be correct if we follow a best-effort subset ordering. For example, consider the following rules:

  1. Non-private static fields are PascalCase
  2. Non-private readonly fields are PascalCase
  3. Constants are PascalCase
  4. Static fields are s_camelCase
  5. Fields are _camelCase

The potential points of confusion are:

  1. Rule (1) and rule (4). In this case, rule (1) is strictly more specific than rule (4), so it would take precedence
  2. Rule (2) and rule (4). In this case, neither rule is strictly more specific than the other
  3. Rule (2) and rule (5). In this case, rule (2) is strictly more specific than rule (5), so it would take precedence
  4. Rule (4) and rule (5). In this case, rule (4) is strictly more specific than rule (5), so it would take precedence

I did note that whenever an overlap arises that isn’t a strict-subset relation, e.g. rule (2) and rule (4), it’s possible to define a new rule which covers exactly the intersection. In this case it would be a rule for “non-private static readonly fields”. I would propose to order the rules as follows, and allow the user to add a missing rule in the event an incorrect result is produced in the intersection:

  1. If the accessibilities of rule A are a subset of those in rule B, apply rule A first (accessibilities are “match any”)
  2. Otherwise, if the required modifiers of rule A are a superset of those in rule B, apply rule A first (required modifier are “match all”)
  3. Otherwise, if the symbols of rule A are a subset of those in rule B, apply rule A first (symbols are “match any”)

This change implements the above ordering proposal, with a final fallback to rule name order to ensure the order is deterministic for any given configuration.

Update 13 June:

Based on an evaluation of .editorconfig files on GitHub with naming rules, the following changes were made to the ordering:

  1. Ordering by modifiers occurs before ordering by accessibility. This change accounts for the fact that people frequently have ordering for all static members, and separately have ordering for non-private instance fields.
  2. When sorting, the 'const' modifier is treated as always including 'static' and 'readonly' (so 'const' matches a subset of the symbols matched by 'static' and/or 'readonly').
  3. The final sort by names uses the rule name instead of the style name.

…e of truth"

Prior to this change, user settings could interact with .editorconfig in unknown ways to
produce deviations in code style across a team which could not be controlled by simply
updating .editorconfig. This change ensures that .editorconfig, when used, becomes the
code style "source of truth" for affected documents.
@sharwell sharwell requested a review from a team as a code owner June 12, 2019 15:20
@sharwell sharwell force-pushed the order-naming-rules branch from 7a1a52a to 86eb4fa Compare June 12, 2019 16:19
@jasonmalinowski
Copy link
Copy Markdown
Member

@sharwell Can you include in a commit message (or just this PR) the email you sent internally describing this and the cases you were seeing? It's really helpful to understand the change and I'd hate for other reviewers or people not on the email to not have that context.

@sharwell
Copy link
Copy Markdown
Contributor Author

@jasonmalinowski added it

@jasonmalinowski
Copy link
Copy Markdown
Member

@sharwell Thanks. While looking at the code (doing so right now and but haven't posted yet) I see it might be awesome to stick the comment too in the logic where all the ordering is in case if this ever gets detached from the PR. But otherwise awesome for putting it here because the "why" was otherwise missing.

@sharwell
Copy link
Copy Markdown
Contributor Author

sharwell commented Jun 12, 2019

@jasonmalinowski Once the algorithm stabilizes I'll do that. I'm going to work with @madskristensen to implement an analyzer for the EditorConfig Language Service that detects cases where the ordering above is not the same as the order in which rules appear in the file, and offer to take action to correct it:

  1. Reorder rules in the file to match the order of evaluation (i.e. the compiler is correct and editorconfig should reflect it)
  2. Define a new rule covering the intersection between the conflicting rules, and force it to match the first rule, and then reorder the conflicting rules (i.e. the editorconfig file is correct, but the compiler can't know that so we create a new one that the compiler and file order agree on)

Copy link
Copy Markdown
Member

@jasonmalinowski jasonmalinowski left a comment

Choose a reason for hiding this comment

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

Some general comments but it feels like there's a few tests missing:

  1. Tests asserting that we're case sensitive for rule names if rule names are differing only by case.
  2. A test that confirms that our choice of which rules go first is also correct. The OrderBy/ThenBy makes it clear enough in the code what order is but it seems that I could change that ordering in the code and no test would break. I'm not sure if it's worth having a case added where two rules are applied both with differing accessibility and modifiers, and the right one is still taken. Consider the common cases that got us here in the first place?

[Theory]
[InlineData("a", "b", "a", "public", "public, private")]
[InlineData("b", "a", "a", "public, private", "public")]
[InlineData("b", "a", "b", "public", "public, private")]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Consider a comment that this and the next are verifying we aren't accidentally sensitive to textual names?

@jasonmalinowski
Copy link
Copy Markdown
Member

And otherwise this looks great modulo the nitpicks, but it seems those missing tests make me a bit worried to outright hit signoff.

sharwell added 2 commits June 13, 2019 16:16
During modifier ordering, 'const' is explicitly treated as a further
restriction on 'static' and 'readonly'.
@sharwell
Copy link
Copy Markdown
Contributor Author

@jasonmalinowski I'm going to file a follow-up issue to get more tests added

@sharwell sharwell force-pushed the order-naming-rules branch from fa5fb78 to f0b12ff Compare June 13, 2019 23:14
@sharwell
Copy link
Copy Markdown
Contributor Author

🔗 See madskristensen/EditorConfigLanguage#71 for the new analyzer to help users avoid problems in this space

@jasonmalinowski
Copy link
Copy Markdown
Member

@sharwell Do you need a follow-up review or I see you're still tweaking some things?

@sharwell
Copy link
Copy Markdown
Contributor Author

@jasonmalinowski I'm going to try and find a way to add the name for ordering without changing the serialization everywhere.

return (result: combinedNamingStylePreferences, succeeded: true);
}

// no existing naming styles were passed so just return the set of styles that were parsed from editorconfig
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Comment is now stale.

internal static ModifierKind FromXElement(XElement modifierElement)
=> new ModifierKind((ModifierKindEnum)Enum.Parse(typeof(ModifierKindEnum), modifierElement.Value));

public override bool Equals(object obj)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is just for the perf benefit of not relying on the slow struct equals?

namingStyles.Add(namingStyle);
namingRules.Add(serializableNamingRule);

var ruleKey = (serializableNamingRule.SymbolSpecificationID, serializableNamingRule.NamingStyleID, serializableNamingRule.EnforcementLevel);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This feels like it needs a lot of comments.

if (modifier.ModifierKindWrapper == SymbolSpecification.ModifierKindEnum.IsStatic
|| modifier.ModifierKindWrapper == SymbolSpecification.ModifierKindEnum.IsReadOnly)
{
if (x.SymbolSpecification.RequiredModifierList.Any(x => x.ModifierKindWrapper == SymbolSpecification.ModifierKindEnum.IsConst))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is it easier just writing RequiredModifierList.Contains(new ModifierKind(ModifierKindEnum.Const)) or something? I'll admit it tripped me up that here we're doing an Any and below we're doing a Contains, but that's largely because this whole wrapping thing is...icky. (And not your fault.)

@jasonmalinowski jasonmalinowski requested a review from dpoeschl June 14, 2019 21:33
@jinujoseph jinujoseph added this to the 16.2.P4 milestone Jun 19, 2019
@sharwell sharwell merged commit e1146e5 into dotnet:master Jun 19, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Regression] Wrong naming rules are being enforced for static fields

4 participants