UNPKG

jscs

Version:

JavaScript Code Style

684 lines (596 loc) 18.6 kB
// var assert = require('assert'); var cst = require('cst'); var Parser = cst.Parser; var Token = cst.Token; var Program = cst.types.Program; var Fragment = cst.Fragment; var ScopesApi = cst.api.ScopesApi; var treeIterator = require('./tree-iterator'); // var Program = cst.types.Program; /** * Operator list which are represented as keywords in token list. */ var KEYWORD_OPERATORS = { 'instanceof': true, 'in': true }; /** * File representation for JSCS. * * @name JsFile * @param {Object} params * @param {String} params.filename * @param {String} params.source * @param {Boolean} [params.es3] */ var JsFile = function(params) { params = params || {}; this._parseErrors = []; this._filename = params.filename; this._source = params.source; this._es3 = params.es3 || false; this._lineBreaks = null; this._lines = this._source.split(/\r\n|\r|\n/); var parser = new Parser({ strictMode: false, languageExtensions: { gritDirectives: true, appleInstrumentationDirectives: true } }); try { this._program = parser.parse(this._source); } catch (e) { this._parseErrors.push(e); this._program = new Program([ new Token('EOF', '') ]); } // Lazy initialization this._scopes = null; }; JsFile.prototype = { /** * @returns {cst.types.Program} */ getProgram: function() { return this._program; }, /** * Returns the first line break character encountered in the file. * Assumes LF if the file is only one line. * * @returns {String} */ getLineBreakStyle: function() { var lineBreaks = this.getLineBreaks(); return lineBreaks.length ? lineBreaks[0] : '\n'; }, /** * Returns all line break characters from the file. * * @returns {String[]} */ getLineBreaks: function() { if (this._lineBreaks === null) { this._lineBreaks = this._source.match(/\r\n|\r|\n/g) || []; } return this._lineBreaks; }, /** * Sets whitespace before specified token. * * @param {Object} token - in front of which we will add/remove/replace the whitespace token * @param {String} whitespace - value of the whitespace token - `\n`, `\s`, `\t` */ setWhitespaceBefore: function(token, whitespace) { var prevToken = token.getPreviousToken(); var ws = new Token('Whitespace', whitespace); var fragment = new Fragment(ws); if (prevToken && prevToken.isWhitespace) { if (whitespace === '') { prevToken.remove(); return; } prevToken.parentElement.replaceChild(fragment, prevToken); return; } this._setTokenBefore(token, fragment); }, _setTokenBefore: function(token, fragment) { var parent = token; var grandpa = parent.parentElement; while (grandpa) { try { grandpa.insertChildBefore(fragment, parent); break; } catch (e) {} parent = grandpa; grandpa = parent.parentElement; } }, /** * Returns whitespace before specified token. * * @param {Object} token * @returns {String} */ getWhitespaceBefore: function(token) { if (!token.getPreviousToken) { console.log(token); } var prev = token.getPreviousToken(); if (prev && prev.isWhitespace) { return prev.getSourceCode(); } return ''; }, /** * Returns the first token for the node from the AST. * * @param {Object} node * @returns {Object} */ getFirstNodeToken: function(node) { return node.getFirstToken(); }, /** * Returns the last token for the node from the AST. * * @param {Object} node * @returns {Object} */ getLastNodeToken: function(node) { return node.getLastToken(); }, /** * Returns the first token for the file. * * @param {Option} [options] * @param {Boolean} [options.includeComments=false] * @param {Boolean} [options.includeWhitespace=false] * @returns {Object} */ getFirstToken: function(/*options*/) { return this._program.getFirstToken(); }, /** * Returns the last token for the file. * * @param {Option} [options] * @param {Boolean} [options.includeComments=false] * @param {Boolean} [options.includeWhitespace=false] * @returns {Object} */ getLastToken: function(/*options*/) { return this._program.getLastToken(); }, /** * Returns the first token before the given. * * @param {Object} token * @param {Object} [options] * @param {Boolean} [options.includeComments=false] * @returns {Object|undefined} */ getPrevToken: function(token, options) { if (options && options.includeComments) { return token.getPreviousNonWhitespaceToken(); } return token.getPreviousCodeToken(); }, /** * Returns the first token after the given. * * @param {Object} token * @param {Object} [options] * @param {Boolean} [options.includeComments=false] * @returns {Object|undefined} */ getNextToken: function(token, options) { if (options && options.includeComments) { return token.getNextNonWhitespaceToken(); } else { return token.getNextCodeToken(); } }, /** * Returns the first token before the given which matches type (and value). * * @param {Object} token * @param {String} type * @param {String} [value] * @returns {Object|null} */ findPrevToken: function(token, type, value) { var prevToken = this.getPrevToken(token); while (prevToken) { if (prevToken.type === type && (value === undefined || prevToken.value === value)) { return prevToken; } prevToken = this.getPrevToken(prevToken); } return prevToken; }, /** * Returns the first token after the given which matches type (and value). * * @param {Object} token * @param {String} type * @param {String} [value] * @returns {Object|null} */ findNextToken: function(token, type, value) { var nextToken = token.getNextToken(); while (nextToken) { if (nextToken.type === type && (value === undefined || nextToken.value === value)) { return nextToken; } nextToken = nextToken.getNextToken(); } return nextToken; }, /** * Returns the first token before the given which matches type (and value). * * @param {Object} token * @param {String} value * @returns {Object|null} */ findPrevOperatorToken: function(token, value) { return this.findPrevToken(token, value in KEYWORD_OPERATORS ? 'Keyword' : 'Punctuator', value); }, /** * Returns the first token after the given which matches type (and value). * * @param {Object} token * @param {String} value * @returns {Object|null} */ findNextOperatorToken: function(token, value) { return this.findNextToken(token, value in KEYWORD_OPERATORS ? 'Keyword' : 'Punctuator', value); }, /** * Iterates through the token tree using tree iterator. * Calls passed function for every token. * * @param {Function} cb * @param {Object} [tree] */ iterate: function(cb, tree) { return treeIterator.iterate(tree || this._program, cb); }, /** * Returns nodes by type(s) from earlier built index. * * @param {String|String[]} type * @returns {Object[]} */ getNodesByType: function(type) { type = Array.isArray(type) ? type : [type]; var result = []; for (var i = 0, l = type.length; i < l; i++) { var nodes = this._program.selectNodesByType(type[i]); if (nodes) { result = result.concat(nodes); } } return result; }, /** * Iterates nodes by type(s) from earlier built index. * Calls passed function for every matched node. * * @param {String|String[]} type * @param {Function} cb * @param {Object} context */ iterateNodesByType: function(type, cb, context) { return this.getNodesByType(type).forEach(cb, context || this); }, /** * Iterates tokens by type(s) from the token array. * Calls passed function for every matched token. * * @param {String|String[]} type * @param {Function} cb */ iterateTokensByType: function(type, cb) { var tokens; if (Array.isArray(type)) { tokens = []; for (var i = 0; i < type.length; i++) { var items = this._program.selectTokensByType(type[i]); tokens = tokens.concat(items); } } else { tokens = this._program.selectTokensByType(type); } tokens.forEach(cb); }, /** * Iterates tokens by type and value(s) from the token array. * Calls passed function for every matched token. * * @param {String} type * @param {String|String[]} value * @param {Function} cb */ iterateTokensByTypeAndValue: function(type, value, cb) { var values = (typeof value === 'string') ? [value] : value; var valueIndex = {}; values.forEach(function(type) { valueIndex[type] = true; }); this.iterateTokensByType(type, function(token) { if (valueIndex[token.value]) { cb(token); } }); }, getFirstTokenOnLineWith: function(element, options) { options = options || {}; var firstToken = element; if (element.isComment && !options.includeComments) { firstToken = null; } if (element.isWhitespace && !options.includeWhitespace) { firstToken = null; } var currentToken = element.getPreviousToken(); while (currentToken) { if (currentToken.isWhitespace) { if (currentToken.getNewlineCount() > 0 || !currentToken.getPreviousToken()) { if (options.includeWhitespace) { firstToken = currentToken; } break; } } else if (currentToken.isComment) { if (options.includeComments) { firstToken = currentToken; break; } if (currentToken.getNewlineCount() > 0) { break; } } else { firstToken = currentToken; } currentToken = currentToken.getPreviousToken(); } if (firstToken) { return firstToken; } currentToken = element.getNextToken(); while (currentToken) { if (currentToken.isWhitespace) { if (currentToken.getNewlineCount() > 0 || !currentToken.getNextToken()) { if (options.includeWhitespace) { firstToken = currentToken; } break; } } else if (currentToken.isComment) { if (options.includeComments) { firstToken = currentToken; break; } if (currentToken.getNewlineCount() > 0) { break; } } else { firstToken = currentToken; } currentToken = currentToken.getNextToken(); } return firstToken; }, /** * Returns last token for the specified line. * Line numbers start with 1. * * @param {Number} lineNumber * @param {Object} [options] * @param {Boolean} [options.includeComments = false] * @param {Boolean} [options.includeWhitespace = false] * @returns {Object|null} */ getLastTokenOnLine: function(lineNumber, options) { options = options || {}; var loc; var token = this._program.getLastToken(); var currentToken; while (token) { loc = token.getLoc(); currentToken = token; token = token.getPreviousToken(); if (loc.start.line <= lineNumber && loc.end.line >= lineNumber) { // Since whitespace tokens can contain newlines we need to check // if position is in the range, not exact match if (currentToken.isWhitespace && !options.includeWhitespace) { continue; } } if (loc.start.line === lineNumber || loc.end.line === lineNumber) { if (currentToken.isComment && !options.includeComments) { continue; } return currentToken; } } return null; }, /** * Returns which dialect of JS this file supports. * * @returns {String} */ getDialect: function() { if (this._es3) { return 'es3'; } return 'es6'; }, /** * Returns string representing contents of the file. * * @returns {String} */ getSource: function() { return this._source; }, /** * Returns token program. * * @returns {Object} */ getTree: function() { return this._program || {}; }, /** * Returns comment token list. */ getComments: function() { var comments = []; var token = this._program.getFirstToken(); while (token) { if (token.isComment) { comments[comments.length] = token; } token = token.getNextToken(); } return comments; }, /** * Returns source filename for this object representation. * * @returns {String} */ getFilename: function() { return this._filename; }, /** * Returns array of source lines for the file. * * @returns {String[]} */ getLines: function() { return this._lines; }, /** * Returns analyzed scope. * * @returns {Object} */ getScopes: function() { if (!this._scopes) { this._scopes = new ScopesApi(this._program); } return this._scopes; }, /** * Are tokens on the same line. * * @param {Element} tokenBefore * @param {Element} tokenAfter * @return {Boolean} */ isOnTheSameLine: function(tokenBefore, tokenAfter) { if (tokenBefore === tokenAfter) { return true; } tokenBefore = tokenBefore instanceof Token ? tokenBefore : tokenBefore.getLastToken(); tokenAfter = tokenAfter instanceof Token ? tokenAfter : tokenAfter.getFirstToken(); var currentToken = tokenBefore; while (currentToken) { if (currentToken === tokenAfter) { return true; } if (currentToken !== tokenBefore && currentToken.getNewlineCount() > 0) { return false; } currentToken = currentToken.getNextToken(); } return false; }, getDistanceBetween: function(tokenBefore, tokenAfter) { if (tokenBefore === tokenAfter) { return 0; } tokenBefore = tokenBefore instanceof Token ? tokenBefore : tokenBefore.getLastToken(); tokenAfter = tokenAfter instanceof Token ? tokenAfter : tokenAfter.getFirstToken(); var currentToken = tokenBefore.getNextToken(); var distance = 0; while (currentToken) { if (currentToken === tokenAfter) { break; } distance += currentToken.getSourceCodeLength(); currentToken = currentToken.getNextToken(); } return distance; }, getLineCountBetween: function(tokenBefore, tokenAfter) { if (tokenBefore === tokenAfter) { return 0; } tokenBefore = tokenBefore instanceof Token ? tokenBefore : tokenBefore.getLastToken(); tokenAfter = tokenAfter instanceof Token ? tokenAfter : tokenAfter.getFirstToken(); var currentToken = tokenBefore.getNextToken(); var lineCount = 0; while (currentToken) { if (currentToken === tokenAfter) { break; } lineCount += currentToken.getNewlineCount(); currentToken = currentToken.getNextToken(); } return lineCount; }, /** * Returns array of source lines for the file with comments removed. * * @returns {Array} */ getLinesWithCommentsRemoved: function() { var lines = this.getLines().concat(); this.getComments().concat().reverse().forEach(function(comment) { var loc = comment.getLoc(); var startLine = loc.start.line; var startCol = loc.start.column; var endLine = loc.end.line; var endCol = loc.end.column; var i = startLine - 1; if (startLine === endLine) { // Remove tralling spaces (see gh-1968) lines[i] = lines[i].replace(/\*\/\s+/, '\*\/'); lines[i] = lines[i].substring(0, startCol) + lines[i].substring(endCol); } else { lines[i] = lines[i].substring(0, startCol); for (var x = i + 1; x < endLine - 1; x++) { lines[x] = ''; } lines[x] = lines[x].substring(endCol); } }); return lines; }, /** * Renders JS-file sources using token list. * * @returns {String} */ render: function() { return this._program.getSourceCode(); }, /** * Returns list of parse errors. * * @returns {Error[]} */ getParseErrors: function() { return this._parseErrors; } }; module.exports = JsFile;