Skip to content
Open
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
6 changes: 3 additions & 3 deletions .agents/skills/deslop/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,14 @@ Avoid over-simplification that could:

Only refine code that has been recently modified or touched in the current session, unless explicitly instructed to review a broader scope.

## Consolidate Duplicates with truffler
## Consolidate Duplicates with React Doctor

Slop often hides as near-duplicate functions - the same behavior copied across modules under slightly different names. Use `truffler` (the `find-similar-functions` skill) to find symbols that do the same or similar thing, then collapse them into one shared utility instead of leaving parallel copies behind.
Slop often hides as near-duplicate functions - the same behavior copied across modules under slightly different names. Use `react-doctor find` (the `find-similar-functions` skill) to find symbols that do the same or similar thing, then collapse them into one shared utility instead of leaving parallel copies behind.

For each helper, type, or constant you touched or added, search by its name and by the behavior it implements (domain noun + verb):

```bash
bunx @rayhanadev/truffler "format duration" packages --kind function,method,constant,type --limit 20
react-doctor find "format duration" packages --kind function,method,constant,type --limit 20
```

Then consolidate deliberately:
Expand Down
33 changes: 17 additions & 16 deletions .agents/skills/find-similar-functions/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
---
name: find-similar-functions
description: Use truffler to find similar or pre-existing JavaScript/TypeScript symbols before implementing new code, especially helpers, utilities, parsers, formatters, scanners, fuzzy matchers, and other reusable functions. Agents should use this skill whenever they are about to add or refactor functionality in a JS/TS repository and need to avoid duplicating existing code, even if the user does not explicitly mention deduplication.
description: Use react-doctor find to locate similar or pre-existing JavaScript/TypeScript symbols before implementing new code, especially helpers, utilities, parsers, formatters, scanners, fuzzy matchers, and other reusable functions. Agents should use this skill whenever they are about to add or refactor functionality in a JS/TS repository and need to avoid duplicating existing code, even if the user does not explicitly mention deduplication.
---

# Similar Function Finder

Use this skill before writing new JavaScript or TypeScript code that might overlap with existing helpers. The goal is to discover nearby functions, methods, constants, types, and interfaces early enough to reuse or extend them instead of creating duplicate behavior.

`truffler` is a fuzzy symbol search tool. Treat it as a discovery layer: it points you to likely symbols, but you still need to inspect the code before deciding whether something is reusable.
`react-doctor find` is a fuzzy symbol search command. Treat it as a discovery layer: it points you to likely symbols, but you still need to inspect the code before deciding whether something is reusable.

## Workflow

Expand All @@ -17,47 +17,48 @@ Use this skill before writing new JavaScript or TypeScript code that might overl
- domain nouns, such as `symbol`, `path`, `file`, `route`, `token`, or `config`
- verbs, such as `parse`, `normalize`, `discover`, `scan`, `format`, `rank`, `score`, `resolve`, or `validate`
- common abbreviations and synonyms, such as `btn` for `button` or `cfg` for `config`
3. Run `truffler` against the narrowest useful root first, then broaden if needed.
3. Run `react-doctor find` against the narrowest useful root first, then broaden if needed.
4. Inspect the top matches with normal code-reading tools before editing.
5. Prefer reusing, extending, or moving existing code when the behavior substantially matches. Create new code only after the search shows there is not a suitable existing symbol.
6. In your final response, briefly mention what you found and whether you reused something or intentionally added a new implementation.

## Command Recipes

When `truffler` is installed in the target project:
When React Doctor is installed in the target project:

```bash
truffler "normalize path" src --kind function,method,constant,type --limit 20
truffler "score" src --kind function,method --format json --limit 15
react-doctor find "normalize path" src --kind function,method,constant,type --limit 20
react-doctor find "score" src --kind function,method --json --limit 15
```

When working inside this `truffler` repository:
When React Doctor is not installed locally and package execution is acceptable:

```bash
bun src/cli.ts "discover file" src --kind function,method,constant,type --limit 20
bun src/cli.ts "format result" src --kind function,method --format json --limit 15
npx react-doctor@latest find "discover file" src --kind function,method,constant,type --limit 20
npx react-doctor@latest find "format result" src --kind function,method --json --limit 15
```

When the project has no installed binary and you should not add dependencies, use `bunx` if network/package execution is acceptable for the environment:
When working inside the React Doctor repository, use the workspace binary after building it:

```bash
bunx @rayhanadev/truffler "parse config" src --kind function,method,type --limit 20
nr --filter react-doctor build
pnpm exec react-doctor find "parse config" src --kind function,method,type --limit 20
```

If `truffler` cannot be run, say so and fall back to the repository's available search tools. Still follow the same deduplication intent: search before implementing.
If `react-doctor find` cannot be run, say so and fall back to the repository's available search tools. Still follow the same deduplication intent: search before implementing.

## Search Strategy

Start with symbols most likely to represent reusable behavior:

```bash
truffler "<query>" <root> --kind function,method,constant,type,interface --limit 20
react-doctor find "<query>" <root> --kind function,method,constant,type,interface --limit 20
```

Use JSON output when you need structured fields for ranking, locations, signatures, or automation:

```bash
truffler "<query>" <root> --format json --limit 20
react-doctor find "<query>" <root> --json --limit 20
```

Broaden deliberately:
Expand All @@ -80,11 +81,11 @@ When no suitable symbol exists, keep the new implementation near the closest rel
Use a short note like this when the search affects your implementation:

```markdown
I checked for existing symbols with `truffler` using queries like `normalize`, `path`, and `resolve`. The closest match was `normalizePath` in `src/files.ts`, so I reused that behavior instead of adding a separate helper.
I checked for existing symbols with `react-doctor find` using queries like `normalize`, `path`, and `resolve`. The closest match was `normalizePath` in `src/files.ts`, so I reused that behavior instead of adding a separate helper.
```

If nothing relevant exists:

```markdown
I searched with `truffler` for `parse`, `config`, and `loadConfig` across `src/`; the matches were unrelated, so I added a new helper in the nearest module.
I searched with `react-doctor find` for `parse`, `config`, and `loadConfig` across `src/`; the matches were unrelated, so I added a new helper in the nearest module.
```
2 changes: 1 addition & 1 deletion .agents/skills/product-thinking/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ Then run two filters before you commit to building:
Reuse beats adding every time — the cheapest surface is the one you never create. Before adding a flag, option, or report field, search for an existing one to extend:

```bash
bunx @rayhanadev/truffler "<name or behavior>" packages --kind function,interface,type,constant --limit 20
react-doctor find "<name or behavior>" packages --kind function,interface,type,constant --limit 20
rg -n "<flag-or-option-name>" packages/react-doctor/src/cli packages/core/src/types
```

Expand Down
4 changes: 2 additions & 2 deletions .agents/skills/rule-research/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ If the user requested implementation, make the contract concise and continue.
1. Define the rule in one sentence: `This rule catches <pattern> that causes <problem>.`
2. Explain the runtime reason.
3. Inspect nearby rules, tests, utilities, and the generated registry.
4. Use `truffler` before proposing a new detector or helper:
4. Use `react-doctor find` before proposing a new detector or helper:

```sh
bunx @rayhanadev/truffler "<symbol-or-behavior>" \
react-doctor find "<symbol-or-behavior>" \
packages/oxlint-plugin-react-doctor/src/plugin \
--kind function,interface,type,constant --limit 20
```
Expand Down
4 changes: 2 additions & 2 deletions .agents/skills/rule-validate/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ Check for:
- messages that overstate detection
- missing valid and invalid tests

Use `truffler` before accepting a new helper:
Use `react-doctor find` before accepting a new helper:

```sh
bunx @rayhanadev/truffler "<helper-name>" packages \
react-doctor find "<helper-name>" packages \
--kind function,method,interface,type,constant --limit 20
```

Expand Down
2 changes: 1 addition & 1 deletion .agents/skills/rule-writing/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ State the diagnostic condition, required syntax and bindings, unsupported cases,
Before adding a helper, search for one to reuse:

```sh
bunx @rayhanadev/truffler "<symbol-or-behavior>" \
react-doctor find "<symbol-or-behavior>" \
packages/oxlint-plugin-react-doctor/src/plugin \
--kind function,interface,type,constant --limit 20
```
Expand Down
2 changes: 1 addition & 1 deletion .agents/skills/ship/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Read `AGENTS.md` first (its rules change), then run `/review` on this branch's d

## 2. Deslop

Run `/deslop` ([`../deslop/SKILL.md`](../deslop/SKILL.md)) to simplify the recently modified code while preserving functionality, including its `truffler` duplicate-consolidation pass. Apply the refinements before committing.
Run `/deslop` ([`../deslop/SKILL.md`](../deslop/SKILL.md)) to simplify the recently modified code while preserving functionality, including its `react-doctor find` duplicate-consolidation pass. Apply the refinements before committing.

## 3. Commit and push

Expand Down
5 changes: 5 additions & 0 deletions .changeset/tall-mammals-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"react-doctor": patch
---

Add `react-doctor find` with `search` and `grep` aliases for fuzzy JavaScript and TypeScript symbol lookup, including React component, HOC, styled-component, and hook kinds.
13 changes: 6 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,16 @@
- MUST: Frequently re-evaluate and refactor variable names to be more accurate and descriptive.
- MUST: Do not type cast ("as") unless absolutely necessary
- MUST: Remove unused code and don't repeat yourself.
- MUST: Use `truffler` to find existing symbols before adding a utility, helper, type, or rule, and again after finishing a task to catch duplicates and dead code (see "Symbol Search & Deduplication").
- MUST: Use `react-doctor find` to find existing symbols before adding a utility, helper, type, or rule, and again after finishing a task to catch duplicates and dead code (see "Symbol Search & Deduplication").
- MUST: Always search the codebase, think of many solutions, then implement the most _elegant_ solution.
- MUST: Before adding or changing the **public surface** (CLI flags/commands, the score, config, the JSON report, package APIs, the GitHub Action, website, or terminal output), run the `product-thinking` pass (`.agents/skills/product-thinking/`): name the user's job, reuse before adding, wire one telemetry metric, add the compatibility artifacts, and set a kill metric. Lint rules use the rule pipeline instead.
- MUST: Put all magic numbers in `constants.ts` using `SCREAMING_SNAKE_CASE` with unit suffixes (`_MS`, `_PX`).
- MUST: Put small, focused utility functions in `utils/` with one utility per file.
- MUST: Use Boolean over !!.

## Symbol Search & Deduplication (truffler)
## Symbol Search & Deduplication

`@rayhanadev/truffler` (dev dependency) is fuzzy JS/TS symbol search powered by `oxc-parser`.
`react-doctor find` is fuzzy JS/TS symbol search powered by `oxc-parser`.
Use it to avoid duplicating existing code. The `find-similar-functions` skill
(`.agents/skills/find-similar-functions/`) carries the full workflow; the short version:

Expand All @@ -35,12 +35,11 @@ Use it to avoid duplicating existing code. The `find-similar-functions` skill
duplicate an existing helper, and delete any code your change superseded.

```bash
bunx @rayhanadev/truffler "<query>" packages --kind function,method,interface,type,constant --limit 20
react-doctor find "<query>" packages --kind function,method,interface,type,constant --limit 20
```

Run it with `bunx @rayhanadev/truffler` (the published `bin` is a TypeScript entry Bun runs
directly, and the pinned dev dependency is reused rather than re-downloaded). Narrow `<query>`
and the root (e.g. `packages/core/src`) for precision; broaden only when nothing matches.
Narrow `<query>` and the root (e.g. `packages/core/src`) for precision; broaden only when
nothing matches. Use `--json` when structured results are easier to inspect.

## Package Layout

Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@
"devDependencies": {
"@changesets/changelog-github": "^0.7.0",
"@changesets/cli": "^2.31.0",
"@rayhanadev/truffler": "^0.4.2",
"@sentry/cli": "^3.4.3",
"@types/node": "^25.6.0",
"@voidzero-dev/vite-plus-core": "^0.1.15",
Expand Down
12 changes: 12 additions & 0 deletions packages/react-doctor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ npx react-doctor@latest install

Works with Claude Code, Cursor, Codex, OpenCode, and many more.

### Find code symbols

Fuzzy-search JavaScript and TypeScript declarations without falling back to text grep. Results include file locations, symbol kinds, and signatures.

```bash
npx react-doctor@latest find Button src
npx react-doctor@latest find use --kind hook
npx react-doctor@latest find card --kind component,interface --json
```

`find` is also available as `search` and `grep`. The generic symbol kinds include functions, classes, interfaces, types, methods, properties, variables, imports, and exports. React-aware `component` and `hook` kinds follow React naming conventions.

### 3. Run in CI

React Doctor reviews every pull request and reports only the issues your change introduced, not your existing backlog. Set it up with one command:
Expand Down
2 changes: 2 additions & 0 deletions packages/react-doctor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"dependencies": {
"@astrojs/compiler": "^4.0.0",
"@babel/code-frame": "^7.29.0",
"@rayhanadev/truffler": "^0.4.2",
"@sentry/node": "^10.54.0",
"agent-install": "0.0.5",
"conf": "^15.1.0",
Expand All @@ -67,6 +68,7 @@
"figures": "^6.1.0",
"jiti": "^2.7.0",
"magicast": "^0.5.3",
"oxc-parser": "^0.132.0",
"oxc-resolver": "^11.24.2",
"oxlint": ">=1.77.0 <1.78.0",
"oxlint-plugin-react-doctor": "workspace:*",
Expand Down
139 changes: 139 additions & 0 deletions packages/react-doctor/src/cli/commands/find.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { stat } from "node:fs/promises";
import * as path from "node:path";
import { isErrnoException } from "@react-doctor/core";
import { searchSymbols } from "@rayhanadev/truffler";
import type { SymbolSearchResult } from "@rayhanadev/truffler";
import { CliInputError } from "../utils/cli-input-error.js";
import { DEFAULT_FIND_LIMIT, DEFAULT_SOURCE_POSITION, METRIC } from "../utils/constants.js";
import { formatFindPath } from "../utils/format-find-path.js";
import { formatFindSymbolResult } from "../utils/format-find-symbol-result.js";
import { parseFindKinds } from "../utils/parse-find-kinds.js";
import { recordCount } from "../utils/record-metric.js";
import { resolveFindSymbolKind } from "../utils/resolve-find-symbol-kind.js";

export interface FindOptions {
readonly cwd?: string;
readonly json?: boolean;
readonly kind?: string;
readonly limit?: number | string;
}

interface FindJsonResult {
readonly name: string;
readonly kind: string;
readonly symbolKind: SymbolSearchResult["kind"];
readonly location: FindJsonLocation;
readonly container?: string;
readonly signature?: string;
readonly parameters?: SymbolSearchResult["parameters"];
readonly returnType?: string;
readonly score: number;
readonly matches: ReadonlyArray<number>;
}

interface FindJsonOutput {
readonly query: string;
readonly root: string;
readonly count: number;
readonly results: ReadonlyArray<FindJsonResult>;
}

interface FindJsonLocation {
readonly file: string;
readonly line: number;
readonly column: number;
}

const parseFindLimit = (value: number | string | undefined): number => {
if (value === undefined) return DEFAULT_FIND_LIMIT;
if (typeof value === "string" && !/^\d+$/.test(value)) {
throw new CliInputError(`Invalid limit "${value}". Expected a non-negative integer.`);
}
const parsedLimit = typeof value === "number" ? value : Number(value);
if (!Number.isSafeInteger(parsedLimit) || parsedLimit < 0) {
throw new CliInputError(`Invalid limit "${value}". Expected a non-negative integer.`);
}
return parsedLimit;
};

const isRequestedFindKind = (
result: SymbolSearchResult,
requestedKinds: ReadonlySet<string>,
): boolean => requestedKinds.has(result.kind) || requestedKinds.has(resolveFindSymbolKind(result));

const buildFindJsonOutput = (
query: string,
root: string,
cwd: string,
results: ReadonlyArray<SymbolSearchResult>,
): FindJsonOutput => ({
query,
root,
count: results.length,
results: results.map((result) => ({
name: result.name,
kind: resolveFindSymbolKind(result),
symbolKind: result.kind,
location: {
file: formatFindPath(result.file, cwd),
line: result.line ?? DEFAULT_SOURCE_POSITION,
column: result.column ?? DEFAULT_SOURCE_POSITION,
},
container: result.container,
signature: result.signature,
parameters: result.parameters,
returnType: result.returnType,
score: result.score,
matches: result.matches,
})),
});

export const findAction = async (
query: string,
directory = ".",
options: FindOptions = {},
): Promise<void> => {
recordCount(METRIC.cliInvoked, 1, { command: "find" });
const displayQuery = query.trim();
const searchQuery = displayQuery.replace(/[^\p{L}\p{N}_$]+/gu, "");
if (searchQuery.length === 0) throw new CliInputError("Search query cannot be empty.");

const cwd = path.resolve(options.cwd ?? process.cwd());
const rootPath = path.resolve(cwd, directory);
try {
await stat(rootPath);
} catch (error) {
if (isErrnoException(error) && error.code === "ENOENT") {
throw new CliInputError(`Search path "${directory}" does not exist.`);
}
throw error;
}
const limit = parseFindLimit(options.limit);
const parsedKinds = parseFindKinds(options.kind);
const results = await searchSymbols(searchQuery, {
root: directory,
cwd,
symbolKinds: parsedKinds.symbolKinds,
ignoreParseErrors: true,
onParseError: (error) => {
process.stderr.write(
`Warning: skipped ${formatFindPath(error.file, cwd)}: ${error.message}\n`,
);
},
});
const filteredResults = results
.filter((result) => isRequestedFindKind(result, parsedKinds.requestedKinds))
.slice(0, limit);

if (options.json) {
process.stdout.write(
`${JSON.stringify(buildFindJsonOutput(displayQuery, directory, cwd, filteredResults), null, 2)}\n`,
);
return;
}

if (filteredResults.length === 0) return;
process.stdout.write(
`${filteredResults.map((result) => formatFindSymbolResult(result, cwd)).join("\n")}\n`,
);
};
Loading
Loading