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.js does not exist (a development checkout), it registers the CoffeeScript compiler and runs src/commandline.coffee directly, so changes take effect without a build.
  • Otherwise, it tries to resolve a coffeelint package installed in the current project and runs that copy's commandline.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 LexicalLinter calls CoffeeScript.tokens(source) and feeds each token to the rules that declared its type. While doing so, it builds tokensByLine. Some tokens produced from interpolations or multi-line regular expressions lack a line number; the linter uses the last known line for them.
  • The LineLinter then iterates over the raw lines, giving each lintLine rule 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 warn or error, the linter creates an instance with new Rule(linter, config) and keeps it if it implements that linter's method (lintAST, lintToken, or lintLine). A rule set to ignore is never instantiated, and an invalid level throws an error.
  • Normalization: a rule's return value of true becomes 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) plus rule. Findings at level ignore are 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.RULES and the internal constructor map) is module-level: registering a rule affects every later lint call in the same process.
  • The cache set with setCache is module-level too.
  • A custom compiler set via coffeelint.coffeescript replaces 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 tokensByLine to skip multi-line strings.
  • Generated tokens matter. CoffeeScript inserts implicit tokens (parentheses, braces, indentation). Rules must check the generated flag to tell written syntax from inferred syntax, which is exactly how no_implicit_parens and no_implicit_braces work.

For writing rules against these APIs, see Writing Custom Rules.