Skip to content

Conversation

DustinCampbell
Copy link
Member

Every RazorCompletionItem creates an instance of ItemCollection and exposes it via an Items property. The property lazily constructs the ItemCollection, but that doesn't really matter much because the property is always accessed. Every time the RazorCompletionItem constructor is called a "description info" object (used to build completion description tooltips) is immediately added to the Items property. No other data is stored in the ItemCollection.

It is unnecessary overhead to store a single object per RazorCompletionItem in an ItemCollection. Internally, ItemCollection is implemented with a ConcurrentDictionary, so it's definitely not cheap!

This change removes the Items property altogether and provides a DescriptionInfo property that is set via the constructor. I've also done a fair amount of clean up in separate commits. The first commit contains the meat of removing the Items property.

Every RazorCompletionItem creates an instance of ItemCollection and exposes it via an Items property.
The Items property constructs the ItemCollection lazily, but that doesn't matter since it accessed immediately after each RazorCompletionItem constructor call to add a "description info" object (used to build completion description tooltips). No other data is stored in the ItemCollection.

This is an unnecessary amount of overhead to store a single object per RazorCompletionItem. Internally, ItemCollection is implemented with a ConcurrentDictionary, so its definitely not cheap!

The change removes the Items property altogether and provides a DescriptionInfo property that is set via the constructor.
@DustinCampbell DustinCampbell requested a review from a team as a code owner January 7, 2025 22:09
return _items;
}
}
public static RazorCompletionItem CreateMarkupTransition(
Copy link
Contributor

Choose a reason for hiding this comment

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

CreateMarkupTransition

Nit: I personally would've preferred this added to MarkupTransitionCompletionItemProvider since that class is already specific to MarkupTransition.

Copy link
Contributor

Choose a reason for hiding this comment

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

I was doing review commit-by-commit - looking at the last commit, I see we added all factory methods to the completion item itself. I personally would've added them in the corresponding provider types as those are already specific to those more specific item instances, but don't have very strong feelings either way.

Copy link
Member Author

@DustinCampbell DustinCampbell Jan 8, 2025

Choose a reason for hiding this comment

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

The purpose of these factory methods was to ensure that a RazorCompletionItem is always created correctly. To support that goal, I want to enforce that RazorCompletionItem can only be constructed via factory methods. If the factory methods aren't on RazorCompletionItem, the constructor would have to be public and there wouldn't be any enforcement. Plus, it is far more typical from a discoverability perspective for "Create" factory methods for a type to appear on a either the type that is being created or a dedicated factory type.

Copy link
Member Author

@DustinCampbell DustinCampbell Jan 8, 2025

Choose a reason for hiding this comment

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

I personally would've preferred this added to MarkupTransitionCompletionItemProvider since that class is already specific to MarkupTransition.

FYI that I appreciate you explaining your thinking and that's a good place to start logically. For me, when I look at RazorCompletionItem, I see the logic for MarkupTransition already split across other related areas, such as RazorCompletionItemResolver. So, making RazorCompletionItem a bit more knowledgeable about the RazorCompletionItemKinds that it's constructed with seemed reasonable, and the encapsulation win helps avoid future bugs.

insertText: SyntaxConstants.TextTagName,
descriptionInfo: new(CodeAnalysisResources.MarkupTransition_Description),
commitCharacters: RazorCommitCharacter.CreateArray([">"]));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: I that we replaced these checks with ArgHelper.ThrowIfNull in some cases but not in others. Curious about reasoning behind the choices, e.g. why not use ArgHelper.ThrowIfNull here?

Copy link
Member Author

Choose a reason for hiding this comment

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

IMO, there isn't a need to use ArgHelper.ThrowIfNull in internal code called exclusively by our code if the calls won't compile when null is passed.

using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.CodeAnalysis.Razor.Tooltip;
using Microsoft.VisualStudio.Editor.Razor;
using RazorSyntaxNode = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxNode;
Copy link
Contributor

Choose a reason for hiding this comment

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

RazorSyntaxNode

Nit: we had a discussion about this before, there is actually Microsoft.AspNetCore.Razor.Language.Syntax.RazorSyntaxNode. While this change is not an issue for the compiler, I wonder if it may cause human reader confusion as to what type is actually used.

Copy link
Member Author

Choose a reason for hiding this comment

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

I moved this to the top of the file because C#'s Add Using code action adds new using directives in the wrong place if there are usings at the top of the file and inside the namespace.

Yes, we had a discussion, but I don't remember agreeing to alias SyntaxNode in this way, so there may have been confusion about the outcome. (FWIW, most cases like this in Razor tooling alias SyntaxNode as RazorSyntaxNode.)

RazorSyntaaxNode is actually a superfluous type in the compiler. It sits in the inheritance hierarchy between SyntaxNode and all Razor syntax node types. I suspect that it exists to as a built in "alias" for SyntaxNode to avoid exactly the problem that this using type directive is trying to avoid. In fact, I'm pretty sure we could delete all using RazorSyntaxNode = and using SyntaxNode = directives from Razor tooling code, replace SyntaxNode with RazorSyntaxNode everywhere, and everything would work fine. 😄

There are actually only six types that inherit from SyntaxNode directly rather than RazorSyntaxNode: SyntaxTrivia, SyntaxList (+ 3 variations), and RazorSyntaxNode itself. Given that tooling doesn't ever care about processing these types as syntax nodes, it seems like all of the effort to add using aliases is probably unnecessary. If we just use the real RazorSyntaxNode throughout, there'd be more consistency and that would alleviate any human reader confusion.

Copy link
Contributor

@alexgav alexgav left a comment

Choose a reason for hiding this comment

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

:shipit:

RazorCompletionItem implements value equality, but that equality doesn't appear to be used in production code. Looking back through the history of `RazorCompletionItem.cs` it seems that `IEquatable` was added for testing in a8a91e2 when it was a much simpler type. Removing the value equality does not break tests, so I'm removing the Equals override to avoid confusion and subtle equality bugs that might crop up in the future.
@DustinCampbell DustinCampbell merged commit dc45fbc into dotnet:main Jan 9, 2025
17 checks passed
@DustinCampbell DustinCampbell deleted the completion-items-prop branch January 9, 2025 15:49
@dotnet-policy-service dotnet-policy-service bot added this to the Next milestone Jan 9, 2025
@jjonescz jjonescz modified the milestones: Next, 17.14 P1 Mar 11, 2025
DustinCampbell added a commit that referenced this pull request Jun 13, 2025
`ItemCollection` is essentially a property bag that results in
dictionaries being used instead of plain ol' fields, and it's been
overused in Razor. I've been trying to get rid of it for nearly a year.
#10720, #10764, #11360, and #11931 each represent changes that remove a
use of `ItemCollection`. This pull request finally gets rid of
`ItemCollection` completely.

The last use of `ItemCollection` is the `RazorCodeDocument.Items`, which
is used by the compiler to store various objects during compilation, and
tooling uses it to cache some expensive-to-create data. This change
stores all of the compiler objects on `RazorCodeDocument` directly. The
data cached by tooling is stored on the side in a
`ConditionalWeakTable`.

As part of this, I've refactoring the
`RazorCodeDocument.TryComputeNamespace` extension method to be more
understandable and maintainable.

----
CI Build:
https://dev.azure.com/dnceng/internal/_build/results?buildId=2728842&view=results
Test Insertion:
https://dev.azure.com/devdiv/DevDiv/_git/VS/pullrequest/643246
Toolset Run:
https://dev.azure.com/dnceng/internal/_build/results?buildId=2728843&view=results
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants