feat: link Razor and Blazor views into the C# dependency graph - #682
Open
grinidx wants to merge 1 commit into
Open
feat: link Razor and Blazor views into the C# dependency graph#682grinidx wants to merge 1 commit into
grinidx wants to merge 1 commit into
Conversation
The csharp plugin discovered only `.cs`, so `.razor` and `.cshtml` were invisible. Code reachable only from markup therefore looked unreferenced: a `Widget.razor.cs` code-behind partial is reported as an orphaned file with zero importers and a suggestion to delete it, even though deleting it breaks the build. Views are parsed for edges but stay out of the scored extension set, mirroring how the typescript plugin already handles `.svelte`, `.vue` and `.astro`. Scores for existing projects are unchanged. Edges resolve by symbol name rather than by namespace. A view's `@using` says which namespaces are in scope, not which files it depends on, so linking a whole namespace would mark every file in it as live and hide genuinely dead code. Resolved per view: - types the view names, via a type-name index - extension methods it calls, which name no type at the call site - its own code-behind partial (`Widget.razor` -> `Widget.razor.cs`) - components it renders, including components declared in plain C# - partials and layouts referenced by string name - view components and tag helpers, which resolve by naming convention - `@page` marks a view as a routable root, like `Program.cs` - `_Imports.razor` and `_ViewImports.cshtml` usings apply to the subtree Also zones the ambient import files as config and `.razor.g.cs` / `.cshtml.g.cs` as generated, and stops `build_dep_graph` returning early on a project that is all views and no `.cs`. Measured on a ten-project Razor Pages solution: 334 views enter the graph, contributing 1052 edges across 200 `.cs` files, with 136 routable pages marked as roots. No file changed orphan status on that codebase, because the existing namespace matching already linked them.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The csharp plugin discovers only
.cs(languages/csharp/__init__.py,extensions=[".cs"]), so.razorand.cshtmlare invisible to it. Code reachable only from markup therefore looks unreferenced.Minimal Blazor repro - a component with a code-behind partial:
Widget.razor.csis not dead. It is the code-behind forWidget.razor, whichHome.razorrenders. Deleting it breaks the build. The scan also reported "3 production files" for a 6-file project, because the three views were never seen.Approach
Views are parsed for edges but stay out of the scored extension set, mirroring how the typescript plugin already handles
.svelte,.vueand.astro(languages/typescript/detectors/deps/__init__.py,_FRAMEWORK_EXTENSIONS). Scores for existing projects are unchanged; this only adds graph edges.Edges resolve by symbol name, not by namespace. This is the main design decision. My first attempt fed a view's
@usingintoexpand_namespace_matches, and it silently marked genuinely dead code as live:@using App.Serviceslinked every file in that namespace, and since_Imports.razorbroadcasts usings to a whole directory subtree, one ambient import would have marked entire namespaces live across an app. A@usingsays which namespaces are in scope, not which files are depended on. So the view edges go through a type-name index instead, with the using set used only to constrain scope.Resolved per view:
Widget.razor->Widget.razor.cs,Index.cshtml->Index.cshtml.cs).razorfile<partial name="_Card" />,Layout = "_Layout")InvokeAsync("Basket")->BasketViewComponent,<price-tag />->PriceTagTagHelper)@pagemarks a view as a routable root, likeProgram.cs_Imports.razorand_ViewImports.cshtmlusings apply to the directory subtreeAlso zones the ambient import files as config and
.razor.g.cs/.cshtml.g.csas generated, and stopsbuild_dep_graphreturning early on a project that is all views and no.cs(a Razor class library).Testing
17 new tests in
languages/csharp/tests/test_csharp_deps_razor.py, following the structure ofTestFrameworkFilesintest_ts_deps.py.They include negative controls, which I would point a reviewer at first: a type no view names, an unused tag helper, and a never-included partial all stay unlinked. Those tests are what caught the namespace-granularity mistake above, and a second real bug where
build_dep_graphreturned early on a view-only project so none of this code ran.Full suite: 6686 passed, 164 skipped. Six failures are pre-existing and identical on
main(test_cli.py::TestStatePath,test_helpers.py::test_state_path_from_lang_arg,test_bash_unused_imports.py).rufflint gate,mypy, and the import-linter contracts are all clean.Measured effect on real codebases
On a ten-project Razor Pages solution (388k LOC, 334 views): 334 views enter the graph, contributing 1052 edges across 200
.csfiles, with 136 routable pages marked as roots. On a small Blazor app: 5 views, 4 edges.No file changed orphan status on either, and I would rather say so than imply otherwise. On those codebases the existing
.csnamespace matching already links everything the views reach, so the false positive above does not surface there. I confirmed this by measuring rather than assuming: withexpand_namespace_matchesexperimentally tightened, the view edges still rescue exactly zero files. The bug is real and reproducible, but its blast radius is projects where the namespace heuristic does not already over-link.That interaction is worth its own discussion and I will open a separate issue for it, since the underlying cause turns out to be that the C# graph cannot resolve fully-qualified type references at all. Nothing in this PR depends on that being resolved.
Known limits
Html.PartialAsync(someVariable)) cannot be resolved statically@inject IFooServicelinks the interface, not the implementation, which is normally DI-registered from.csanyway