UNPKG

@coffeelint/cli

Version:
274 lines (249 loc) 10.4 kB
(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);