Recently I wrote about maintaining code consistency on growing teams using CoffeeLint. If you use CoffeeLint across many projects or with a large enough team, sooner or later you’ll come across a situation that isn’t covered by an out of the box rule.
CoffeeLint provides a plugin system that makes authoring your own linter easy. The implementation guidelines are outlined in Building Custom Rules. There are three different types of rules that can be implemented depending on your needs: LineLinter, TokenLinter, and ASTLinter.
I’m going to write a TokenLinter that enforces whitespace conventions when invoking a function. To pass this check a function call must:
Note: The points about opening and closing parentheses don’t apply if they have been omitted when calling the function.
# Valid style fn(2, 5) fn 2, 5 fn(2, 5) if x > 10 fn 2, 5 if x > 10 fn 2, someOption: true # Invalid style fn( 2, 5) fn(2, 5 ) fn( 2, 5 ) fn(2,5)
Rather than write a plugin from the ground up, I searched through CoffeeLint’s base rules and found one similar to what I wanted.
The main difference is that I wanted to check both
CALL_END tokens to make sure they were not surrounded by a space. In addition, I would need to test tokens in between to make sure arguments to the function were spaced appropriately.
# ... tokens: ["CALL_START", "CALL_END"] lintToken: (token, tokenApi) -> tokenType = token if tokenType == "CALL_START" isOpeningParenError = true if token.spaced isArgumentError = checkArgumentSpacing(tokenApi) else if tokenType == "CALL_END" isClosingParenError = checkCloseParenSpacing(token, tokenApi) isOpeningParenError || isArgumentError || isClosingParenError # ...
lintToken is the crux of a TokenLinter plugin. In our case it’s called any time a
CALL_END token is encountered. Stepping through it we see that a linting error is triggered if any of the following are true:
The opening parenthesis case is easy to handle since CoffeeScript’s compiler gives us a property
spaced if the token has a space after it.
isOpeningParenError = true if token.spaced
Next, starting at
CALL_START, we use
checkArgumentSpacing to loop over the arguments by peeking forward until we reach
CALL_END. At each step we need to check a few things:
isArgumentIncorrectlySpaced = (token) -> token == "," && !(token.spaced || token.newLine) checkArgumentSpacing = (tokenApi) -> i = 1 insideFunctionCall = true while insideFunctionCall nextToken = tokenApi.peek(i) i += 1 isLintingError = true if isArgumentIncorrectlySpaced(nextToken) insideFunctionCall = nextToken != "CALL_END" isLintingError
This one is trickier than the opening case because you can’t rely on the
spaced property since it only applies to spacing after the given token. You can look at the previous token’s spacing property, but this breaks down when a function is invoked and then followed by a postfix expression. In this situation the previous token will have a space after it and look like a violation of the rule. As a workaround, I check that the current token (
CALL_END) and the previous token are in the same position. That fact that these tokens are reported in the same position seems to be a CoffeeScript compiler quirk, but is reliable enough for our needs.
checkCloseParenSpacing = (token, tokenApi) -> previousToken = tokenApi.peek(-1) if previousToken.spaced # generated means that CoffeeScript is adding parentheses for us # and that the author has omitted them if token.generated unless token.last_column == previousToken.last_column isLintingError = true else isLintingError = true isLintingError
Writing one of these plugins doesn’t take much time and it’ll save you the headache of fighting with developers with strong opposing opinions. Now go out and write all the plugins to bend the code style of your organization to your will!