@coffeelint/cli
Version:
Lint your CoffeeScript
274 lines (249 loc) • 10.4 kB
JavaScript
(function() {
var Indentation,
indexOf = [].indexOf;
module.exports = Indentation = (function() {
class Indentation {
constructor() {
this.arrayTokens = []; // A stack tracking the array token pairs.
}
// Return an error if the given indentation token is not correct.
lintToken(token, tokenApi) {
var chain, currentLine, dotIndent, expected, got, ignoreIndent, isArrayIndent, isMultiline, lineNumber, lines, next, numIndents, previous, previousSymbol, ref, ref1, regExRes, spaces, startsWith, type;
[type, numIndents] = token;
({
first_column: dotIndent
} = token[2]);
({lines, lineNumber} = tokenApi);
expected = tokenApi.config[this.rule.name].value;
// See: 'Indented chained invocations with bad indents'
// This actually checks the chained call to see if its properly indented
if (type === '.') {
// 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 (((ref = currentLine.match(/\S/)) != null ? ref[0] : void 0) === '.') {
next = tokenApi.peek(1);
if (next[0] === 'PROPERTY') {
chain = '.' + next[1];
startsWith = new RegExp('^(\\s*)(\\' + chain + ')');
regExRes = currentLine.match(startsWith);
spaces = (regExRes != null ? regExRes[1].length : void 0) || -1;
if ((regExRes != null ? regExRes.index : void 0) === 0 && spaces === dotIndent) {
got = dotIndent;
if (dotIndent - expected > expected) {
got %= expected;
}
if (dotIndent % expected !== 0) {
return {
token,
context: `Expected ${expected} got ${got}`
};
}
}
}
}
return void 0;
}
if (type === '[' || type === ']') {
this.lintArray(token);
return void 0;
}
if ((token.generated != null) || (token.explicit != null)) {
return null;
}
// Ignore the indentation inside of an array, so that
// we can allow things like:
// x = ["foo",
// "bar"]
previous = tokenApi.peek(-1);
isArrayIndent = this.inArray() && (previous != null ? previous.newLine : void 0);
// Ignore indents used to for formatting on multi-line expressions, so
// we can allow things like:
// a = b =
// c = d
previousSymbol = (ref1 = tokenApi.peek(-1)) != null ? ref1[0] : void 0;
isMultiline = previousSymbol === '=' || previousSymbol === ',';
// Summarize the indentation conditions we'd like to ignore
ignoreIndent = isArrayIndent || 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 = this.getCorrectIndent(tokenApi);
// Now check the indentation.
if (!ignoreIndent && !(indexOf.call(numIndents, expected) >= 0)) {
return {
token,
context: `Expected ${expected} got ${numIndents[0]}`
};
}
}
// Return true if the current token is inside of an array.
inArray() {
return this.arrayTokens.length > 0;
}
// Lint the given array token.
lintArray(token) {
// Track the array token pairs
if (token[0] === '[') {
this.arrayTokens.push(token);
} else if (token[0] === ']') {
this.arrayTokens.pop();
}
// Return null, since we're not really linting
// anything here.
return null;
}
grabLineTokens(tokenApi, lineNumber, all = false) {
var i, k, len, len1, ref, ref1, results, results1, tok, tokensByLine;
({tokensByLine} = tokenApi);
if (lineNumber < 0) {
lineNumber = 0;
}
while (!((tokensByLine[lineNumber] != null) || lineNumber === 0)) {
lineNumber--;
}
if (all) {
ref = tokensByLine[lineNumber];
results = [];
for (i = 0, len = ref.length; i < len; i++) {
tok = ref[i];
results.push(tok);
}
return results;
} else {
ref1 = tokensByLine[lineNumber];
results1 = [];
for (k = 0, len1 = ref1.length; k < len1; k++) {
tok = ref1[k];
if ((tok.generated == null) && tok[0] !== 'OUTDENT') {
results1.push(tok);
}
}
return results1;
}
}
// Returns a corrected INDENT value if the current line is part of
// a chained call. Otherwise returns original INDENT value.
getCorrectIndent(tokenApi) {
var _, curIndent, i, j, len, lineNumber, lines, prevIndent, prevNum, prevTokens, ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7, ret, skipAssign, t, tokens, tryLine;
({lineNumber, lines, tokens} = tokenApi);
curIndent = (ref = lines[lineNumber].match(/\S/)) != null ? ref.index : void 0;
prevNum = 1;
while (/^\s*(#|$)/.test(lines[lineNumber - prevNum])) {
prevNum += 1;
}
prevTokens = this.grabLineTokens(tokenApi, lineNumber - prevNum);
if (((ref1 = prevTokens[0]) != null ? ref1[0] : void 0) === 'INDENT') {
// Pass both the INDENT value and the location of the first token
// after the INDENT because sometimes CoffeeScript doesn't return
// the correct INDENT if there is something like an if/else
// inside an if/else inside of a -> function definition: e.g.
// ->
// r = if a
// if b
// 2
// else
// 3
// else
// 4
// will error without: curIndent - prevTokens[1]?[2].first_column
return [curIndent - ((ref2 = prevTokens[1]) != null ? ref2[2].first_column : void 0), curIndent - prevTokens[0][1]];
} else {
prevIndent = (ref3 = prevTokens[0]) != null ? ref3[2].first_column : void 0;
// This is a scan to handle extra indentation from if/else
// statements to make them look nicer: e.g.
// r = if a
// true
// else
// false
// is valid.
// r = if a
// true
// else
// false
// is also valid.
for (j = i = 0, len = prevTokens.length; i < len; j = ++i) {
_ = prevTokens[j];
if (!(prevTokens[j][0] === '=' && ((ref4 = prevTokens[j + 1]) != null ? ref4[0] : void 0) === 'IF')) {
continue;
}
skipAssign = curIndent - prevTokens[j + 1][2].first_column;
ret = curIndent - prevIndent;
if (skipAssign < 0) {
return [ret];
}
return [skipAssign, ret];
}
// This happens when there is an extra indent to maintain long
// conditional statements (IF/UNLESS): e.g.
// ->
// if a is c and
// (false or
// long.expression.that.necessitates(linebreak))
// @foo()
// is valid (note that there an only an extra indent in the last
// statement is required and not the line above it
// ->
// if a is c and
// (false or
// long.expression.that.necessitates(linebreak))
// @foo()
// is also OK.
while (prevIndent > curIndent) {
tryLine = lineNumber - prevNum;
prevTokens = this.grabLineTokens(tokenApi, tryLine, true);
// This is to handle weird object/string indentation.
// See: 'Handle edge-case weirdness with strings in objects'
// test case in test_indentation.coffee or in the file,
// test_no_empty_functions.coffee, which is why/how I
// caught this.
if (((ref5 = prevTokens[0]) != null ? ref5[0] : void 0) === 'INDENT') {
prevIndent = prevTokens[0][1];
prevTokens = prevTokens.slice(1);
}
t = 0;
// keep looping prevTokens until we find a token in @keywords
// or we just run out of tokens in prevTokens
while (!((prevTokens[t] == null) || (ref6 = prevTokens[t][0], indexOf.call(this.keywords, ref6) >= 0))) {
t++;
}
// slice off everything before 't'
prevTokens = prevTokens.slice(t);
prevNum++;
if (prevTokens[0] == null) {
// if there isn't a valid token, restart the while loop
continue;
}
// set new "prevIndent"
prevIndent = (ref7 = prevTokens[0]) != null ? ref7[2].first_column : void 0;
}
}
return [curIndent - prevIndent];
}
};
Indentation.prototype.rule = {
type: 'problem',
name: 'indentation',
value: 2,
level: 'error',
message: 'Line contains inconsistent indentation',
description: `This rule imposes a standard number of spaces(tabs) 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.`
};
Indentation.prototype.tokens = ['INDENT', '[', ']', '.'];
Indentation.prototype.keywords = ['->', '=>', '@', 'CATCH', 'CLASS', 'DEFAULT', 'ELSE', 'EXPORT', 'FINALLY', 'FOR', 'FORIN', 'FOROF', 'IDENTIFIER', 'IF', 'IMPORT', 'LEADING_WHEN', 'LOOP', 'PROPERTY', 'RETURN', 'SWITCH', 'THROW', 'TRY', 'UNTIL', 'WHEN', 'WHILE', 'YIELD'];
return Indentation;
}).call(this);
}).call(this);