Skip to content

Conversation

@DustinCampbell
Copy link
Member

@DustinCampbell DustinCampbell commented Jun 11, 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

@DustinCampbell DustinCampbell requested review from a team as code owners June 11, 2025 20:09
@DustinCampbell
Copy link
Member Author

DustinCampbell commented Jun 11, 2025

There's an AspNetCoreMvc test failing with this change: Microsoft.AspNetCore.Mvc.FunctionalTests.RazorPagesNamespaceTest.Page_ImportedNamespace_UsedFromViewImports(). Will fix soon!

Fixed now.

Copy link
Member

@davidwengier davidwengier left a comment

Choose a reason for hiding this comment

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

Tooling looks good. PR title brings nothing but joy :D

@DustinCampbell DustinCampbell changed the base branch from dev/dustinca/intermediate-nodes to main June 12, 2025 13:48
@DustinCampbell DustinCampbell changed the base branch from main to dev/dustinca/intermediate-nodes June 12, 2025 15:21
@DustinCampbell DustinCampbell changed the base branch from dev/dustinca/intermediate-nodes to main June 12, 2025 15:21
Store the RazorSyntaxTree produced before the tag helper rewrite as a field on RazorCodeDocument instead of the ItemCollection.
Store the IReadOnlyList<TagHelperDescriptors> as a field on RazorCodeDocument instead of the ItemCollection.
Store the ISet<TagHelperDescriptors> as a field on RazorCodeDocument instead of the ItemCollection.
Store the RazorSyntaxTree as a field on RazorCodeDocument instead of the ItemCollection.
Store the import RazorSyntaxTrees as a field on RazorCodeDocument instead of the ItemCollection.
Store the DocumentIntermediateNode as a field on RazorCodeDocument instead of the ItemCollection.
Store the RazorCSharpDocument as a field on RazorCodeDocument instead of the ItemCollection.
Store CssScope on RazonCodeGenerationOptions instead of the RazorCodeDocument's ItemCollection.
This is just a bit of clean up. The code generation options do not need to be stored on RazorCSharpDocument or RazorHtmlDocument, since they're already stored on RazorCodeDocument.
Avoid creating a new NamespaceDirectiveVisitor for each RazorSyntaxTree
Store the computed namespace name and span on RazorCodeDocument instead of the ItemCollection.
Store the RazorHtmlDocument as a field on RazorCodeDocument instead of the ItemCollection.
Tooling stashes a couple of items in RazorCodeDocument.Items. Employ a ConditionalWeakTable to avoid this.
At long last, ItemCollection is now deleted.
Introduce PropertyTable, which wraps an array of values and exposes each slot as a property. Properties can be reference types or value types. For value types, a StrongBox<T> is used in the array to hold the value.
When searching imports for @namespace directives, we need to iterate them in reverse order to ensure that we prefer imports closer to the source document.
@DustinCampbell DustinCampbell force-pushed the remove-itemcollection branch from aa89304 to 5efed97 Compare June 12, 2025 15:34
return syntaxTree.Root is RazorSyntaxNode root
? root.FindInnermostNode(absoluteIndex)
: null;
return codeDocument.GetRequiredSyntaxRoot().FindInnermostNode(absoluteIndex);
Copy link
Contributor

Choose a reason for hiding this comment

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

GetRequiredSyntaxRoot

nit: used to not throw if tree.Root was null.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, but this was wrong and unnecessarily paranoid, so I changed it here. RazorSyntaxTree.Root is annotated as non-null and RazorSyntaxTree's constructor throws if root is null:

internal RazorSyntaxTree(
SyntaxNode root,
RazorSourceDocument source,
ImmutableArray<RazorDiagnostic> diagnostics,
RazorParserOptions options)
{
ArgHelper.ThrowIfNull(root);
ArgHelper.ThrowIfNull(source);
ArgHelper.ThrowIfNull(options);
Root = root;
Source = source;
_diagnostics = diagnostics.NullToEmpty();
Options = options;
}

Copy link
Contributor

@ToddGrun ToddGrun left a comment

Choose a reason for hiding this comment

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

:shipit:

- Remove the index consts because they don't really add much value, since they're just used once and its clear what they mean.
- Make Property<T> reference the _values slot directly on modern .NET with a ref field.
- Make BoxedProperty<T>.StrongBox a readonly field.
- Add comments
Copy link
Member

@chsienki chsienki left a comment

Choose a reason for hiding this comment

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

Awesome. :shipit:

@DustinCampbell
Copy link
Member Author

Thanks everyone! ♥️

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.

6 participants