|
| 1 | +# P-028 — Unneeded-dependency profile (`Own.Lean`) |
| 2 | + |
| 3 | +- **Status:** draft — not started. |
| 4 | +- **Depends on:** [P-001](P-001-csharp-extractor.md) (the Roslyn extractor |
| 5 | + seam), [P-006](P-006-di-lifetimes.md) (the DI `services[]` registration |
| 6 | + graph — YDN002 extends its facts with the closed generic arguments the |
| 7 | + existing graph collapses away, see Sketch), [P-015](P-015-configuration-surface.md) |
| 8 | + (severity/opt-in surface for the phase-2 heuristics). Bounded explicitly |
| 9 | + against [P-021](P-021-async-audit-pack.md) (`ASYNC040` already owns the |
| 10 | + "trivial async passthrough" case — not duplicated here) and |
| 11 | + [P-023](P-023-architecture-guard.md) (Own.Arch gates *forbidden* structure; |
| 12 | + this profile flags *provably redundant* structure — different verdict shape, |
| 13 | + never a build gate). |
| 14 | + |
| 15 | +## Motivation |
| 16 | + |
| 17 | +The `you-dont-need/You-Dont-Need` meta-list (a curated collection of |
| 18 | +"You Might Not Need Lodash/Moment/Redux/…" write-ups) makes one real point |
| 19 | +under all the individual takes: teams often reach for a popular dependency |
| 20 | +because it is popular, not because the problem in front of them needs it. The |
| 21 | +honest version of that point is not "dependencies are bad" — it is that every |
| 22 | +dependency has to clear a bar: |
| 23 | + |
| 24 | +```text |
| 25 | +dependency_value > dependency_cost |
| 26 | +``` |
| 27 | + |
| 28 | +where cost is never just install size — it is maintenance, transitive CVEs, |
| 29 | +build complexity, onboarding, and the debugging friction of an indirection |
| 30 | +layer nobody on the team wrote. .NET has its own instances of the same |
| 31 | +pattern: an `AutoMapper` profile that copies five identically-named properties |
| 32 | +and nothing else, a `MediatR` handler with exactly one implementation and no |
| 33 | +pipeline behaviours standing in for a direct method call. The libraries are |
| 34 | +not the problem — using them where they buy nothing is. |
| 35 | + |
| 36 | +The trap is that "you don't need X" is trivially easy to turn into an |
| 37 | +opinionated hot-take generator (see the source list's own "You Might Not Need |
| 38 | +TypeScript" entry) that flags a library's mere presence. That is exactly the |
| 39 | +kind of noisy, ungrounded quality gate this project's other proposals |
| 40 | +deliberately reject (see P-023's "no SOLID detector" stance). So the scope |
| 41 | +here is narrower and stricter than the inspiration: |
| 42 | + |
| 43 | +> **Own.Lean never passes judgment on a library. It flags one call site at a |
| 44 | +> time, only when the code at that site proves the abstraction added nothing |
| 45 | +> — and the moment any real customization is visible, it stays silent.** |
| 46 | +
|
| 47 | +## Scope |
| 48 | + |
| 49 | +### MVP — deterministic, evidence-only |
| 50 | + |
| 51 | +| Code | Finding | Evidence required | Suggestion | |
| 52 | +|------|---------|--------------------|------------| |
| 53 | +| `YDN001` | `AutoMapper` `CreateMap<TSrc,TDest>()` (or `Profile`-declared map) that is a pure 1:1 copy | every public writable member of `TDest` has an exact-name, assignable-type public readable counterpart on `TSrc`; **no** `.ForMember`/`.Ignore`/`.ConvertUsing`/custom value resolver/`.ReverseMap`; member count within a configurable bound (default 10) | replace with an explicit object initializer or a mapping constructor | |
| 54 | +| `YDN002` | `MediatR` `IRequestHandler<TReq[,TResp]>` resolved via `ISender`/`IMediator` with exactly one registered implementation and zero registered `IPipelineBehavior<,>` (open or closed) anywhere in the DI graph | needs the DI registration graph's generic arguments preserved per `IRequestHandler<TReq[,TResp]>` registration (see Sketch — today's graph collapses these) | inject the handler directly instead of dispatching through the mediator | |
| 55 | + |
| 56 | +`YDN001` is structurally the same shape already used for `DI001` (P-006): read |
| 57 | +a graph the extractor already builds, compare cardinalities and declared |
| 58 | +customization, emit a verdict only when the customization set is empty. |
| 59 | +`YDN002` needs the same shape but over a graph the extractor does not yet |
| 60 | +build in the needed resolution — see Sketch. Neither rule inspects call-site |
| 61 | +*style* — only the declared shape of the mapping/registration. |
| 62 | + |
| 63 | +### Phase 2 — heuristic, report-only, opt-in via P-015 |
| 64 | + |
| 65 | +| Code | Finding | Confidence | |
| 66 | +|------|---------|------------| |
| 67 | +| `YDN010` | A DI-registered service with exactly one registration across the whole solution, not exposed as a public extension point, and never re-registered in a test project | heuristic — a real single-impl service and a "this interface is pure ceremony" service look identical without knowing intent; ships as report-only or not at all | |
| 68 | + |
| 69 | +This tier stays report-only, never a build gate, and is the honest limit of |
| 70 | +what this profile should attempt — see Non-goals for the parts of the "You |
| 71 | +Don't Need" list that were deliberately left out rather than downgraded to |
| 72 | +Phase 2. |
| 73 | + |
| 74 | +## Non-goals |
| 75 | + |
| 76 | +- **No library blocklist.** "Don't use Lodash/Axios/Moment" has no .NET |
| 77 | + analogue that would be evidence rather than opinion, and even in spirit, |
| 78 | + Own.Lean does not ship a list of disfavoured packages. Every finding names a |
| 79 | + specific call site and the specific evidence at it. |
| 80 | +- **No "replace the ORM with hand-written SQL" suggestion.** Whether a |
| 81 | + hand-rolled query beats an ORM call depends on performance requirements this |
| 82 | + tool cannot observe statically. Not evidence-based; not built. |
| 83 | +- **No reflection → source-generator suggestion.** There is no oracle for |
| 84 | + "this reflection could have been codegen'd" short of writing the generator — |
| 85 | + guesswork, not a finding. |
| 86 | +- **No JSON → MessagePack/binary-format suggestion.** A wire-format choice |
| 87 | + depends on external constraints (interop, human-readability requirements) |
| 88 | + invisible to static analysis. |
| 89 | +- **No reimplementation of existing Roslyn/FxCop LINQ micro-optimizations** |
| 90 | + (`.Where(p).Count()` → `.Count(p)` and siblings — already `CA1826`/`CA1827`/ |
| 91 | + `CA1828`/`CA1829`). Own.NET's differentiator is checks nobody else runs, not |
| 92 | + a third copy of a rule two analyzers already ship. |
| 93 | +- **No build-blocking severity, ever, for this family.** Every `YDN###` is |
| 94 | + info/warning and never wired into the P-023 architecture-guard ratchet. A |
| 95 | + false positive here costs a reviewer one comment, not a red PR. |
| 96 | +- **No bundle-size / transitive-CVE / dependency-count scoring.** That is a |
| 97 | + supply-chain audit tool (NuGet advisory scanning, dependency-graph size), |
| 98 | + a different project; if ever pursued it is its own proposal, not folded in |
| 99 | + here. |
| 100 | +- **No hostility to AutoMapper or MediatR as libraries.** Both are legitimate |
| 101 | + the moment they are used for what they are for — custom resolvers, cross- |
| 102 | + cutting pipeline behaviours, polymorphic dispatch over many handlers. |
| 103 | + `YDN001`/`YDN002` are silent the instant any of that evidence appears. |
| 104 | + |
| 105 | +## Sketch |
| 106 | + |
| 107 | +```text |
| 108 | +C# source --[Roslyn extractor]--> mapping-profile facts (YDN001) |
| 109 | + \-> services[] graph, extended with closed generic args (YDN002) |
| 110 | + | |
| 111 | + [core: same Python seam] |
| 112 | + | |
| 113 | + YDN### verdicts --> SARIF + markdown |
| 114 | +``` |
| 115 | + |
| 116 | +`YDN001` needs one new extractor fact family: for each `CreateMap<TSrc,TDest>` |
| 117 | +call (or `Profile`-declared map), emit the two member lists plus whichever |
| 118 | +customization calls (`.ForMember`, `.Ignore`, `.ConvertUsing`, `.ReverseMap`) |
| 119 | +appear in the same fluent chain. |
| 120 | + |
| 121 | +`YDN002` is **not** a free ride on the existing P-006 `services[]` graph, and |
| 122 | +the MVP scope above was wrong to claim otherwise (caught in review): the |
| 123 | +extractor's `DiTypeName` helper |
| 124 | +(`frontend/roslyn/OwnSharp.Extractor/Program.cs`) deliberately reduces a |
| 125 | +generic registration to its rightmost identifier — `IRequestHandler<Foo,Bar>` |
| 126 | +and `IRequestHandler<Baz,Qux>` both become the bare `IRequestHandler` — because |
| 127 | +P-006's captive-lifetime checks never needed to distinguish closed generic |
| 128 | +arguments. `YDN002` does need that distinction: counting "implementations of |
| 129 | +*this* `TReq[,TResp]`" from an identifier-only graph would silently count |
| 130 | +every unrelated handler in the solution as the same bucket the moment a |
| 131 | +project has more than one MediatR request. The fix is a small, additive |
| 132 | +extractor change — preserve the closed type-argument pair (and the |
| 133 | +`IPipelineBehavior<,>` type arguments, open or closed) alongside the existing |
| 134 | +service/impl identifiers when the generic is one of the MediatR marker |
| 135 | +interfaces — not a reinterpretation of the current collapsed facts. |
| 136 | + |
| 137 | +## Open questions |
| 138 | + |
| 139 | +1. Where does the "still trivial" member-count bound for `YDN001` live — |
| 140 | + hardcoded default, or a P-015 per-project knob? Leaning: a default with a |
| 141 | + P-015 override, consistent with how severity is already configured |
| 142 | + elsewhere. |
| 143 | +2. Does `YDN002` also need to inspect the handler body for inline cross- |
| 144 | + cutting code (logging/validation) that a pipeline behaviour would normally |
| 145 | + own, or is DI-graph evidence (impl count + behaviour count) sufficient on |
| 146 | + its own? Needs a trial against a real MediatR-using corpus sample. |
| 147 | +3. Naming/positioning: a standalone `Own.Lean` family, or a phase-4 "ceremony" |
| 148 | + tier under `Own.Arch` (P-023)? Leaning: standalone — P-023 gates *forbidden* |
| 149 | + structure (a graph-edge violation); this profile flags *provably redundant* |
| 150 | + structure (an indirection with zero customization). The verdict shapes |
| 151 | + differ (a gate vs. a suggestion), which argues for keeping them separate |
| 152 | + families sharing only the extractor seam. |
| 153 | +4. Prefix: following the `ASYNC`/`ARCH`/`OBL` precedent of a family-specific |
| 154 | + code rather than overloading `OWN###` — `YDN###` as proposed above, unless |
| 155 | + a shorter/clearer prefix surfaces during naming review. |
0 commit comments