Transforms, CoffeeScript Flavors, and Literate CoffeeScript

CoffeeLint normally lints plain CoffeeScript with the bundled CoffeeScript 2 compiler. Three features change what it sees: transforms that rewrite the source before linting, a custom compiler for alternative CoffeeScript dialects, and Literate CoffeeScript support. This page explains how each works and the trade-offs involved.

Transforms

Transforms were added in CoffeeLint 1.8, primarily so that projects using JSX-flavored CoffeeScript (for React) could be linted. A transform is a function that takes the source text and returns new source text. CoffeeLint runs all configured transforms, in order, before any rule sees the code.

Configuring Transforms

Transforms are listed in the special coffeelint section of your configuration:

{
  "coffeelint": {
    "transforms": ["coffee-react-transform"]
  }
}

Each entry is a module name that is loaded with require. Install the module in your project:

npm install --save-dev coffee-react-transform

Writing a Transform

A transform module exports a single function:

# replace-build-placeholders.coffee
# Turns build-time placeholders such as <%= VERSION %> into string
# literals, so the file parses, without changing the number of lines.
module.exports = (source) ->
  source.replace /<%=\s*(\w+)\s*%>/g, (match, name) -> "'#{name}'"

Rules:

  • The function receives the whole file as one string and must return a string.
  • It should keep every line where it was. Never add or remove lines if you can avoid it.
  • It should be fast and side-effect free, since it runs for every linted file.

How Transform Modules Are Resolved

  • Discovered configuration (no -f): each name is resolved to a full path relative to the directory of the file being linted, using Node's module resolution and the extensions .js, .coffee, .litcoffee, and .coffee.md. Package names work well because resolution walks up to your project's node_modules. Relative paths such as ./tools/transform.coffee, however, are interpreted relative to each linted file's directory, so they only work for files in one folder.
  • Explicit configuration (-f) and the API: names are resolved when linting, from the current directory first, then with a plain require, then as a path relative to the current directory.

For reliable results, publish shared transforms as packages (or reference them with absolute paths).

Caveats

  • Failures are silent. If a transform module cannot be loaded or throws, CoffeeLint skips it without any message and lints the source as it was before that transform. If a transform seems to have no effect, test that it loads with node -p "require('module-name')".
  • Transformed code is what gets linted. CoffeeLint cannot control what transforms produce, and the output may violate your rules. Use inline directives such as # coffeelint: disable=max_line_length where needed.
  • Line numbers may shift. If a transform changes the number of lines, CoffeeLint emits the transform_messes_up_line_numbers warning, because every reported line number in that file is probably wrong. The check only compares totals, so a transform that adds a line in one place and removes one elsewhere goes unnoticed. If line numbers are off, contact the transform's maintainers about preserving lines.
  • Not available in the browser. Transforms are loaded with Node's module system and are skipped in the browser build.

Custom CoffeeScript Compilers (Flavors)

CoffeeLint 1.8 also added a way to replace the bundled compiler with another module, for dialects such as IcedCoffeeScript:

{
  "coffeelint": {
    "coffeescript": "iced-coffee-script"
  }
}

The module is resolved like transforms. It replaces the compiler used to tokenize and parse your code, so token-based and AST-based rules operate on the dialect's output.

The project's own documentation calls this functionality "basically unsupported": if a flavor breaks things, it is up to you to work with that flavor's maintainers to align it with the official CoffeeScript. Known issue: IcedCoffeeScript breaks the cyclomatic_complexity rule.

Two implementation details of version 2.1.0 to be aware of when using this option:

  • The replacement compiler is stored in a module-level variable. Once a file configured with a custom compiler has been linted, that compiler stays in effect for every later file linted in the same process.
  • In the source, the compiler module is loaded through a helper that is only initialized while loading transforms. If configuring coffeescript on its own fails with a TypeError about reading require of undefined, that is the cause; configuring at least one transform alongside it avoids the problem.

Literate CoffeeScript

Literate CoffeeScript files (.litcoffee, .coffee.md) are Markdown documents whose indented blocks are code. CoffeeLint handles them by converting them into regular CoffeeScript before linting:

  1. CoffeeScript's own helper turns the Markdown prose into # comment lines.
  2. CoffeeLint then removes the first four spaces (or one tab) from every line, so code returns to the indentation it would have in a normal .coffee file.
  3. All rules run on the result, so line numbers still match the original file.

Special handling:

  • max_line_length subtracts the two characters of the # prefix added to prose lines, so prose is measured at its real length.
  • Line rules can call lineApi.isLiterate() to adapt their behavior. See Writing Custom Rules.

How files are recognized:

  • Files passed explicitly are treated as literate based on their extension.
  • Directories only yield *.coffee files by default; add --ext litcoffee to include Literate CoffeeScript.
  • For standard input, pass --literate.
coffeelint --ext litcoffee src docs
cat guide.litcoffee | coffeelint --stdin --literate

From the API, pass true as the third argument to coffeelint.lint(source, config, literate).