code-scent
v0.1.0
Published
A fast CLI that detects Martin Fowler-inspired code smells in JavaScript and TypeScript.
Maintainers
Readme
code-scent
code-scent is a Node CLI dev-tool for finding code smells in JavaScript and TypeScript. It feels like a test runner: point it at source paths, get file-and-line diagnostics, use the exit code in CI, or keep it running in watch mode.
The smell names and design intent follow Martin Fowler’s Refactoring: Improving the Design of Existing Code, Second Edition. Descriptions in this project are original paraphrases; the book remains the authoritative explanation. Fowler also maintains the official Refactoring catalog.
Code smells are prompts for investigation, not proof that code is wrong. Static rules are necessarily heuristic, so thresholds are configurable and individual rules can be disabled.
Quick start
Requires Node 20 or newer.
npm install --save-dev code-scent
npx code-scent srcWith npm 7+, the locally installed binary can also be run through an npm script:
{
"scripts": {
"lint:smells": "code-scent src --fail-on error"
}
}npm run lint:smellsWhile developing this repository, the same command can be run directly:
node ./bin/code-scent.js srcCommon forms:
code-scent # checks ./src
code-scent src packages/core # checks multiple source trees
code-scent src --watch # reruns after edits
code-scent . --format json # machine-readable diagnostics
code-scent src --fail-on error # warnings do not fail the process
code-scent src --rule loops=off # one-run rule override
code-scent --list-smells # all 24 catalog entriesSupported extensions are .js, .cjs, .mjs, .jsx, .ts, .cts, .mts, and .tsx. Common generated and dependency directories are skipped.
Configuration
The CLI automatically loads the first configuration file it finds in the working directory:
code-scent.config.jscode-scent.config.mjscode-scent.config.cjscode-scent.config.json
For example:
import { defineConfig } from 'code-scent/config';
export default defineConfig({
failOn: 'warning', // warning | error | never
format: 'pretty', // pretty | json
exclude: ['node_modules', 'dist', 'coverage', '.git', 'generated'],
rules: {
'long-function': { maxLines: 60, maxStatements: 35 },
'long-parameter-list': { maxParameters: 5 },
'mysterious-name': { enabled: false }
}
});See code-scent.config.example.js and src/config.js for every default threshold.
Detection coverage
The complete second-edition catalog is represented. The CLI separates smells that have useful static evidence from smells that need history or human design judgment.
| Smell | Detection | Evidence used |
| --------------------------------------------- | -------------- | ------------------------------------------------------------ |
| Mysterious Name | Heuristic | Repeated one-character or low-information declarations |
| Duplicated Code | Heuristic | Exact normalized function bodies above size thresholds |
| Long Function | Heuristic | Lines and statement count |
| Long Parameter List | Heuristic | Declared parameter count |
| Global Data | Heuristic | Module-scope let and var declarations |
| Mutable Data | Heuristic | Assignment/update density inside a function |
| Divergent Change | Manual review | Requires knowledge of responsibilities and reasons to change |
| Shotgun Surgery | Change history | Requires observing one change spread across modules |
| Feature Envy | Heuristic | Concentrated access through another object |
| Data Clumps | Heuristic | Repeated parameter groups across functions |
| Primitive Obsession | Heuristic | Large groups of primitive TypeScript parameters |
| Repeated Switches | Heuristic | Repeated case-label shapes across the project |
| Loops | Heuristic | Long imperative loops that may hide a transformation |
| Lazy Element | Manual review | Usage and design intent are needed |
| Speculative Generality | Manual review | Current and anticipated use must be understood |
| Temporary Field | Heuristic | Empty-by-default fields used by only one method |
| Message Chains | Heuristic | Member-access chain depth |
| Middle Man | Heuristic | Ratio of methods that only delegate |
| Insider Trading | Heuristic | Repeated access to private-looking foreign members |
| Large Class | Heuristic | Class lines and method count |
| Alternative Classes with Different Interfaces | Manual review | Semantic roles cannot be reliably inferred from syntax |
| Data Class | Heuristic | Fields and accessors without detected behavior |
| Refused Bequest | Heuristic | Subclass methods that reject inherited operations |
| Comments | Heuristic | Comments that resemble disabled source code |
The conservative gaps are intentional. For example, import fan-out alone does not prove Shotgun Surgery, and an unused abstraction alone does not prove Speculative Generality.
Exit codes
0: no finding at or above the configured failure threshold1: findings crossedfailOnor--max-warnings2: the CLI itself could not run, such as an invalid option or missing path
Syntax errors are reported as error findings so one bad file does not prevent other files from being analyzed.
Programmatic API
import { analyze, defaultConfig } from 'code-scent';
const result = await analyze(['src/index.ts'], {
cwd: process.cwd(),
config: defaultConfig
});
for (const finding of result.findings) {
console.log(finding.ruleId, finding.file, finding.line, finding.message);
}Development
npm install
npm test
npm run checkThe tests use Node’s built-in test runner. The only runtime dependency is Babel’s parser.
Publishing
After signing in to npm and choosing a new version, publish from a clean working tree:
npm test
npm run check
npm version patch
npm publishnpm publish is configured for public access and runs the same test and check commands through prepublishOnly. Verify the exact release contents first with npm pack --dry-run.
This project is not affiliated with or endorsed by Martin Fowler or Pearson.
