ludo-technologies / ludo-technologies/polyscan
[BUG][auto] ts/local-binding-counted-as-cbo-dependency: local variables and parameters are counted as coupled classes
- Dominant language
- Go
- Stars
- 12
- Forks
- 7
- Avg merge
- 5h 46m
- Merged PRs (30d)
- 49
Description
## Summary
For JS/TS, `cbo.classes[].metrics.dependent_classes` includes **local bindings** — function parameters, local `const`/`let` declarations, destructured variables and module-private consts — whenever a method is called on them. Any `obj.method()` call records `obj` as a coupled class without checking whether `obj` is a declared local rather than an imported name.
This inflates `coupling_count` on essentially every non-trivial JS/TS file and can flip `risk_level` to `high` on a file with ordinary coupling.
## Repro
`src/dep.ts`:
```ts
export class Widget {
render(): string { return 'w' }
}
```
`src/main.ts`:
```ts
import { Widget } from './dep'
const cache = new Map()
export function run(input: { load(): string }, label: string): string {
const w = new Widget()
cache.set(label, w.render())
return input.load()
}
```
```
polyscan analyze --select cbo --format json .
```
## Expected
`main.ts` has exactly one dependency (`./dep`), so `coupling_count: 1`.
## Actual
```
file=main.ts coupling_count=5 deps=Widget,cache,dep,input,w
```
`cache` (module-level const, line 3), `input` (function parameter, line 5) and `w` (local const, line 6) are not dependencies. (`Widget` is a separate defect — see the companion issue on constructor identifiers being counted alongside their module.)
## Real-world impact
In `rayriffy/elysia-rate-limit@ecb2515`, `src/services/plugin.ts` reports:
```json
{
"name": "plugin",
"metrics": {
"coupling_count": 15,
"dependent_classes": ["@types/Options","@types/Server","DefaultContext","Elysia","app","constants","defaultContext","defaultKeyGenerator","elysia","elysiaContextKeys","logger","nextReset","options","plugin","target"]
},
"risk_level": "high"
}
```
The file has 7 import statements. Six entries are locals: `elysiaContextKeys` (module const, L14), `options` (local const, L33), `app` (parameter, L44), `plugin` (local const, L53), `target` (parameter, L69), `nextReset` (destructured, L139). Note `plugin` — the module's pseudo-class name is derived from the filename, so `plugin.ts` ends up listed as **its own dependency**.
De-inflated coupling is 7, which is below the high threshold; `risk_level: "high"` is entirely an artifact. This was the only thing keeping the project's coupling score at 85/100.
## Suspected cause
`extractAttributeAccessDependencies` in `polyscan/internal/js/analyzer/cbo.go` (~L246-270) falls through to "count the object itself (could be a class instance)" and records any non-builtin receiver identifier, with no check against declared locals, parameters or module-level consts.
## Priority
P1 — a wrong metric on a construct present in nearly every JS/TS file (calling a method on a local variable), and it changes the reported risk level.
Found via the FP-audit skill in repo `rayriffy/elysia-rate-limit@ecb2515`.
polyscan version: `polyscan version 0.4.0`
Contributor guide
Research direction
Start in polyscan/internal/js/analyzer/cbo.go, at extractAttributeAccessDependencies around lines 246-270, and reproduce the issue with the supplied src/dep.ts and src/main.ts example. Check the existing JS/TS dependency analysis and tests before changing the behavior. Done means local variables, parameters, destructured bindings, and module-private constants are absent from dependent_classes while the imported dependency remains counted.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go, javascript, typescript
- Domain
- devtools, tooling
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100