Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
221 changes: 112 additions & 109 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ clap = { version = "4.6.3", features = ["derive"] }
colored = "3.1.1"
dunce = "1.0.5"
fixedbitset = "0.5.7"
gix = { version = "0.86.0", default-features = false, features = ["blob-diff", "max-performance-safe", "revision", "sha1"] }
gix = { version = "0.87.1", default-features = false, features = ["blob-diff", "max-performance-safe", "revision", "sha1"] }
globset = "0.4.19"
ignore = "0.4.31"
lasso = { version = "0.7.3", features = ["multi-threaded"] }
Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,7 @@ walking up from the analysis root to the nearest directory containing one
{
"aggressive": true,
"roots": ["MyApp.Program.Main"],
"entryPoints": ["Jobs/", "Api/Handlers/**/*.cs"],
"libraryProjects": ["MyLib"],
"ignore": ["Migrations/**", "Generated/", "**/*.designer.cs"],
"deadCode": {
Expand Down Expand Up @@ -471,7 +472,11 @@ matches the whole directory, so `"Generated/"` needs no `**`). `aggressive`,
`roots`, and `libraryProjects` set defaults for the matching CLI flags: an
explicit `--aggressive`, `--root`, or `--library` always wins, otherwise the
config's value applies, otherwise the built-in default (`false` / no extra
roots / no extra library projects). The same `ignore` list also applies to
roots / no extra library projects). `entryPoints` globs are the file-level
counterpart to `roots`: every declaration in a matching file is treated as
an entry point, so the file and everything it references count as used —
unlike an `ignore` glob, which only drops the findings in the matching file
itself. The same `ignore` list also applies to
`roe dupes`, since a duplicate spans multiple files and doesn't map cleanly
onto a single-line inline suppression comment. Each command's section takes
an `ignore` list of its own with the same rules, unioned with the top-level
Expand Down
8 changes: 6 additions & 2 deletions docs/commands/dead-code.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,12 @@ roe dead-code --library MyLib
```

Both can also be set persistently in a [config file](/configuration). A
config's `deadCode.ignore` glob list drops every dead-code finding in
matching files without affecting the other commands — see
config's `entryPoints` glob list roots whole files rather than single
symbols — every declaration in a matching file counts as used, along with
everything it references — see
[Entry-point files](/configuration#entry-point-files). And a config's
`deadCode.ignore` glob list drops every dead-code finding in matching files
without affecting the other commands — see
[Suppressing findings](/suppressing-findings).

## What counts as an entry point
Expand Down
17 changes: 17 additions & 0 deletions docs/concepts/entry-points.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,23 @@ roe dead-code --root App.Jobs.NightlyCleanupJob
}
```

When a whole file is consumed in ways roe can't see — a plugin host loading
it by path, an external tool reading it directly — name the file with the
`entryPoints` config field instead of listing every symbol in it. Patterns
are globs resolved relative to the config file's directory (a trailing `/`
matches the whole directory); every declaration in a matching file becomes
a root, so the file and everything it references count as used:

```json roe.json
{
"entryPoints": ["Plugins/", "Jobs/**/*.cs"]
}
```

A pattern that matches no file at all is reported as a note, so a stale
path can't silently drop the protection it promised. See
[Configuration](/configuration#entry-point-files) for the full rules.

## How detection works

Under the hood, `roe dead-code` runs a five-step pipeline:
Expand Down
33 changes: 30 additions & 3 deletions docs/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ error.
{
"aggressive": true,
"roots": ["MyApp.Program.Main"],
"entryPoints": ["Jobs/", "Api/Handlers/**/*.cs"],
"libraryProjects": ["MyLib"],
"ignore": ["Migrations/**", "Generated/", "**/*.designer.cs"],
"deadCode": {
Expand Down Expand Up @@ -55,6 +56,7 @@ error.
| --- | --- | --- |
| `aggressive` | boolean | Also flag enum members and public settable auto-properties. Default for the `--aggressive` flag. |
| `roots` | string[] | Fully-qualified symbol names to treat as extra entry-point roots. Default for `--root`. |
| `entryPoints` | string[] | Glob patterns naming files to treat as entry points: every declaration in a matching file is a root, so the file and everything it references count as used. |
| `libraryProjects` | string[] | Project names to always treat in library mode (public API counts as used). Default for `--library`. |
| `ignore` | string[] | Glob patterns; every finding in a matching file is dropped. Applies to all three commands; each command's section can add its own. |
| `deadCode` | object | Extra `ignore` globs applied only to [`roe dead-code`](/commands/dead-code). |
Expand Down Expand Up @@ -110,6 +112,31 @@ passing `--baseline` is also what makes a bare `roe` and
its own. A path that doesn't exist is a hard error, not a silent
full-backlog run.

## Entry-point files

`entryPoints` is the file-level counterpart to `roots`: where `roots` names
one symbol by its fully-qualified name, an entry-point glob keeps a whole
file's contents alive. Every declaration in a matching file becomes a root,
so the file is never flagged and everything it references counts as used —
unlike an `ignore` glob, which only drops the findings in the matching file
itself.

Use it for files that are consumed in ways roe can't see: a plugin host
loading them by path, string-based reflection, or an external tool reading
the file directly.

```json roe.json
{
"entryPoints": ["Jobs/", "Api/Handlers/**/*.cs"]
}
```

Patterns resolve relative to the config file's own directory with the same
rules as `ignore` globs — a trailing `/` matches the whole directory, and
`..` is unsupported. A pattern that matches no file at all is reported as a
note in the output, so a stale path can't silently drop the protection it
promised.

## Ignore globs

`ignore` patterns are resolved relative to the config file's own directory.
Expand Down Expand Up @@ -168,9 +195,9 @@ Dupes settings follow the same per-field order: `--min-tokens 100` overrides
carries a value, so unlike `--aggressive` and `--exclude-tests` it has no
on-only nuance — `--mode exact` overrides a config's `"semantic"` cleanly.

Ignore lists sit outside this order entirely: they have no CLI flag, and a
command's own `ignore` list is unioned with the top-level one rather than
replacing it.
Ignore lists and `entryPoints` sit outside this order entirely: neither has
a CLI flag, and a command's own `ignore` list is unioned with the top-level
one rather than replacing it.

To confirm a config-only `excludeTests` or `ignore` actually applied, read the
`roe health` footer: the scanned counts narrow by whatever was excluded, and
Expand Down
18 changes: 14 additions & 4 deletions skills/roe/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,9 @@ The `kind` values (identical to the inline-suppression rule names):

Prefer fixing real findings over suppressing. When a finding genuinely is a
false positive (e.g. code invoked via string-based reflection), first
consider whether `--root`/`roots` describes the situation better — a root
documents *why* the code is alive, a suppression just silences the report.
consider whether `--root`/`roots` (or `entryPoints` for a whole file)
describes the situation better — a root documents *why* the code is alive,
a suppression just silences the report.

Inline comments work eslint-style, checked after the first `//` (so `///`
doc-comments work too):
Expand Down Expand Up @@ -188,6 +189,7 @@ fields are a hard error, so typos fail loudly.
{
"aggressive": false,
"roots": ["App.Jobs.NightlyCleanupJob"],
"entryPoints": ["Plugins/", "Jobs/**/*.cs"],
"libraryProjects": ["App.Sdk"],
"ignore": ["Migrations/**", "Generated/"],
"deadCode": {
Expand Down Expand Up @@ -228,8 +230,16 @@ Precedence is CLI flag → config → built-in default, with gotchas:
- `--root` and `--library` **replace** the config's `roots`/`libraryProjects`
lists entirely rather than merging with them.
- The `dupes` settings are plain per-field overrides (`--mode exact` beats a
config `"semantic"`), and ignore lists have no CLI flag at all — scoped
lists only ever add to the top-level one.
config `"semantic"`), and ignore lists and `entryPoints` have no CLI flag
at all — scoped ignore lists only ever add to the top-level one.

`entryPoints` is the file-level counterpart to `roots`: globs (resolved
relative to the config file's directory, trailing `/` means the whole
subtree) whose matching files are treated as entry points — every
declaration in them is a root, so the file and everything it references
count as used. Prefer it over `deadCode.ignore` for plugin/reflection-loaded
files: an ignore only hides the file's own findings, an entry point also
keeps alive what the file uses.

## Adopting roe on a legacy codebase

Expand Down
33 changes: 31 additions & 2 deletions src/commands/dead_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,27 @@ pub struct Analysis {

/// The full dead-code pipeline: discover → extract → symbol table → kill
/// list → entry points → graph → reachability → detectors → inline
/// suppressions.
/// suppressions. `entry_points` globs resolve against the workspace root;
/// the config-aware path through [`execute`] resolves them against the
/// config file's own directory instead.
pub fn analyze(
root: &Path,
aggressive: bool,
manual_roots: &[String],
library_projects: &[String],
entry_points: &[String],
) -> anyhow::Result<Analysis> {
let extracted = commands::Extracted::build(root)?;
let entry_point_dir = extracted.workspace.root.clone();

analyze_extracted(
&extracted,
extracted.started,
aggressive,
manual_roots,
library_projects,
entry_points,
&entry_point_dir,
)
}

Expand All @@ -42,6 +48,8 @@ pub(crate) fn analyze_extracted(
aggressive: bool,
manual_roots: &[String],
library_projects: &[String],
entry_points: &[String],
entry_point_dir: &Path,
) -> anyhow::Result<Analysis> {
let mut workspace = extracted.workspace.clone();
let rodeo = &extracted.rodeo;
Expand All @@ -51,7 +59,7 @@ pub(crate) fn analyze_extracted(

rules::apply_kill_list(&mut resolution, &workspace, rodeo, aggressive);

let notes = entry_points::mark_roots(
let mut notes = entry_points::mark_roots(
&mut resolution,
&workspace,
facts,
Expand All @@ -60,6 +68,14 @@ pub(crate) fn analyze_extracted(
rodeo,
);

let entry_point_globs =
config::build_entry_point_globs(entry_point_dir, entry_points, &mut workspace.warnings);
notes.extend(entry_points::mark_entry_point_files(
&mut resolution,
&workspace,
&entry_point_globs,
));

let symbol_graph = graph::build_graph(&mut resolution, &workspace, facts, rodeo);
let roots: Vec<SymbolId> = resolution
.symbols
Expand Down Expand Up @@ -109,12 +125,25 @@ pub(crate) fn execute_extracted(
&args.library_projects,
);

// `entryPoints` has no CLI flag (like `ignore`), so it comes straight
// from the config, and its globs resolve against the config file's own
// directory the way `ignore` globs do.
let (entry_points, entry_point_dir) = match context.config.as_ref() {
Some(resolved) => (
resolved.config.entry_points.clone().unwrap_or_default(),
resolved.dir.clone(),
),
None => (Vec::new(), context.root.clone()),
};

let mut analysis = analyze_extracted(
extracted,
extracted.started,
effective.aggressive,
&effective.roots,
&effective.library_projects,
&entry_points,
&entry_point_dir,
)?;
analysis
.workspace
Expand Down
Loading
Loading