Architecture
This page describes how CoffeeLint is organized internally and what happens, step by step, when a file is linted. It is aimed at rule authors, integrators, and anyone debugging surprising results.
Source Layout
| Path | Responsibility |
|---|---|
src/coffeelint.coffee |
Core library: rule registry, configuration merging, the lint pipeline, public API. |
src/base_linter.coffee |
Shared linter logic: rule instantiation, error creation, result normalization. |
src/ast_linter.coffee |
Parses the source into an AST and runs lintAST rules; reports syntax errors. |
src/lexical_linter.coffee |
Tokenizes the source and runs lintToken rules. |
src/line_linter.coffee |
Runs lintLine rules and collects inline directives. |
src/rules.coffee |
The default configuration of coffeescript_error. |
src/rules/*.coffee |
One file per built-in rule. |
src/error_report.coffee |
Multi-file results, summaries, exit code. |
src/commandline.coffee |
The coffeelint command: options, file discovery, ignore file, config loading, reporters. |
src/configfinder.coffee |
Configuration discovery, extends, and module path resolution. |
src/ruleLoader.coffee |
Loading rule, transform, and compiler modules. |
src/cache.coffee |
The file-based result cache. |
src/reporters/*.coffee |
Output formats. |
bin/coffeelint |
The executable entry point. |
The build (cake compile) produces lib/: lib/coffeelint.js is a Browserify standalone bundle of the core, while the command-line modules, configfinder, cache, ruleLoader, and the reporters are compiled individually.
The Executable
bin/coffeelint decides which code to run:
- If
lib/commandline.jsdoes not exist (a development checkout), it registers the CoffeeScript compiler and runssrc/commandline.coffeedirectly, so changes take effect without a build. - Otherwise, it tries to resolve a
coffeelintpackage installed in the current project and runs that copy'scommandline.js, falling back to its own. This is why a global install defers to a project-local version.
The lint Pipeline
source, config, literate
│
├─ cache lookup (if a cache is set)
├─ merge config with rule defaults; load rules that have a "module"
├─ literate? → convert Markdown to comments, un-indent code
├─ run transforms; warn if the line count changed
├─ swap compiler (coffeelint.coffeescript)
├─ pre-scan for "enable" directives on ignored rules
├─ AST pass ── syntax error? → coffeescript_error only
├─ lexical pass (token rules)
├─ line pass (line rules + directive collection)
├─ sort findings by line number
├─ apply inline directives line by line
└─ store in cache → return findings
1. Configuration Merge
For each registered rule, the user's settings are laid over the rule's defaults with a shallow merge. Rules with a module key are loaded and registered first, so they take part in the merge. Unknown rule names are ignored. See Configuration.
2. Preprocessing
Literate sources are converted, then transforms run. If the line count changed, a transform_messes_up_line_numbers finding is queued. See Transforms & Flavors.
3. Directive Pre-Scan
CoffeeLint scans every line for enable and enable-line directives that name a single rule currently set to ignore. Such rules are switched to error for this run, but marked as initially disabled so they only report from the directive onward.
4. AST Pass
The ASTLinter calls CoffeeScript.nodes(source). If parsing fails, the compiler's error is converted into a coffeescript_error finding (with the line number taken from the error's location, or parsed from its message) and the pass ends. Otherwise each lintAST rule walks the tree and pushes findings into a shared array.
5. Lexical and Line Passes
These passes only run if the source tokenizes without errors.
- The
LexicalLintercallsCoffeeScript.tokens(source)and feeds each token to the rules that declared its type. While doing so, it buildstokensByLine. Some tokens produced from interpolations or multi-line regular expressions lack a line number; the linter uses the last known line for them. - The
LineLinterthen iterates over the raw lines, giving eachlintLinerule the line plus the tokens collected for it, maintaining class context, and recording inline directives.
6. Directive Application
All findings are sorted by line number. CoffeeLint then walks the file line by line, tracking two sets of disabled rules: one for the rest of the file (disable/enable) and one for the current line only (disable-line/enable-line). A finding is kept only if its rule is not disabled on its line. A bare disable disables every rule that produced findings; a bare enable restores the initial state.
7. Result
The final array is stored in the cache (if any) and returned. The command line wraps this in an ErrorReport, one entry per file, and hands it to the reporter.
How Rules Are Run
BaseLinter is shared by the three linters:
- Instantiation: for every registered rule whose configured level is
warnorerror, the linter creates an instance withnew Rule(linter, config)and keeps it if it implements that linter's method (lintAST,lintToken, orlintLine). A rule set toignoreis never instantiated, and an invalid level throws an error. - Normalization: a rule's return value of
truebecomes a finding with the rule's defaults; an object is merged into the finding; anything else means no finding. - Error creation: a finding is the rule's merged configuration plus the finding's attributes (
lineNumber,context, and so on) plusrule. Findings at levelignoreare discarded.
Rule instances live for the duration of one pass over one file, so they can keep state between tokens or lines. They are recreated for every file.
Global State
- The rule registry (
coffeelint.RULESand the internal constructor map) is module-level: registering a rule affects every laterlintcall in the same process. - The cache set with
setCacheis module-level too. - A custom compiler set via
coffeelint.coffeescriptreplaces the module-level compiler reference.
Tools that lint many projects in one long-running process (editor servers, for example) should keep this in mind.
Design Consequences
- Syntax errors short-circuit. Because token and line rules cannot run on invalid code, one syntax error hides all other findings in that file.
- Three views of the code. Rules choose the representation that fits: tokens for spacing and operators, lines for length and whitespace, AST for structure. Rules can combine views, for example line rules using
tokensByLineto skip multi-line strings. - Generated tokens matter. CoffeeScript inserts implicit tokens (parentheses, braces, indentation). Rules must check the
generatedflag to tell written syntax from inferred syntax, which is exactly howno_implicit_parensandno_implicit_braceswork.
For writing rules against these APIs, see Writing Custom Rules.