jscs
Version:
JavaScript Code Style
412 lines (354 loc) • 13.3 kB
JavaScript
var utils = require('util');
var EventEmitter = require('events').EventEmitter;
var Token = require('cst').Token;
/**
* Token assertions class.
*
* @name {TokenAssert}
* @param {JsFile} file
*/
function TokenAssert(file) {
EventEmitter.call(this);
this._file = file;
}
utils.inherits(TokenAssert, EventEmitter);
/**
* Requires to have whitespace between specified tokens. Ignores newlines.
*
* @param {Object} options
* @param {Object} options.token
* @param {Object} options.nextToken
* @param {String} [options.message]
* @param {Number} [options.spaces] Amount of spaces between tokens.
* @return {Boolean} whether an error was found
*/
TokenAssert.prototype.whitespaceBetween = function(options) {
options.atLeast = 1;
return this.spacesBetween(options);
};
/**
* Requires to have no whitespace between specified tokens.
*
* @param {Object} options
* @param {Object} options.token
* @param {Object} options.nextToken
* @param {String} [options.message]
* @param {Boolean} [options.disallowNewLine=false]
* @return {Boolean} whether an error was found
*/
TokenAssert.prototype.noWhitespaceBetween = function(options) {
options.exactly = 0;
return this.spacesBetween(options);
};
/**
* Requires to have the whitespace between specified tokens with the provided options.
*
* @param {Object} options
* @param {Object} options.token
* @param {Object} options.nextToken
* @param {String} [options.message]
* @param {Object} [options.atLeast] At least how many spaces the tokens are apart
* @param {Object} [options.atMost] At most how many spaces the tokens are apart
* @param {Object} [options.exactly] Exactly how many spaces the tokens are apart
* @param {Boolean} [options.disallowNewLine=false]
* @return {Boolean} whether an error was found
*/
TokenAssert.prototype.spacesBetween = function(options) {
var token = options.token;
var nextToken = options.nextToken;
var atLeast = options.atLeast;
var atMost = options.atMost;
var exactly = options.exactly;
if (!token || !nextToken) {
return false;
}
this._validateOptions(options);
if (!options.disallowNewLine && !this._file.isOnTheSameLine(token, nextToken)) {
return false;
}
// Only attempt to remove or add lines if there are no comments between the two nodes
// as this prevents accidentally moving a valid token onto a line comment ed line
var fixed = !options.token.getNextNonWhitespaceToken().isComment;
var emitError = function(countPrefix, spaceCount) {
var fix = function() {
this._file.setWhitespaceBefore(nextToken, new Array(spaceCount + 1).join(' '));
}.bind(this);
var msgPostfix = token.value + ' and ' + nextToken.value;
if (!options.message) {
if (exactly === 0) {
// support noWhitespaceBetween
options.message = 'Unexpected whitespace between ' + msgPostfix;
} else if (exactly !== undefined) {
// support whitespaceBetween (spaces option)
options.message = spaceCount + ' spaces required between ' + msgPostfix;
} else if (atLeast === 1 && atMost === undefined) {
// support whitespaceBetween (no spaces option)
options.message = 'Missing space between ' + msgPostfix;
} else {
options.message = countPrefix + ' ' + spaceCount + ' spaces required between ' + msgPostfix;
}
}
this.emit('error', {
message: options.message,
element: token,
offset: token.getSourceCodeLength(),
fix: fixed ? fix : undefined
});
}.bind(this);
var spacesBetween = this._file.getDistanceBetween(token, nextToken);
if (atLeast !== undefined && spacesBetween < atLeast) {
emitError('at least', atLeast);
return true;
}
if (atMost !== undefined && spacesBetween > atMost) {
emitError('at most', atMost);
return true;
}
if (exactly !== undefined && spacesBetween !== exactly) {
emitError('exactly', exactly);
return true;
}
return false;
};
/**
* Requires the specified line to have the expected indentation.
*
* @param {Object} options
* @param {Number} options.actual
* @param {Number} options.expected
* @param {String} options.indentChar
* @param {String} options.token
* @return {Boolean} whether an error was found
*/
TokenAssert.prototype.indentation = function(options) {
var token = options.token;
var lineNumber = options.lineNumber;
var actual = options.actual;
var expected = options.expected;
var indentChar = options.indentChar;
if (actual === expected) {
return false;
}
this.emit('error', {
message: 'Expected indentation of ' + expected + ' characters',
line: lineNumber,
column: expected,
fix: function() {
var newWhitespace = (new Array(expected + 1)).join(indentChar);
this._updateWhitespaceByLine(token, function(lines) {
lines[lines.length - 1] = newWhitespace;
return lines;
});
if (token.isComment) {
this._updateCommentWhitespace(token, indentChar, actual, expected);
}
}.bind(this)
});
return true;
};
/**
* Updates the whitespace of a line by passing split lines to a callback function
* for editing.
*
* @param {Object} token
* @param {Function} callback
*/
TokenAssert.prototype._updateWhitespaceByLine = function(token, callback) {
var lineBreak = this._file.getLineBreakStyle();
var lines = this._file.getWhitespaceBefore(token).split(/\r\n|\r|\n/);
lines = callback(lines);
this._file.setWhitespaceBefore(token, lines.join(lineBreak));
};
/**
* Updates the whitespace of a line by passing split lines to a callback function
* for editing.
*
* @param {Object} token
* @param {Function} indentChar
* @param {Number} actual
* @param {Number} expected
*/
TokenAssert.prototype._updateCommentWhitespace = function(token, indentChar, actual, expected) {
var difference = expected - actual;
var tokenLines = token.value.split(/\r\n|\r|\n/);
var i = 1;
if (difference >= 0) {
var lineWhitespace = (new Array(difference + 1)).join(indentChar);
for (; i < tokenLines.length; i++) {
tokenLines[i] = tokenLines[i] === '' ? '' : lineWhitespace + tokenLines[i];
}
} else {
for (; i < tokenLines.length; i++) {
tokenLines[i] = tokenLines[i].substring(-difference);
}
}
var newComment = new Token('CommentBlock', tokenLines.join(this._file.getLineBreakStyle()));
token.parentElement.replaceChild(newComment, token);
};
/**
* Requires tokens to be on the same line.
*
* @param {Object} options
* @param {Object} options.token
* @param {Object} options.nextToken
* @param {Boolean} [options.stickToPreviousToken]
* @param {String} [options.message]
* @return {Boolean} whether an error was found
*/
TokenAssert.prototype.sameLine = function(options) {
options.exactly = 0;
return this.linesBetween(options);
};
/**
* Requires tokens to be on different lines.
*
* @param {Object} options
* @param {Object} options.token
* @param {Object} options.nextToken
* @param {Object} [options.message]
* @return {Boolean} whether an error was found
*/
TokenAssert.prototype.differentLine = function(options) {
options.atLeast = 1;
return this.linesBetween(options);
};
/**
* Requires tokens to have a certain amount of lines between them.
* Set at least one of atLeast or atMost OR set exactly.
*
* @param {Object} options
* @param {Object} options.token
* @param {Object} options.nextToken
* @param {Object} [options.message]
* @param {Object} [options.atLeast] At least how many lines the tokens are apart
* @param {Object} [options.atMost] At most how many lines the tokens are apart
* @param {Object} [options.exactly] Exactly how many lines the tokens are apart
* @param {Boolean} [options.stickToPreviousToken] When auto-fixing stick the
* nextToken onto the previous token.
* @return {Boolean} whether an error was found
*/
TokenAssert.prototype.linesBetween = function(options) {
var token = options.token;
var nextToken = options.nextToken;
var atLeast = options.atLeast;
var atMost = options.atMost;
var exactly = options.exactly;
if (!token || !nextToken) {
return false;
}
this._validateOptions(options);
// Only attempt to remove or add lines if there are no comments between the two nodes
// as this prevents accidentally moving a valid token onto a line comment ed line
var fixed = !options.token.getNextNonWhitespaceToken().isComment;
var linesBetween = this._file.getLineCountBetween(token, nextToken);
var emitError = function(countPrefix, lineCount) {
var msgPrefix = token.value + ' and ' + nextToken.value;
var fix = function() {
this._augmentLineCount(options, lineCount);
}.bind(this);
if (!options.message) {
if (exactly === 0) {
// support sameLine
options.message = msgPrefix + ' should be on the same line';
} else if (atLeast === 1 && atMost === undefined) {
// support differentLine
options.message = msgPrefix + ' should be on different lines';
} else {
// support linesBetween
options.message = msgPrefix + ' should have ' + countPrefix + ' ' + lineCount + ' line(s) between them';
}
}
this.emit('error', {
message: options.message,
element: token,
offset: token.getSourceCodeLength(),
fix: fixed ? fix : undefined
});
}.bind(this);
if (atLeast !== undefined && linesBetween < atLeast) {
emitError('at least', atLeast);
return true;
}
if (atMost !== undefined && linesBetween > atMost) {
emitError('at most', atMost);
return true;
}
if (exactly !== undefined && linesBetween !== exactly) {
emitError('exactly', exactly);
return true;
}
return false;
};
/**
* Throws errors if atLeast, atMost, and exactly options don't mix together properly or
* if the tokens provided are equivalent.
*
* @param {Object} options
* @param {Object} options.token
* @param {Object} options.nextToken
* @param {Object} [options.atLeast] At least how many spaces the tokens are apart
* @param {Object} [options.atMost] At most how many spaces the tokens are apart
* @param {Object} [options.exactly] Exactly how many spaces the tokens are apart
* @throws {Error} If the options are non-sensical
* @private
*/
TokenAssert.prototype._validateOptions = function(options) {
var token = options.token;
var nextToken = options.nextToken;
var atLeast = options.atLeast;
var atMost = options.atMost;
var exactly = options.exactly;
if (token === nextToken) {
throw new Error('You cannot specify the same token as both token and nextToken');
}
if (atLeast === undefined &&
atMost === undefined &&
exactly === undefined) {
throw new Error('You must specify at least one option');
}
if (exactly !== undefined && (atLeast !== undefined || atMost !== undefined)) {
throw new Error('You cannot specify atLeast or atMost with exactly');
}
if (atLeast !== undefined && atMost !== undefined && atMost < atLeast) {
throw new Error('atLeast and atMost are in conflict');
}
};
/**
* Augments token whitespace to contain the correct number of newlines while preserving indentation
*
* @param {Object} options
* @param {Object} options.nextToken
* @param {Boolean} [options.stickToPreviousToken]
* @param {Number} lineCount
* @private
*/
TokenAssert.prototype._augmentLineCount = function(options, lineCount) {
var token = options.nextToken;
if (lineCount === 0) {
if (options.stickToPreviousToken) {
var nextToken = this._file.getNextToken(token, {
includeComments: true
});
this._file.setWhitespaceBefore(nextToken, this._file.getWhitespaceBefore(token));
}
this._file.setWhitespaceBefore(token, ' ');
return;
}
this._updateWhitespaceByLine(token, function(lines) {
var currentLineCount = lines.length;
var lastLine = lines[lines.length - 1];
if (currentLineCount <= lineCount) {
// add additional lines that maintain the same indentation as the former last line
for (; currentLineCount <= lineCount; currentLineCount++) {
lines[lines.length - 1] = '';
lines.push(lastLine);
}
} else {
// remove lines and then ensure that the new last line maintains the previous indentation
lines = lines.slice(0, lineCount + 1);
lines[lines.length - 1] = lastLine;
}
return lines;
});
};
module.exports = TokenAssert;