coffeescript-cognitive-complexity
v1.0.0
Published
Native Cognitive Complexity (SonarSource S3776) analyzer for CoffeeScript — measures the source AST, not compiled JS.
Maintainers
Readme
coffeescript-cognitive-complexity
Cognitive Complexity (SonarSource / G. Ann Campbell, rule S3776) for CoffeeScript, measured natively on the CoffeeScript source AST — not by compiling to JS first.
Cognitive Complexity estimates how hard code is to understand (unlike Cyclomatic
Complexity, which measures testability). Three rules: ignore readable shorthand;
+1 for each break in linear flow (if/loop/switch/catch/logical-operator
sequences/recursion); +1 more for each level such a structure is nested.
npm install --save-dev coffeescript-cognitive-complexity# CLI (a directory is walked recursively for *.coffee):
npx coffee-cc --top=20 src/
npx coffee-cc --explain path/to/File.coffee
npx coffee-cc --by-class src/// API:
const { analyzeSource } = require('coffeescript-cognitive-complexity');
const r = analyzeSource(coffeeSource, 'File.coffee');
// → { file, functions:[{name,klass,line,complexity,increments}], fileTop, total, classes }Why native, not compile-to-JS?
There is no other cognitive-complexity tool for CoffeeScript — coffeelint and
escomplex-coffee only do cyclomatic complexity (decision counting, no nesting
penalty), and every real cognitive-complexity implementation (eslint-plugin-sonarjs,
cognitive-complexity-ts, SonarQube…) targets JS/TS/Python and can't parse .coffee.
You can compile .coffee to JS and score that — but it skews upward and loses
source line attribution, because CoffeeScript's readable shorthands expand into
exactly the control-flow structures the metric penalizes. Same snippets, this tool
(native) vs. the canonical eslint-plugin-sonarjs and ccts on the compiled JS:
native | sonarjs | ccts | case
-------|---------|-------|------------------------------------------------
0 | 6 | 6 | a?.b?.c?.d soak chain → nested null-guards
0 | 1 | 1 | x = a ? b existential → a != null ? a : b
0 | 1 | 1 | @x ?= def() existential assign → if (x == null)
3 | 8 | 8 | (u?.name for u in xs when u?.active) sugar on sugar
6 | 7 | 7 | plain control flow implicit-return `for` injects an else
1 | 1 | 1 | unless / switch / chained ternary … (no sugar → all agree)The two compiled-JS tools agree with each other everywhere and with native
wherever there's no CoffeeScript sugar — which is what shows the divergences are
genuine sugar-skew (the spec says to ignore null-coalescing), not tool error.
Reproduce with examples/skew-comparison.js (needs eslint, eslint-plugin-sonarjs,
cognitive-complexity-ts installed alongside).
Granularity: method → class → file
Cognitive Complexity is a per-method metric — that is the actionable unit (which function to refactor; SonarQube gates each method at 15 by default). Because it charges no "cost of entry" per method (an empty method scores 0, not 1), summing methods up to the class or file level is meaningful: a high class total distinguishes a god-class with tangled control flow from a large-but-simple class of getters/setters — something a cyclomatic total (which just tracks method count / LOC) cannot do.
- default — every function/method ranked by its own complexity
--explain— the per-increment breakdown within a method--by-class/--by-file— the sums--json— all of the above, machine-readable
CLI options: --explain, --json, --top=N (0 = all), --threshold=N,
--by-file, --by-class, --sort=complexity|name|line,
--comprehensions=loop|shorthand|off.
What it counts
Following S3776 (Appendix B), against the CoffeeScript AST:
| Construct | Increment |
|---|---|
| if / ternary (value-position if…then…else) | +1, + nesting |
| else / else if | +1 (flat) |
| switch (whole statement, once) | +1, + nesting |
| for / comprehension, while / until / loop | +1, + nesting |
| catch | +1, + nesting |
| sequence of like and/or (&&/||) operators | +1 per run (flat) |
| direct recursion | +1 (flat) |
| nested function / lambda | no increment, but +1 nesting for its body |
Top-level methods get no cost of entry; nesting resets to 0 in each.
CoffeeScript-specific decisions
- Null-coalescing is ignored (the spec's Rule 1): the existential operator
a ? b, soaksa?.b, and?=/or=/and=compound assigns contribute 0. This is the main fidelity win over scoring compiled JS, where they become countedif/&&guards. unlessand postfixif/while/untilare the same AST nodes as their prefix forms and count normally.- Ternary vs. statement-
ifis decided the way the CoffeeScript compiler decidesa?b:cvs.if(){}else{}: anif…then…elseis a ternary (a single increment, no separateelse) only when its value is consumed in expression position and both branches are single expressions; a function-tail or standaloneif/elseis a statement (if+1,else/else if+1 each). Verified againstCoffeeScript.compileoutput. A nested ternary in an alternate position does receive a nesting increment (spec B3 lists "ternary operator"; it excludes "else if") — confirmed againsteslint-plugin-sonarjs. - Comprehensions —
--comprehensions:loop(default): a comprehension is a loop → +1 (+nesting); itswhenguard → +1 more.shorthand: like.filter().map()— the iteration is free; only real control flow / logical sequences inside still count.off: comprehensions contribute nothing.
- Bare
super(CoffeeScript-1 argument-forwarding form, which vanilla CoffeeScript 2 rejects) is rewritten tosuper()before parsing — a complexity-neutral shim, on by default (superShimoption), a no-op on code without it. - Labeled jumps (
goto/break LABEL), a spec increment category, don't exist in CoffeeScript, so that category never applies.
Validation
npm test runs 23 cases, including the SonarSource white paper's own worked examples
translated to CoffeeScript, reproduced exactly: getWords (switch) = 1, nested
try/if/for/while = 9, nested lambda = 2 — plus compiler-grounded ternary cases,
logical-operator sequences, existential/soak = 0, recursion, comprehension policies,
and else if vs. nested-if-in-else. The tool also agrees with the canonical
eslint-plugin-sonarjs reference on every snippet containing no CoffeeScript sugar.
Limitations
- Direct recursion only — mutual/indirect recursion isn't detected (needs a whole-program call graph).
- Parses whole-file with the installed CoffeeScript 2.x (+ the bare-
supershim). The control-flow AST is stable across 2.x, so the score is unaffected. - Comprehension weighting is a documented judgment call (
--comprehensions); the default (loop) is the most spec-literal.
API
const { analyzeSource, Analyzer, DEFAULT_OPTIONS } = require('coffeescript-cognitive-complexity');
analyzeSource(src, filename, options)
// options: { comprehensions: 'loop'|'shorthand'|'off', superShim: true }
// → { file, functions, fileTop, total, classes } | { file, error }
// Each function: { name, klass, line, complexity, increments:[{delta,line,reason,nesting}] }License
MIT
