coffeelint
Version:
Lint your CoffeeScript
169 lines (134 loc) • 6.3 kB
text/coffeescript
module.exports = class Indentation
rule:
name: 'indentation'
value : 2
level : 'error'
message : 'Line contains inconsistent indentation'
description: """
This rule imposes a standard number of spaces to be used for
indentation. Since whitespace is significant in CoffeeScript, it's
critical that a project chooses a standard indentation format and
stays consistent. Other roads lead to darkness. <pre> <code>#
Enabling this option will prevent this ugly
# but otherwise valid CoffeeScript.
twoSpaces = () ->
fourSpaces = () ->
eightSpaces = () ->
'this is valid CoffeeScript'
</code>
</pre>
Two space indentation is enabled by default.
"""
tokens: ['INDENT', '[', ']', '.']
constructor: ->
@arrayTokens = [] # A stack tracking the array token pairs.
# Return an error if the given indentation token is not correct.
lintToken: (token, tokenApi) ->
[type, numIndents, { first_line: lineNumber }] = token
{ lines, lineNumber } = tokenApi
expected = tokenApi.config[@rule.name].value
# See: 'Indented chained invocations with bad indents'
# This actually checks the chained call to see if its properly indented
if type is '.'
# Keep this if statement separately, since we still need to let
# the linting pass if the '.' token is not at the beginning of
# the line
currentLine = lines[lineNumber]
if currentLine.match(/\S/i)?[0] is '.'
return @handleChain(tokenApi, expected)
return undefined
if type in ['[', ']']
@lintArray(token)
return undefined
return null if token.generated?
# HACK: CoffeeScript's lexer insert indentation in string
# interpolations that start with spaces e.g. "#{ 123 }"
# so ignore such cases. Are there other times an indentation
# could possibly follow a '+'?
previous = tokenApi.peek(-2)
isInterpIndent = previous and previous[0] is '+'
# Ignore the indentation inside of an array, so that
# we can allow things like:
# x = ["foo",
# "bar"]
previous = tokenApi.peek(-1)
isArrayIndent = @inArray() and previous?.newLine
# Ignore indents used to for formatting on multi-line expressions, so
# we can allow things like:
# a = b =
# c = d
previousSymbol = tokenApi.peek(-1)?[0]
isMultiline = previousSymbol in ['=', ',']
# Summarize the indentation conditions we'd like to ignore
ignoreIndent = isInterpIndent or isArrayIndent or isMultiline
# Correct CoffeeScript's incorrect INDENT token value when functions
# get chained. See https://github.com/jashkenas/coffeescript/issues/3137
# Also see CoffeeLint Issues: #4, #88, #128, and many more.
numIndents = @getCorrectIndent(tokenApi)
# Now check the indentation.
if not ignoreIndent and numIndents isnt expected
return { context: "Expected #{expected} got #{numIndents}" }
# Return true if the current token is inside of an array.
inArray: () ->
return @arrayTokens.length > 0
# Lint the given array token.
lintArray: (token) ->
# Track the array token pairs
if token[0] is '['
@arrayTokens.push(token)
else if token[0] is ']'
@arrayTokens.pop()
# Return null, since we're not really linting
# anything here.
null
handleChain: (tokenApi, expected) ->
lastCheck = 1
callStart = 1
prevNum = 1
{ lineNumber, lines } = tokenApi
currentLine = lines[lineNumber]
# Traverse up the token list until we see a CALL_START token.
# Don't scan above this line
findCallStart = tokenApi.peek(-callStart)
while (findCallStart and findCallStart[0] isnt 'TERMINATOR')
{ first_line: lastCheck } = findCallStart[2]
callStart += 1
findCallStart = tokenApi.peek(-callStart)
# Keep going back until we are not at a comment or a blank lines
# and set a new "previousLine"
while (lineNumber - prevNum > lastCheck) and
not /^\s*\./.test(lines[lineNumber - prevNum])
prevNum += 1
checkNum = lineNumber - prevNum
if checkNum >= 0
prevLine = lines[checkNum]
# If this is just a one-chain function, or the "corrected"
# previous line begins with a '.', check for correct
# indentation
if prevLine.match(/\S/i)[0] is '.' or checkNum is lastCheck
currentSpaces = currentLine.match(/\S/i)?.index
prevSpaces = prevLine.match(/\S/i)?.index
numIndents = currentSpaces - prevSpaces
# If both prev and current lines have uneven spacing,
# assume the current line could be lined by default
# indent spacing, and set numIndents to current
# number of spaces
prevIsIndent = prevSpaces % expected isnt 0
currIsIndent = currentSpaces % expected isnt 0
if prevIsIndent and currIsIndent
numIndents = currentSpaces
if numIndents % expected isnt 0
return { context: "Expected #{expected} got #{numIndents}" }
# Returns a corrected INDENT value if the current line is part of
# a chained call. Otherwise returns original INDENT value.
getCorrectIndent: (tokenApi) ->
{ lineNumber, lines, tokens, i } = tokenApi
curIndent = lines[lineNumber].match(/\S/)?.index
prevNum = 1
prevNum += 1 while (/^\s*(#|$)/.test(lines[lineNumber - prevNum]))
prevLine = lines[lineNumber - prevNum]
prevIndent = prevLine.match(/^(\s*)\./)?[1].length
if prevIndent > 0
return curIndent - prevLine.match(/\S/)?.index
else
return tokens[i][1]