From a81be15e81ea96d00b50ea8e57755040ec68a1e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Garc=C3=ADa?= Date: Sat, 24 Feb 2018 21:38:26 +0100 Subject: [PATCH 01/11] remove cli init, skip non-issues, massive refactor, asyncify tests --- .codeclimate.yml | 1 + README.md | 12 - lib/index.js | 543 ++++++----- package-lock.json | 1592 +++++++++++++++++++++++++++++++ package.json | 3 +- spec/fixtures/.codeclimate.yml | 3 - spec/linter-codeclimate-spec.js | 56 +- 7 files changed, 1952 insertions(+), 258 deletions(-) create mode 100644 package-lock.json diff --git a/.codeclimate.yml b/.codeclimate.yml index 5182826..7520277 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -7,3 +7,4 @@ ratings: - "**.js" exclude_paths: - node_modules/**/* +- spec/**/* diff --git a/README.md b/README.md index a3e95e5..d92a2c8 100644 --- a/README.md +++ b/README.md @@ -49,15 +49,3 @@ And you should see some progress, and most likely, some results. Cool! Now it's Once you have a functioning installation of the Code Climate CLI, you're ready to see the same results within the comfort of your favorite text editor. Install the `linter` package, the `linter-codeclimate` package, and make sure any of the language modes you prefer are also installed. Once these are installed, reload your editor (`View` -> `Reload` from the Atom menu), open a file of your choice, and save it. Code Climate analysis will run in the background and then pop up results that you can inspect right inside Atom. Awesome! You're now linting with superpowers. - -## Installation: Special Considerations - -Note that currently `linter-codeclimate` works only on single file analysis types, not on engines which analyze the entire codebase at once. The following engines currently work with the Atom package (this will soon be all packages - thanks for your patience as we work out some kinks): - - -* All Languages: fixme -* Python: Radon, Pep8 -* Ruby: Rubocop -* CoffeeScript: CoffeeLint -* JavaScript: ESLint -* PHP: PHPCodeSniffer, PHPMD diff --git a/lib/index.js b/lib/index.js index fe8cd25..b5410fe 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,88 +1,356 @@ 'use babel'; +/* eslint no-use-before-define:0 */ + // eslint-disable-next-line import/extensions, import/no-extraneous-dependencies import { CompositeDisposable } from 'atom'; -import { dirname } from 'path'; - -const badCommands = new Set(); +import { dirname, join } from 'path'; +import * as Helpers from 'atom-linter'; -const startMeasure = (baseName) => { - const startMark = `${baseName}-start`; - // Clear start mark from previous execution for the same file - if (performance.getEntriesByName(startMark).length) { - performance.clearMarks(startMark); - } - performance.mark(startMark); +const configurationFile = '.codeclimate.yml'; +const execArgs = ['analyze', '-f', 'json']; +const logHeader = 'linter-codeclimate::'; +const linting = {}; +const fingerprints = {}; +const debounceTimeout = 250; +const mapSeverity = { + major: 'error', + minor: 'warning', +}; +const notificationDefaults = { + buttons: [{ + className: 'btn-install', + onDidClick: () => { + // eslint-disable-next-line import/no-extraneous-dependencies + require('shell').openExternal('https://github.com/codeclimate/codeclimate'); + }, + text: 'Install guide', + }], + dismissable: true, }; -const endMeasure = (baseName) => { - if (atom.inDevMode()) { - performance.mark(`${baseName}-end`); - performance.measure(baseName, `${baseName}-start`, `${baseName}-end`); - // eslint-disable-next-line no-console - console.log(`${baseName} took: ${performance.getEntriesByName(baseName)[0].duration}`); - performance.clearMarks(`${baseName}-end`); - performance.clearMeasures(baseName); - } - performance.clearMarks(`${baseName}-start`); +/** + * @summary Promisify a delay (timeout). + * @param {Integer} ms The time (milliseconds) to delay. + * @return {Promise} Promise that is resolved after `ms` milliseconds. + */ +const delay = async ms => new Promise(resolve => setTimeout(resolve, ms)); + +/** + * @summary Resets the flags for project at `projectRoot`. + * @param {String} projectRoot The absolute path to the project root. + */ +const reset = (projectRoot) => { + delete fingerprints[projectRoot]; + delete linting[projectRoot]; }; -const mapSeverity = { - major: 'error', - minor: 'warning', +const measure = { + start(cwd) { + if (!atom.inDevMode()) return; + const startMark = `${cwd}-start`; + // Clear start mark from previous execution for the same file + if (performance.getEntriesByName(startMark).length) { + performance.clearMarks(startMark); + } + performance.mark(startMark); + }, + + end(cwd) { + if (!atom.inDevMode()) return; + const mark = { + start: `${cwd}-start`, + end: `${cwd}-end`, + }; + performance.mark(mark.end); + performance.measure(cwd, mark.start, mark.end); + // eslint-disable-next-line no-console + console.log( + `${logHeader} Analysis for ${cwd} took:`, + performance.getEntriesByName(cwd)[0].duration.toFixed(2), + ); + performance.clearMeasures(cwd); + performance.clearMarks(mark.start); + performance.clearMarks(mark.end); + }, }; /** - * Show a clearer error in Atom when the exact problem is known. - * - * @param {Error} err The caught error. - * @param {String} cmd The CodeClimate commmand. - * @param {String} [description=''] A descriptive explanation of the error in - * Markdown (preserves line feeds). - * @param {String} [extraDetails=''] Additional details to document the error (shown - * only code and message when available by default) - * (plain text, does NOT preserve line feeds). - * @param {Object[]} [buttons=[]] Array of buttons to show. - * @see {@link https://atom.io/docs/api/v1.8.0/NotificationManager#instance-addError|Adding error notifications} + * @summary Show a clearer error in Atom when the exact problem is known. + * @param {Error} err The caught error. + * @param {String} [description=''] A descriptive explanation of the error in + * Markdown (preserves line feeds). + * @see {@link https://atom.io/docs/api/latest/NotificationManager#instance-addError|Adding error notifications} */ -const notifyError = (err, cmd, description = '', extraDetails = '', buttons = []) => { +const notifyError = (err, description = '') => { let friendlyDesc = ''; - let detail = `Exception details:\n- COMMAND: \`${cmd}\``; - if (err && err.code) { + let detail = 'Exception details:'; + + if (err.message) { + detail += `\n- MESSAGE: ${err.message}`; + } + + if (err.code) { detail += `\n- CODE: ${err.code}`; switch (err.code) { case 'ENOENT': - friendlyDesc = 'CodeClimate binary could not be found.'; break; + friendlyDesc = 'CodeClimate binary could not be found.'; + break; case 'EACCES': case 'EDIR': - friendlyDesc = 'Executable path not pointing to a binary.'; break; + friendlyDesc = 'Executable path not pointing to a binary.'; + break; default: friendlyDesc = 'CodeClimate execution failed.'; } } - if (err && err.message) detail += `\n- MESSAGE: ${err.message}`; - if (extraDetails) detail += `\n${extraDetails}`; - if (!badCommands.has(cmd)) { - atom.notifications.addError('linter-codeclimate error', { - buttons: [{ - className: 'btn-install', - onDidClick: () => { - // eslint-disable-next-line import/no-extraneous-dependencies - require('shell').openExternal('https://github.com/codeclimate/codeclimate'); - }, - text: 'Install guide', - }].concat(buttons), - detail, - description: `${description}\n${friendlyDesc}`.trim(), - dismissable: true, - stack: err.stack, + + const options = Object.assign(notificationDefaults, { + description: `${description}\n${friendlyDesc}`.trim(), + detail, + stack: err.stack, + }); + atom.notifications.addError('linter-codeclimate error', options); +}; + +/** + * @summary Checks if the reported issue has been reported previously (duplicated). + * @return {Boolean} Whether the issue is duplicated (`true`) or not (`false`). + * @todo Remove after fixing https://github.com/phpmd/phpmd/issues/467 + */ +const reportedPreviously = (projectRoot, fingerprint) => { + if (!Object.prototype.hasOwnProperty.call(fingerprints, projectRoot)) { + fingerprints[projectRoot] = new Set(); + } + + if (fingerprints[projectRoot].has(fingerprint)) return true; + + fingerprints[projectRoot].add(fingerprint); + return false; +}; + +/** + * Search for a CodeClimate config file in the project tree. If none found, + * use the presence of a `.git` directory as the assumed project root. + * + * @param {String} filePath The absolute path to the file triggering the analysis. + * @return {Promise} The absolute path to the project root. + */ +const findProjectRoot = async (filePath) => { + const fileDir = dirname(filePath); + const configurationFilePath = await Helpers.findAsync(fileDir, configurationFile); + + if (configurationFilePath !== null) { + return dirname(configurationFilePath); + } + + // Fall back to dir of current file if a .git repo can't be found. + const gitPath = await Helpers.findAsync(fileDir, '.git'); + return dirname(gitPath || filePath); +}; + +/** + * Returns the range (lines/columns) for a given issue from its location. + * + * @param {TextEditor} textEditor The Atom TextEditor instance. + * @param {Object} location The location object of the CodeClimate issue. + * @return {Array[]} The range: `[[lineNumber, colStart], [lineNumber, colEnd]]`. + */ +const calcRange = (textEditor, location) => { + // Issue only has a line number + if (!Object.prototype.hasOwnProperty.call(location, 'positions')) { + return Helpers.generateRange(textEditor, location.lines.begin - 1); + } + + const { positions } = location; + const line = positions.begin.line - 1; + + // Invalid starting column, just treat it as a line number + if (positions.begin.column === undefined) { + return Helpers.generateRange(textEditor, line); + } + + const colStart = (positions.begin.column - 1) || 0; + const colEnd = (positions.end.column === undefined) + ? undefined : (positions.end.column - 1); + + // No valid end column, let generateRange highlight a word + if (colEnd === undefined || colStart === colEnd) { + return Helpers.generateRange(textEditor, line, colStart); + } + + // Valid end column, and different from the start one + return [[line, colStart], [line, colEnd]]; +}; + +/** + * @summary Fetch the paths of all currently open files on Atom. + * @return {Object} Dictionary of open file abspaths ~> textEditor. + * + * NOTE Files indexed by abspath to avoid relative filepath collision among different projects. + */ +const fetchOpenFilepaths = () => { + const openFiles = {}; + atom.workspace.textEditorRegistry.editors.forEach((textEditor) => { + openFiles[textEditor.getPath()] = textEditor; + }); + return openFiles; +}; + +/** + * @summary Parses the issues reported by CodeClimate CLI to the format AtomLinter expects. + * @param {Object} path Object with paths for project root and triggering file. + * @param {Object} result JSON string from the CodeClimate CLI output to parse. + * @return {Object[]} Parsed issues, with following keys per oobject (array item): + * - severity: the issue severity (one of (info|warning|error)). + * - excerpt: summary of the issue. + * - description: explanation of the issue. + * - location: { file, position }. + */ +const parseIssues = (path, result) => { + let messages; + + try { + messages = JSON.parse(result); + } catch (e) { + notifyError(e, 'Invalid JSON returned from CodeClimate. See the Console for details.'); + // eslint-disable-next-line no-console + console.error('Invalid JSON returned from CodeClimate:', result); + return []; + } + + const open = fetchOpenFilepaths(); + const linterResults = []; + messages.forEach((issue) => { + // Exit early if not an issue + if (issue.type.toLowerCase() !== 'issue') return; + + // Exit early if issued file is not open + const file = join(path.project, issue.location.path); + if (!open[file]) return; + + // Exit early if duplicated issue + if (reportedPreviously(path.project, issue.fingerprint)) return; + + const position = calcRange(open[file], issue.location); + linterResults.push({ + severity: mapSeverity[issue.severity] || 'warning', + excerpt: `${issue.engine_name.toUpperCase()}: ${issue.description} [${issue.check_name}]`, + description: (issue.content && issue.content.body) ? issue.content.body : undefined, + location: { file, position }, }); - // Only notify once per path - badCommands.add(cmd); + }); + + return linterResults; +}; + +/** + * @summary Runs the CodeClimate CLI in a spawned process. + * @param {String} cwd The absolute path to the project root. + * @return {Promise|null} Promise with the output from executing the CLI. + * @todo Remove option `ignoreExitCode` after fixing https://github.com/steelbrain/exec/issues/97 + */ +const runCli = async (cwd) => { + const execOpts = { + cwd, + uniqueKey: cwd, + ignoreExitCode: true, + }; + + if (ccLinter.disableTimeout || global.waitsForPromise) { + execOpts.timeout = Infinity; + } + + // Execute the Code Climate CLI, parse the results, and emit them to the + // Linter package as warnings. The Linter package handles the styling. + try { + return await Helpers.exec(ccLinter.executablePath, execArgs, execOpts); + } catch (e) { + notifyError(e); + return null; + } +}; + +/** + * @summary Keeps track of open files and cache their project roots. + * @param {TextEditor} textEditor TextEditor instance of the file which triggered the analysis. + * @return {Promise} An object with the absolute paths to project/triggering file. + */ +const track = async (textEditor) => { + const path = { file: textEditor.getPath() }; + + // Exit early on `untitled` files (not saved into disk yet) + if (path.file === undefined) return path; + + // Fetch previously cached paths when available. + if (ccLinter.openOnTextEditor[path.file]) { + path.project = ccLinter.openOnTextEditor[path.file].project; + return path; } + + path.project = await findProjectRoot(path.file); + return path; +}; + +/** + * @summary Lints a project. + * @param {Object} path The absolute paths to project/triggering file. + * @return {Promise} An array of issues in the format that AtomLinter expects. + */ +const lintProject = async (path) => { + // Debug the command executed to run the Code Climate CLI to the console + if (atom.inDevMode()) { + // eslint-disable-next-line no-console + console.log(`${logHeader} Analyzing project @ ${path.project}`); + } + + // Start measure for how long the analysis took + measure.start(path.project); + + // Exec cc-cli and handle unique spawning (killed execs will return `null`) + const result = await runCli(path.project); + if (result === null) return null; + + const linterResults = parseIssues(path, result); + + // Log the length of time it took to run analysis + measure.end(path.project); + + reset(path.project); + return linterResults; }; -export default { +/** + * @summary Debounces the linting to join triggerings from multiple files of same project. + * @param {TextEditor} textEditor The TextEditor instance of the triggering file. + * @return {Promise} An array of issues in the format that AtomLinter expects. + */ +const debouncedLint = async (textEditor) => { + const now = Date.now(); + const path = await track(textEditor); + + // Exit early on `untitled` files (not saved into disk yet) + if (path.file === undefined) return null; + + if (linting[path.project] === undefined) { + linting[path.project] = [now]; + } else { + linting[path.project].push(now); + } + + await delay(debounceTimeout); + linting[path.project].shift(); + + // More lints for the same project have been requested and delayed. + if (linting[path.project].length > 0) return null; + + // This is the last requested lint, so analyze! + return lintProject(path); +}; + +const ccLinter = { + openOnTextEditor: {}, + activate() { // Idle callback to check version this.idleCallbacks = new Set(); @@ -102,10 +370,6 @@ export default { 'linter-codeclimate.executablePath', (value) => { this.executablePath = value; }, ), - atom.config.observe( - 'linter-codeclimate.init', - (value) => { this.init = value; }, - ), atom.config.observe( 'linter-codeclimate.disableTimeout', (value) => { this.disableTimeout = value; }, @@ -120,163 +384,14 @@ export default { }, provideLinter() { - const Helpers = require('atom-linter'); - const configurationFile = '.codeclimate.yml'; return { name: 'Code Climate', grammarScopes: ['*'], scope: 'file', lintsOnChange: false, - lint: async (textEditor) => { - const filePath = textEditor.getPath(); - const fileDir = dirname(filePath); - - // Search for a .codeclimate.yml in the project tree. If one isn't found, - // use the presence of a .git directory as the assumed project root, - // and offer to create a .codeclimate.yml file there. If the user doesn't - // want one, and says no, we won't bug them again. - const configurationFilePath = await Helpers.findAsync(fileDir, configurationFile); - if (configurationFilePath === null) { - const gitPath = await Helpers.findAsync(fileDir, '.git'); - let gitDir; - if (gitPath !== null) { - gitDir = dirname(gitPath); - } else { - // Fall back to the directory of the current file if a .git repo - // can't be found. - gitDir = dirname(filePath); - } - - if (atom.config.get('linter-codeclimate.init') !== false) { - const message = 'No .codeclimate.yml file found. Should I ' + - `initialize one for you in ${gitDir}?`; - // eslint-disable-next-line no-alert, no-restricted-globals - const initRepo = confirm(message); - if (initRepo) { - try { - await Helpers.exec( - this.executablePath, - ['init'], - { cwd: gitDir }, - ); - atom.notifications.addSuccess('init complete. Save your code ' + - 'again to run Code Climate analysis.'); - } catch (e) { - notifyError( - e, `${this.executablePath} init`, - `Unable to initialize \`.codeclimate.yml\` file in \`${gitDir}\`.`, - ); - } - } else { - atom.config.set('linter-codeclimate.init', false); - } - } - return []; - } - - // Construct the command line invocation which runs the Code Climate CLI - const relpath = atom.project.relativizePath(filePath).pop(); - const execArgs = ['analyze', '-f', 'json', relpath]; - const execOpts = { - cwd: dirname(configurationFilePath), - uniqueKey: `linter-codeclimate::${relpath}`, - }; - if (this.disableTimeout) { - execOpts.timeout = Infinity; - } - - // Debug the command executed to run the Code Climate CLI to the console - if (atom.inDevMode()) { - // eslint-disable-next-line no-console - console.log('linter-codeclimate:: Command: ' + - `\`${this.executablePath} ${execArgs.join(' ')}\``); - } - - // Start measure for how long the analysis took. - const measureId = `linter-codeclimate: \`${relpath}\` analysis`; - startMeasure(measureId); - - // Execute the Code Climate CLI, parse the results, and emit them to the - // Linter package as warnings. The Linter package handles the styling. - let result; - try { - result = await Helpers.exec(this.executablePath, execArgs, execOpts); - } catch (e) { - notifyError(e, `${this.executablePath} ${execArgs.join(' ')}`); - return null; - } - - // Handle unique spawning: killed execs will return null - if (result === null) { - return null; - } - - let messages; - try { - messages = JSON.parse(result); - } catch (e) { - notifyError( - e, `${this.executablePath} ${execArgs.join(' ')}`, - 'Invalid JSON returned from CodeClimate. See the Console for details.', - ); - // eslint-disable-next-line no-console - console.error('Invalid JSON returned from CodeClimate:', result); - return []; - } - const linterResults = []; - const fingerprints = new Set(); - let range; - Object.keys(messages).forEach((issueKey) => { - const issue = messages[issueKey]; - if (Object.prototype.hasOwnProperty.call(issue.location, 'positions')) { - const line = issue.location.positions.begin.line - 1; - let colStart; - let colEnd; - if (issue.location.positions.begin.column !== undefined) { - colStart = (issue.location.positions.begin.column - 1) || 0; - if (issue.location.positions.end.column !== undefined) { - // Valid end column, attempt to generate full range - colEnd = issue.location.positions.end.column - 1; - } - if (colEnd !== undefined && colStart !== colEnd) { - // Valid end column, and it isn't the same as the start - range = [[line, colStart], [line, colEnd]]; - } else { - // No valid end column, let generateRange highlight a word - range = Helpers.generateRange(textEditor, line, colStart); - } - } else { - // No valid starting column, just treat it as a line number - range = Helpers.generateRange(textEditor, line); - } - } else { - // Issue only has a line number - const line = issue.location.lines.begin - 1; - range = Helpers.generateRange(textEditor, line); - } - - // Avoid duplicated issues - // TODO Remove when https://github.com/phpmd/phpmd/issues/467 fixed - if (fingerprints.has(issue.fingerprint)) { - return; - } - fingerprints.add(issue.fingerprint); - - linterResults.push({ - severity: mapSeverity[issue.severity] || 'warning', - excerpt: `${issue.engine_name.toUpperCase()}: ${issue.description} [${issue.check_name}]`, - description: (issue.content && issue.content.body) ? issue.content.body : undefined, - location: { - file: filePath, - position: range, - }, - }); - }); - - // Log the length of time it took to run analysis - endMeasure(measureId); - return linterResults; - }, + lint: debouncedLint, }; }, }; + +export default ccLinter; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..5ffdb5c --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1592 @@ +{ + "name": "linter-codeclimate", + "version": "0.2.5", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "acorn": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.4.1.tgz", + "integrity": "sha1-/cWNnRf0pOmNEC3tgmqbl1kSUQI=", + "dev": true + }, + "acorn-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", + "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", + "dev": true, + "requires": { + "acorn": "3.3.0" + }, + "dependencies": { + "acorn": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", + "dev": true + } + } + }, + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "dev": true, + "requires": { + "co": "4.6.0", + "fast-deep-equal": "1.0.0", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.3.1" + } + }, + "ajv-keywords": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.1.tgz", + "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=", + "dev": true + }, + "ansi-escapes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.0.0.tgz", + "integrity": "sha1-7D6LTp+AZPwCw6ybZfHCdb2o75I=", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha1-vNZ5HqWuCXJeF+WtmIE0zUCz2RE=", + "dev": true, + "requires": { + "sprintf-js": "1.0.3" + } + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "1.0.3" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "atom-linter": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/atom-linter/-/atom-linter-10.0.0.tgz", + "integrity": "sha1-0nu3Tl+PCKdKQL6ynuGlDZdUPIk=", + "requires": { + "named-js-regexp": "1.3.3", + "sb-exec": "4.0.0", + "sb-promisify": "2.0.2", + "tmp": "0.0.33" + } + }, + "atom-package-deps": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/atom-package-deps/-/atom-package-deps-4.6.1.tgz", + "integrity": "sha1-NlWecNsl0HewBfAnjI4Xpnloats=", + "requires": { + "atom-package-path": "1.1.0", + "sb-fs": "3.0.0", + "semver": "5.5.0" + } + }, + "atom-package-path": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/atom-package-path/-/atom-package-path-1.1.0.tgz", + "integrity": "sha1-tR/tvADnyM5SI9DYA9t6P09pYU8=", + "requires": { + "sb-callsite": "1.1.2" + } + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + } + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha1-PH/L9SnYcibz0vUrlm/1Jx60Qd0=", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "caller-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", + "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", + "dev": true, + "requires": { + "callsites": "0.2.0" + } + }, + "callsites": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", + "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", + "dev": true + }, + "chalk": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", + "integrity": "sha1-Uj/iZ4rsewToBBkJKS/osXBZt5Y=", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "5.2.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha1-wVm41b4PnlpvNG2rlPFs4CIWG4g=", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "supports-color": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.2.0.tgz", + "integrity": "sha1-sNUzOxGE3TZmy+WqC0XFrHrBeko=", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + } + } + }, + "chardet": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", + "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", + "dev": true + }, + "circular-json": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", + "integrity": "sha1-gVyZ6oT2gJUp0vRXkb34JxE1LWY=", + "dev": true + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "requires": { + "restore-cursor": "2.0.0" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "dev": true + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "color-convert": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", + "integrity": "sha1-wSYRB66y8pTr/+ye2eytUppgl+0=", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "concat-stream": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", + "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.4", + "typedarray": "0.0.6" + } + }, + "consistent-env": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/consistent-env/-/consistent-env-1.3.1.tgz", + "integrity": "sha1-9oI018afxt2WVviuI0Kc4EmbZfs=", + "requires": { + "lodash.uniq": "4.5.0" + } + }, + "contains-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", + "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "4.1.1", + "shebang-command": "1.2.0", + "which": "1.3.0" + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "del": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", + "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", + "dev": true, + "requires": { + "globby": "5.0.0", + "is-path-cwd": "1.0.0", + "is-path-in-cwd": "1.0.0", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "rimraf": "2.6.2" + } + }, + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha1-XNAfwQFiG0LEzX9dGmYkNxbT850=", + "dev": true, + "requires": { + "esutils": "2.0.2" + } + }, + "error-ex": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", + "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", + "dev": true, + "requires": { + "is-arrayish": "0.2.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "eslint": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.18.1.tgz", + "integrity": "sha1-uROEQMsemLL0Sg1XjG7Pjq5hULA=", + "dev": true, + "requires": { + "ajv": "5.5.2", + "babel-code-frame": "6.26.0", + "chalk": "2.3.1", + "concat-stream": "1.6.0", + "cross-spawn": "5.1.0", + "debug": "3.1.0", + "doctrine": "2.1.0", + "eslint-scope": "3.7.1", + "eslint-visitor-keys": "1.0.0", + "espree": "3.5.3", + "esquery": "1.0.0", + "esutils": "2.0.2", + "file-entry-cache": "2.0.0", + "functional-red-black-tree": "1.0.1", + "glob": "7.1.2", + "globals": "11.3.0", + "ignore": "3.3.7", + "imurmurhash": "0.1.4", + "inquirer": "3.3.0", + "is-resolvable": "1.1.0", + "js-yaml": "3.10.0", + "json-stable-stringify-without-jsonify": "1.0.1", + "levn": "0.3.0", + "lodash": "4.17.5", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "natural-compare": "1.4.0", + "optionator": "0.8.2", + "path-is-inside": "1.0.2", + "pluralize": "7.0.0", + "progress": "2.0.0", + "require-uncached": "1.0.3", + "semver": "5.5.0", + "strip-ansi": "4.0.0", + "strip-json-comments": "2.0.1", + "table": "4.0.2", + "text-table": "0.2.0" + } + }, + "eslint-config-airbnb-base": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-12.1.0.tgz", + "integrity": "sha1-OGRB5UoSzNlXsKklZKS6/r10eUQ=", + "dev": true, + "requires": { + "eslint-restricted-globals": "0.1.1" + } + }, + "eslint-import-resolver-node": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz", + "integrity": "sha1-WPFfuDm40FdsqYBBNHaqskcttmo=", + "dev": true, + "requires": { + "debug": "2.6.9", + "resolve": "1.5.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "eslint-module-utils": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.1.1.tgz", + "integrity": "sha1-q67IJBd2E7ipWymWOeG2+s9HNEk=", + "dev": true, + "requires": { + "debug": "2.6.9", + "pkg-dir": "1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "eslint-plugin-import": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.9.0.tgz", + "integrity": "sha1-JgAu+/ylmJtyiKwEdQi9JPIXsWk=", + "dev": true, + "requires": { + "builtin-modules": "1.1.1", + "contains-path": "0.1.0", + "debug": "2.6.9", + "doctrine": "1.5.0", + "eslint-import-resolver-node": "0.3.2", + "eslint-module-utils": "2.1.1", + "has": "1.0.1", + "lodash": "4.17.5", + "minimatch": "3.0.4", + "read-pkg-up": "2.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "doctrine": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", + "dev": true, + "requires": { + "esutils": "2.0.2", + "isarray": "1.0.0" + } + } + } + }, + "eslint-restricted-globals": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/eslint-restricted-globals/-/eslint-restricted-globals-0.1.1.tgz", + "integrity": "sha1-NfDVy8ZMLj7WLpO0saevBbp+1Nc=", + "dev": true + }, + "eslint-scope": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.1.tgz", + "integrity": "sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=", + "dev": true, + "requires": { + "esrecurse": "4.2.0", + "estraverse": "4.2.0" + } + }, + "eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha1-PzGA+y4pEBdxastMnW1bXDSmqB0=", + "dev": true + }, + "espree": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.3.tgz", + "integrity": "sha1-kx4K9k5/u+0msFCinarR/GR5n6Y=", + "dev": true, + "requires": { + "acorn": "5.4.1", + "acorn-jsx": "3.0.1" + } + }, + "esprima": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", + "integrity": "sha1-RJnt3NERDgshi6zy+n9/WfVcqAQ=", + "dev": true + }, + "esquery": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.0.tgz", + "integrity": "sha1-z7qLV9f7qT8XKYqKAGoEzaE9gPo=", + "dev": true, + "requires": { + "estraverse": "4.2.0" + } + }, + "esrecurse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.0.tgz", + "integrity": "sha1-+pVo2Y04I/mkHZHpAtyrnqblsWM=", + "dev": true, + "requires": { + "estraverse": "4.2.0", + "object-assign": "4.1.1" + } + }, + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "external-editor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.1.0.tgz", + "integrity": "sha1-PQJqIbf5W1cmOH1CAKwWDTcsO0g=", + "dev": true, + "requires": { + "chardet": "0.4.2", + "iconv-lite": "0.4.19", + "tmp": "0.0.33" + } + }, + "fast-deep-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", + "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "dev": true, + "requires": { + "escape-string-regexp": "1.0.5" + } + }, + "file-entry-cache": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", + "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", + "dev": true, + "requires": { + "flat-cache": "1.3.0", + "object-assign": "4.1.1" + } + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "2.1.0", + "pinkie-promise": "2.0.1" + } + }, + "flat-cache": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.0.tgz", + "integrity": "sha1-0wMLMrOBVPTjt+nHCfSQ9++XxIE=", + "dev": true, + "requires": { + "circular-json": "0.3.3", + "del": "2.2.2", + "graceful-fs": "4.1.11", + "write": "0.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha1-pWiZ0+o8m6uHS7l3O3xe3pL0iV0=", + "dev": true + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "globals": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.3.0.tgz", + "integrity": "sha1-4E/be5eW2K2snI9kwUg3sjEzeLA=", + "dev": true + }, + "globby": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", + "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", + "dev": true, + "requires": { + "array-union": "1.0.2", + "arrify": "1.0.1", + "glob": "7.1.2", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + }, + "has": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", + "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=", + "dev": true, + "requires": { + "function-bind": "1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "hosted-git-info": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", + "integrity": "sha1-bWDjSzq7yDEwYsO3mO+NkBoHrzw=", + "dev": true + }, + "iconv-lite": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha1-90aPYBNfXl2tM5nAqBvpoWA6CCs=", + "dev": true + }, + "ignore": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.7.tgz", + "integrity": "sha1-YSKJv7PCIOGGpYEYYY1b6MG6sCE=", + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "inquirer": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz", + "integrity": "sha1-ndLyrXZdyrH/BEO0kUQqILoifck=", + "dev": true, + "requires": { + "ansi-escapes": "3.0.0", + "chalk": "2.3.1", + "cli-cursor": "2.1.0", + "cli-width": "2.2.0", + "external-editor": "2.1.0", + "figures": "2.0.0", + "lodash": "4.17.5", + "mute-stream": "0.0.7", + "run-async": "2.3.0", + "rx-lite": "4.0.8", + "rx-lite-aggregates": "4.0.8", + "string-width": "2.1.1", + "strip-ansi": "4.0.0", + "through": "2.3.8" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-builtin-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "dev": true, + "requires": { + "builtin-modules": "1.1.1" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "is-path-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", + "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", + "dev": true + }, + "is-path-in-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz", + "integrity": "sha1-ZHdYK4IU1gI0YJRWcAO+ip6sBNw=", + "dev": true, + "requires": { + "is-path-inside": "1.0.1" + } + }, + "is-path-inside": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "dev": true, + "requires": { + "path-is-inside": "1.0.2" + } + }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", + "dev": true + }, + "is-resolvable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", + "integrity": "sha1-+xj4fOH+uSUWnJpAfBkxijIG7Yg=", + "dev": true + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "jasmine-fix": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jasmine-fix/-/jasmine-fix-1.3.1.tgz", + "integrity": "sha512-jxfPMW5neQUrgEZR7FIXp1UAberYAHkpWTmdSfN/ulU+sC/yUsB827tRiwGUaUyw+1kNC5jqcINst0FF8tvVvg==", + "dev": true + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "js-yaml": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz", + "integrity": "sha1-LnhEFka9RoLpY/IrbpKCPDCcYtw=", + "dev": true, + "requires": { + "argparse": "1.0.10", + "esprima": "4.0.0" + } + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "1.1.2", + "type-check": "0.3.2" + } + }, + "load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "strip-bom": "3.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "2.0.0", + "path-exists": "3.0.0" + }, + "dependencies": { + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + } + } + }, + "lodash": { + "version": "4.17.5", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz", + "integrity": "sha1-maktZcAnLevoyWtgV7yPv6O+1RE=", + "dev": true + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" + }, + "lru-cache": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz", + "integrity": "sha1-Yi4y6CSItJJ5EUpPns9F581rulU=", + "dev": true, + "requires": { + "pseudomap": "1.0.2", + "yallist": "2.1.2" + } + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha1-ggyGo5M0ZA6ZUWkovQP8qIBX0CI=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "dev": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "dev": true + }, + "named-js-regexp": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/named-js-regexp/-/named-js-regexp-1.3.3.tgz", + "integrity": "sha1-ousWVcdMuCITpPyCd337Z7iV2Mg=" + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha1-EvlaMH1YNSB1oEkHuErIvpisAS8=", + "dev": true, + "requires": { + "hosted-git-info": "2.5.0", + "is-builtin-module": "1.0.0", + "semver": "5.5.0", + "validate-npm-package-license": "3.0.1" + } + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "dev": true, + "requires": { + "mimic-fn": "1.2.0" + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true, + "requires": { + "deep-is": "0.1.3", + "fast-levenshtein": "2.0.6", + "levn": "0.3.0", + "prelude-ls": "1.1.2", + "type-check": "0.3.2", + "wordwrap": "1.0.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + }, + "p-limit": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.2.0.tgz", + "integrity": "sha1-DpK2vty1nwIsE9DxlJ3ILRWQnxw=", + "dev": true, + "requires": { + "p-try": "1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "1.2.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "1.3.1" + } + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "2.0.1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-parse": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", + "dev": true + }, + "path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "dev": true, + "requires": { + "pify": "2.3.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "2.0.4" + } + }, + "pkg-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz", + "integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=", + "dev": true, + "requires": { + "find-up": "1.1.2" + } + }, + "pluralize": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz", + "integrity": "sha1-KYuJ34uTsCIdv0Ia0rGx6iP8Z3c=", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha1-o31zL0JxtKsa0HDTVQjoKQeI/6o=", + "dev": true + }, + "progress": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz", + "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=", + "dev": true + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "dev": true, + "requires": { + "load-json-file": "2.0.0", + "normalize-package-data": "2.4.0", + "path-type": "2.0.0" + } + }, + "read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "dev": true, + "requires": { + "find-up": "2.1.0", + "read-pkg": "2.0.0" + }, + "dependencies": { + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "2.0.0" + } + } + } + }, + "readable-stream": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.4.tgz", + "integrity": "sha1-yUbD9H+n2Oq8C2FQ9KEvaaRXQHE=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + } + }, + "require-uncached": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", + "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", + "dev": true, + "requires": { + "caller-path": "0.1.0", + "resolve-from": "1.0.1" + } + }, + "resolve": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz", + "integrity": "sha1-HwmsznlsmnYlefMbLBzEw83fnzY=", + "dev": true, + "requires": { + "path-parse": "1.0.5" + } + }, + "resolve-from": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", + "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", + "dev": true + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "dev": true, + "requires": { + "onetime": "2.0.1", + "signal-exit": "3.0.2" + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha1-LtgVDSShbqhlHm1u8PR8QVjOejY=", + "dev": true, + "requires": { + "glob": "7.1.2" + } + }, + "run-async": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", + "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "dev": true, + "requires": { + "is-promise": "2.1.0" + } + }, + "rx-lite": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz", + "integrity": "sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=", + "dev": true + }, + "rx-lite-aggregates": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz", + "integrity": "sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74=", + "dev": true, + "requires": { + "rx-lite": "4.0.8" + } + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha1-iTMSr2myEj3vcfV4iQAWce6yyFM=", + "dev": true + }, + "sb-callsite": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sb-callsite/-/sb-callsite-1.1.2.tgz", + "integrity": "sha1-KBkftm1k46PukghKlakPy1ECJDs=" + }, + "sb-exec": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/sb-exec/-/sb-exec-4.0.0.tgz", + "integrity": "sha1-RnR/DfFiYmwW6/D+pCJFrRqoWco=", + "requires": { + "consistent-env": "1.3.1", + "lodash.uniq": "4.5.0", + "sb-npm-path": "2.0.0" + } + }, + "sb-fs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/sb-fs/-/sb-fs-3.0.0.tgz", + "integrity": "sha1-+9zdMBDoChuOJ0kM7zNgZJdCA7g=", + "requires": { + "sb-promisify": "2.0.2", + "strip-bom-buf": "1.0.0" + } + }, + "sb-memoize": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/sb-memoize/-/sb-memoize-1.0.2.tgz", + "integrity": "sha1-EoN1xi3bnMT/qQXQxaWXwZuurY4=" + }, + "sb-npm-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sb-npm-path/-/sb-npm-path-2.0.0.tgz", + "integrity": "sha1-D2zCzzcd68p9k27Xa31MPMHrPVg=", + "requires": { + "sb-memoize": "1.0.2", + "sb-promisify": "2.0.2" + } + }, + "sb-promisify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/sb-promisify/-/sb-promisify-2.0.2.tgz", + "integrity": "sha1-QnelR1RIiqlnXYhuNU24lMm9yYE=" + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha1-3Eu8emyp2Rbe5dQ1FvAJK1j3uKs=" + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "slice-ansi": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz", + "integrity": "sha1-BE8aSdiEL/MHqta1Be0Xi9lQE00=", + "dev": true, + "requires": { + "is-fullwidth-code-point": "2.0.0" + } + }, + "spdx-correct": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz", + "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=", + "dev": true, + "requires": { + "spdx-license-ids": "1.2.2" + } + }, + "spdx-expression-parse": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz", + "integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=", + "dev": true + }, + "spdx-license-ids": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz", + "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=", + "dev": true + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha1-q5Pyeo3BPSjKyBXEYhQ6bZASrp4=", + "dev": true, + "requires": { + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "4.0.0" + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha1-D8Z9fBQYJd6UKC3VNr7GubzoYKs=", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "3.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + } + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-bom-buf": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-buf/-/strip-bom-buf-1.0.0.tgz", + "integrity": "sha1-HLRar1dTD0yvhsf3UXnSyaUd1XI=", + "requires": { + "is-utf8": "0.2.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "table": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/table/-/table-4.0.2.tgz", + "integrity": "sha1-ozRHN1OR52atNNNIbm4q7chNLjY=", + "dev": true, + "requires": { + "ajv": "5.5.2", + "ajv-keywords": "2.1.1", + "chalk": "2.3.1", + "lodash": "4.17.5", + "slice-ansi": "1.0.0", + "string-width": "2.1.1" + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha1-bTQzWIl2jSGyvNoKonfO07G/rfk=", + "requires": { + "os-tmpdir": "1.0.2" + } + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "1.1.2" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz", + "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=", + "dev": true, + "requires": { + "spdx-correct": "1.0.2", + "spdx-expression-parse": "1.0.4" + } + }, + "which": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", + "integrity": "sha1-/wS9/AEO5UfXgL7DjhrBwnd9JTo=", + "dev": true, + "requires": { + "isexe": "2.0.0" + } + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", + "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", + "dev": true, + "requires": { + "mkdirp": "0.5.1" + } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + } + } +} diff --git a/package.json b/package.json index bfb45e9..9879a21 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,8 @@ "devDependencies": { "eslint": "^4.6.0", "eslint-config-airbnb-base": "^12.0.0", - "eslint-plugin-import": "^2.7.0" + "eslint-plugin-import": "^2.7.0", + "jasmine-fix": "^1.3.1" }, "scripts": { "lint": "eslint .", diff --git a/spec/fixtures/.codeclimate.yml b/spec/fixtures/.codeclimate.yml index 94af96d..c0495a0 100644 --- a/spec/fixtures/.codeclimate.yml +++ b/spec/fixtures/.codeclimate.yml @@ -4,8 +4,5 @@ engines: enabled: true fixme: enabled: true -ratings: - paths: - - "**.coffee" exclude_paths: - node_modules/**/* diff --git a/spec/linter-codeclimate-spec.js b/spec/linter-codeclimate-spec.js index 1f597a0..1fef539 100644 --- a/spec/linter-codeclimate-spec.js +++ b/spec/linter-codeclimate-spec.js @@ -1,40 +1,40 @@ 'use babel'; +// eslint-disable-next-line no-unused-vars +import { it, fit, wait, beforeEach, afterEach } from 'jasmine-fix'; import { join } from 'path'; const fixturesPath = join(__dirname, 'fixtures'); const coolCodePath = join(fixturesPath, 'cool_code.rb'); -const TIMEOUT = process.env.CI ? 60000 : 10000; -describe('The codeclimate provider for Linter', () => { - const { lint } = require('../lib/index.js').provideLinter(); +const { lint } = require('../lib/index.js').provideLinter(); - beforeEach(() => { - atom.workspace.destroyActivePaneItem(); +// Codeclimate can sometimes be quite slow (especially in a CI environment) +jasmine.getEnv().defaultTimeoutInterval = 5 * 60 * 1000; // 5 minutes - waitsForPromise(() => - Promise.all([ - atom.packages.activatePackage('linter-codeclimate'), - ])); +describe('The codeclimate provider for Linter', () => { + beforeEach(async () => { + atom.workspace.destroyActivePaneItem(); + await atom.packages.activatePackage('linter-codeclimate'); }); - it('works with a valid .codeclimate.yml file', () => - waitsForPromise( - { timeout: TIMEOUT }, - () => - atom.workspace.open(coolCodePath).then(editor => lint(editor)).then((messages) => { - expect(messages[0].severity).toBe('warning'); - expect(messages[0].excerpt).toBe('RUBOCOP: Unused method argument - ' + - "`bar`. If it's necessary, use `_` or `_bar` as an argument name to " + - "indicate that it won't be used. You can also write as `foo(*)` if " + - "you want the method to accept any arguments but don't care about " + - 'them. [Rubocop/Lint/UnusedMethodArgument]'); - expect(messages[0].description).toBeDefined(); - expect(messages[0].reference).not.toBeDefined(); - expect(messages[0].icon).not.toBeDefined(); - expect(messages[0].solutions).not.toBeDefined(); - expect(messages[0].location.file).toBe(coolCodePath); - expect(messages[0].location.position).toEqual([[1, 11], [1, 14]]); - }), - )); + it('works with a valid .codeclimate.yml file', async () => { + const editor = await atom.workspace.open(coolCodePath); + const messages = await lint(editor); + + const issueExcerpt = "RUBOCOP: Unused method argument - `bar`. If it's necessary," + + " use `_` or `_bar` as an argument name to indicate that it won't be used." + + ' You can also write as `foo(*)` if you want the method to accept any' + + " arguments but don't care about them. [Rubocop/Lint/UnusedMethodArgument]"; + const msgIndex = messages.map(msg => msg.excerpt).indexOf(issueExcerpt); + const msg = messages[msgIndex]; + expect(msg.severity).toBe('warning'); + expect(msg.excerpt).toBe(issueExcerpt); + expect(msg.description).toBeDefined(); + expect(msg.reference).not.toBeDefined(); + expect(msg.icon).not.toBeDefined(); + expect(msg.solutions).not.toBeDefined(); + expect(msg.location.file).toBe(coolCodePath); + expect(msg.location.position).toEqual([[1, 11], [1, 14]]); + }); }); From d237b5dd6a0c8277f53a297d73f0a0ddbe074c9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Garc=C3=ADa?= Date: Mon, 16 Apr 2018 10:20:16 +0200 Subject: [PATCH 02/11] Move `configurationFile` to `findProjectRoot` --- lib/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/index.js b/lib/index.js index b5410fe..9cf66b6 100644 --- a/lib/index.js +++ b/lib/index.js @@ -7,7 +7,6 @@ import { CompositeDisposable } from 'atom'; import { dirname, join } from 'path'; import * as Helpers from 'atom-linter'; -const configurationFile = '.codeclimate.yml'; const execArgs = ['analyze', '-f', 'json']; const logHeader = 'linter-codeclimate::'; const linting = {}; @@ -138,7 +137,7 @@ const reportedPreviously = (projectRoot, fingerprint) => { */ const findProjectRoot = async (filePath) => { const fileDir = dirname(filePath); - const configurationFilePath = await Helpers.findAsync(fileDir, configurationFile); + const configurationFilePath = await Helpers.findAsync(fileDir, '.codeclimate.yml'); if (configurationFilePath !== null) { return dirname(configurationFilePath); From 90a2d0b1fc394b8b9bdf30331baafa58279d08d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Garc=C3=ADa?= Date: Mon, 16 Apr 2018 10:34:26 +0200 Subject: [PATCH 03/11] Use `atom.inSpecMode()` instead of `global.waitsForPromise` --- lib/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/index.js b/lib/index.js index 9cf66b6..b84fe43 100644 --- a/lib/index.js +++ b/lib/index.js @@ -256,7 +256,7 @@ const runCli = async (cwd) => { ignoreExitCode: true, }; - if (ccLinter.disableTimeout || global.waitsForPromise) { + if (ccLinter.disableTimeout || atom.inSpecMode()) { execOpts.timeout = Infinity; } From db5d229e7f72cc45fb4d9eca06362579c7aea0d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Garc=C3=ADa?= Date: Mon, 16 Apr 2018 11:28:20 +0200 Subject: [PATCH 04/11] Make `runCli()` a method of `ccLinter` --- lib/index.js | 56 ++++++++++++++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/lib/index.js b/lib/index.js index b84fe43..d495bc8 100644 --- a/lib/index.js +++ b/lib/index.js @@ -243,33 +243,6 @@ const parseIssues = (path, result) => { return linterResults; }; -/** - * @summary Runs the CodeClimate CLI in a spawned process. - * @param {String} cwd The absolute path to the project root. - * @return {Promise|null} Promise with the output from executing the CLI. - * @todo Remove option `ignoreExitCode` after fixing https://github.com/steelbrain/exec/issues/97 - */ -const runCli = async (cwd) => { - const execOpts = { - cwd, - uniqueKey: cwd, - ignoreExitCode: true, - }; - - if (ccLinter.disableTimeout || atom.inSpecMode()) { - execOpts.timeout = Infinity; - } - - // Execute the Code Climate CLI, parse the results, and emit them to the - // Linter package as warnings. The Linter package handles the styling. - try { - return await Helpers.exec(ccLinter.executablePath, execArgs, execOpts); - } catch (e) { - notifyError(e); - return null; - } -}; - /** * @summary Keeps track of open files and cache their project roots. * @param {TextEditor} textEditor TextEditor instance of the file which triggered the analysis. @@ -307,7 +280,7 @@ const lintProject = async (path) => { measure.start(path.project); // Exec cc-cli and handle unique spawning (killed execs will return `null`) - const result = await runCli(path.project); + const result = await ccLinter.runCli(path.project); if (result === null) return null; const linterResults = parseIssues(path, result); @@ -391,6 +364,33 @@ const ccLinter = { lint: debouncedLint, }; }, + + /** + * @summary Runs the CodeClimate CLI in a spawned process. + * @param {String} cwd The absolute path to the project root. + * @return {Promise|null} Promise with the output from executing the CLI. + * @todo Remove option `ignoreExitCode` after fixing https://github.com/steelbrain/exec/issues/97 + */ + async runCli(cwd) { + const execOpts = { + cwd, + uniqueKey: cwd, + ignoreExitCode: true, + }; + + if (this.disableTimeout || atom.inSpecMode()) { + execOpts.timeout = Infinity; + } + + // Execute the Code Climate CLI, parse the results, and emit them to the + // Linter package as warnings. The Linter package handles the styling. + try { + return await Helpers.exec(ccLinter.executablePath, execArgs, execOpts); + } catch (e) { + notifyError(e); + return null; + } + }, }; export default ccLinter; From 1fbe397a263fffcadd034ca895c593b7438bb492 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Garc=C3=ADa?= Date: Mon, 16 Apr 2018 11:33:24 +0200 Subject: [PATCH 05/11] Change linter scope to project --- lib/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/index.js b/lib/index.js index d495bc8..71dbcf9 100644 --- a/lib/index.js +++ b/lib/index.js @@ -359,7 +359,7 @@ const ccLinter = { return { name: 'Code Climate', grammarScopes: ['*'], - scope: 'file', + scope: 'project', lintsOnChange: false, lint: debouncedLint, }; From 0f6f3ff05ff88cae46ef25d5fb072c5460a6cf80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Garc=C3=ADa?= Date: Mon, 16 Apr 2018 11:34:36 +0200 Subject: [PATCH 06/11] Declare `mapSeverity` inside `parseIssues()` --- lib/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/index.js b/lib/index.js index 71dbcf9..4387954 100644 --- a/lib/index.js +++ b/lib/index.js @@ -12,10 +12,6 @@ const logHeader = 'linter-codeclimate::'; const linting = {}; const fingerprints = {}; const debounceTimeout = 250; -const mapSeverity = { - major: 'error', - minor: 'warning', -}; const notificationDefaults = { buttons: [{ className: 'btn-install', @@ -232,6 +228,10 @@ const parseIssues = (path, result) => { if (reportedPreviously(path.project, issue.fingerprint)) return; const position = calcRange(open[file], issue.location); + const mapSeverity = { + major: 'error', + minor: 'warning', + }; linterResults.push({ severity: mapSeverity[issue.severity] || 'warning', excerpt: `${issue.engine_name.toUpperCase()}: ${issue.description} [${issue.check_name}]`, From 3a3456510caea1365a0290a479c0b016086d6996 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Garc=C3=ADa?= Date: Mon, 16 Apr 2018 11:41:35 +0200 Subject: [PATCH 07/11] Add provider name to unique exec key --- lib/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/index.js b/lib/index.js index 4387954..e68cf00 100644 --- a/lib/index.js +++ b/lib/index.js @@ -374,7 +374,7 @@ const ccLinter = { async runCli(cwd) { const execOpts = { cwd, - uniqueKey: cwd, + uniqueKey: `linter-codeclimate::${cwd}`, ignoreExitCode: true, }; From aca2557d706139fd99595882aa2143626e677a90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Garc=C3=ADa?= Date: Mon, 16 Apr 2018 12:04:23 +0200 Subject: [PATCH 08/11] Declare `execArgs` inside `ccLinter.runCli()` --- lib/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/index.js b/lib/index.js index e68cf00..32ec854 100644 --- a/lib/index.js +++ b/lib/index.js @@ -7,7 +7,6 @@ import { CompositeDisposable } from 'atom'; import { dirname, join } from 'path'; import * as Helpers from 'atom-linter'; -const execArgs = ['analyze', '-f', 'json']; const logHeader = 'linter-codeclimate::'; const linting = {}; const fingerprints = {}; @@ -372,6 +371,7 @@ const ccLinter = { * @todo Remove option `ignoreExitCode` after fixing https://github.com/steelbrain/exec/issues/97 */ async runCli(cwd) { + const execArgs = ['analyze', '-f', 'json']; const execOpts = { cwd, uniqueKey: `linter-codeclimate::${cwd}`, From fea0e9c9f3e056c8c399c35d8f8d54de2ac28fa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Garc=C3=ADa?= Date: Mon, 16 Apr 2018 12:05:18 +0200 Subject: [PATCH 09/11] Declare `notificationDefaults` inside `notifyError()` --- lib/index.js | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/lib/index.js b/lib/index.js index 32ec854..656ddb9 100644 --- a/lib/index.js +++ b/lib/index.js @@ -11,17 +11,6 @@ const logHeader = 'linter-codeclimate::'; const linting = {}; const fingerprints = {}; const debounceTimeout = 250; -const notificationDefaults = { - buttons: [{ - className: 'btn-install', - onDidClick: () => { - // eslint-disable-next-line import/no-extraneous-dependencies - require('shell').openExternal('https://github.com/codeclimate/codeclimate'); - }, - text: 'Install guide', - }], - dismissable: true, -}; /** * @summary Promisify a delay (timeout). @@ -99,6 +88,18 @@ const notifyError = (err, description = '') => { } } + const notificationDefaults = { + buttons: [{ + className: 'btn-install', + onDidClick: () => { + // eslint-disable-next-line import/no-extraneous-dependencies + require('shell').openExternal('https://github.com/codeclimate/codeclimate'); + }, + text: 'Install guide', + }], + dismissable: true, + }; + const options = Object.assign(notificationDefaults, { description: `${description}\n${friendlyDesc}`.trim(), detail, From fce512a035a651be6776fb050385cf0d21ec4c22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Garc=C3=ADa?= Date: Mon, 16 Apr 2018 12:07:47 +0200 Subject: [PATCH 10/11] Make `track()` a method of `ccLinter` --- lib/index.js | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/lib/index.js b/lib/index.js index 656ddb9..ded74de 100644 --- a/lib/index.js +++ b/lib/index.js @@ -243,27 +243,6 @@ const parseIssues = (path, result) => { return linterResults; }; -/** - * @summary Keeps track of open files and cache their project roots. - * @param {TextEditor} textEditor TextEditor instance of the file which triggered the analysis. - * @return {Promise} An object with the absolute paths to project/triggering file. - */ -const track = async (textEditor) => { - const path = { file: textEditor.getPath() }; - - // Exit early on `untitled` files (not saved into disk yet) - if (path.file === undefined) return path; - - // Fetch previously cached paths when available. - if (ccLinter.openOnTextEditor[path.file]) { - path.project = ccLinter.openOnTextEditor[path.file].project; - return path; - } - - path.project = await findProjectRoot(path.file); - return path; -}; - /** * @summary Lints a project. * @param {Object} path The absolute paths to project/triggering file. @@ -365,6 +344,27 @@ const ccLinter = { }; }, + /** + * @summary Keeps track of open files and cache their project roots. + * @param {TextEditor} textEditor TextEditor instance of the file which triggered the analysis. + * @return {Promise} An object with the absolute paths to project/triggering file. + */ + async track(textEditor) { + const path = { file: textEditor.getPath() }; + + // Exit early on `untitled` files (not saved into disk yet) + if (path.file === undefined) return path; + + // Fetch previously cached paths when available. + if (this.openOnTextEditor[path.file]) { + path.project = this.openOnTextEditor[path.file].project; + return path; + } + + path.project = await findProjectRoot(path.file); + return path; + }, + /** * @summary Runs the CodeClimate CLI in a spawned process. * @param {String} cwd The absolute path to the project root. From 7c028a4555874c038ad7083920f59fcaaca861f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Garc=C3=ADa?= Date: Mon, 16 Apr 2018 18:38:59 +0200 Subject: [PATCH 11/11] Move functions into linter object --- lib/index.js | 401 +++++++++++++++++++++++++++------------------------ 1 file changed, 214 insertions(+), 187 deletions(-) diff --git a/lib/index.js b/lib/index.js index ded74de..35a7858 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,16 +1,15 @@ 'use babel'; -/* eslint no-use-before-define:0 */ - // eslint-disable-next-line import/extensions, import/no-extraneous-dependencies import { CompositeDisposable } from 'atom'; import { dirname, join } from 'path'; import * as Helpers from 'atom-linter'; -const logHeader = 'linter-codeclimate::'; -const linting = {}; -const fingerprints = {}; -const debounceTimeout = 250; +const devLog = (msg) => { + if (!atom.inDevMode()) return; + // eslint-disable-next-line no-console + console.log(`linter-codeclimate:: ${msg}`); +}; /** * @summary Promisify a delay (timeout). @@ -19,15 +18,6 @@ const debounceTimeout = 250; */ const delay = async ms => new Promise(resolve => setTimeout(resolve, ms)); -/** - * @summary Resets the flags for project at `projectRoot`. - * @param {String} projectRoot The absolute path to the project root. - */ -const reset = (projectRoot) => { - delete fingerprints[projectRoot]; - delete linting[projectRoot]; -}; - const measure = { start(cwd) { if (!atom.inDevMode()) return; @@ -47,11 +37,14 @@ const measure = { }; performance.mark(mark.end); performance.measure(cwd, mark.start, mark.end); + devLog(`Analysis for ${cwd} took: ${performance.getEntriesByName(cwd)[0].duration.toFixed(2)}`); + /* // eslint-disable-next-line no-console console.log( `${logHeader} Analysis for ${cwd} took:`, performance.getEntriesByName(cwd)[0].duration.toFixed(2), ); + */ performance.clearMeasures(cwd); performance.clearMarks(mark.start); performance.clearMarks(mark.end); @@ -73,34 +66,36 @@ const notifyError = (err, description = '') => { detail += `\n- MESSAGE: ${err.message}`; } + const binErrorDefaults = { + buttons: [{ + className: 'btn-install', + onDidClick: () => { + // eslint-disable-next-line import/no-extraneous-dependencies + require('shell').openExternal('https://github.com/codeclimate/codeclimate'); + }, + text: 'Install guide', + }], + dismissable: true, + }; + let defaults = {}; if (err.code) { detail += `\n- CODE: ${err.code}`; switch (err.code) { case 'ENOENT': friendlyDesc = 'CodeClimate binary could not be found.'; + defaults = binErrorDefaults; break; case 'EACCES': case 'EDIR': friendlyDesc = 'Executable path not pointing to a binary.'; + defaults = binErrorDefaults; break; default: friendlyDesc = 'CodeClimate execution failed.'; } } - const notificationDefaults = { - buttons: [{ - className: 'btn-install', - onDidClick: () => { - // eslint-disable-next-line import/no-extraneous-dependencies - require('shell').openExternal('https://github.com/codeclimate/codeclimate'); - }, - text: 'Install guide', - }], - dismissable: true, - }; - - const options = Object.assign(notificationDefaults, { + const options = Object.assign(defaults, { description: `${description}\n${friendlyDesc}`.trim(), detail, stack: err.stack, @@ -108,22 +103,6 @@ const notifyError = (err, description = '') => { atom.notifications.addError('linter-codeclimate error', options); }; -/** - * @summary Checks if the reported issue has been reported previously (duplicated). - * @return {Boolean} Whether the issue is duplicated (`true`) or not (`false`). - * @todo Remove after fixing https://github.com/phpmd/phpmd/issues/467 - */ -const reportedPreviously = (projectRoot, fingerprint) => { - if (!Object.prototype.hasOwnProperty.call(fingerprints, projectRoot)) { - fingerprints[projectRoot] = new Set(); - } - - if (fingerprints[projectRoot].has(fingerprint)) return true; - - fingerprints[projectRoot].add(fingerprint); - return false; -}; - /** * Search for a CodeClimate config file in the project tree. If none found, * use the presence of a `.git` directory as the assumed project root. @@ -145,11 +124,34 @@ const findProjectRoot = async (filePath) => { }; /** - * Returns the range (lines/columns) for a given issue from its location. - * - * @param {TextEditor} textEditor The Atom TextEditor instance. - * @param {Object} location The location object of the CodeClimate issue. - * @return {Array[]} The range: `[[lineNumber, colStart], [lineNumber, colEnd]]`. + * @summary Estimates the range for a non-open file. + * @param {Object} location The location object of the CodeClimate issue. + * @return {Array[]} The range: `[[lineNumber, colStart], [lineNumber, colEnd]]`. + */ +const estimateRange = (location) => { + if (Object.prototype.hasOwnProperty.call(location, 'lines')) { + return [ + [location.lines.begin - 1, 0], + [location.lines.end - 1, 0], + ]; + } + + if (Object.prototype.hasOwnProperty.call(location, 'positions')) { + const { begin, end } = location.positions; + return [ + [begin.line - 1, begin.column - 1], + [end.line - 1, end.column - 1], + ]; + } + + return [[0, 0], [0, 0]]; +}; + +/** + * @summary Returns the range (lines/columns) for a given issue from its location. + * @param {TextEditor} textEditor The Atom TextEditor instance. + * @param {Object} location The location object of the CodeClimate issue. + * @return {Array[]} The range: `[[lineNumber, colStart], [lineNumber, colEnd]]`. */ const calcRange = (textEditor, location) => { // Issue only has a line number @@ -165,11 +167,11 @@ const calcRange = (textEditor, location) => { return Helpers.generateRange(textEditor, line); } - const colStart = (positions.begin.column - 1) || 0; + const colStart = positions.begin.column - 1; const colEnd = (positions.end.column === undefined) ? undefined : (positions.end.column - 1); - // No valid end column, let generateRange highlight a word + // No valid end column, let `generateRange()` highlight a word if (colEnd === undefined || colStart === colEnd) { return Helpers.generateRange(textEditor, line, colStart); } @@ -178,129 +180,11 @@ const calcRange = (textEditor, location) => { return [[line, colStart], [line, colEnd]]; }; -/** - * @summary Fetch the paths of all currently open files on Atom. - * @return {Object} Dictionary of open file abspaths ~> textEditor. - * - * NOTE Files indexed by abspath to avoid relative filepath collision among different projects. - */ -const fetchOpenFilepaths = () => { - const openFiles = {}; - atom.workspace.textEditorRegistry.editors.forEach((textEditor) => { - openFiles[textEditor.getPath()] = textEditor; - }); - return openFiles; -}; - -/** - * @summary Parses the issues reported by CodeClimate CLI to the format AtomLinter expects. - * @param {Object} path Object with paths for project root and triggering file. - * @param {Object} result JSON string from the CodeClimate CLI output to parse. - * @return {Object[]} Parsed issues, with following keys per oobject (array item): - * - severity: the issue severity (one of (info|warning|error)). - * - excerpt: summary of the issue. - * - description: explanation of the issue. - * - location: { file, position }. - */ -const parseIssues = (path, result) => { - let messages; - - try { - messages = JSON.parse(result); - } catch (e) { - notifyError(e, 'Invalid JSON returned from CodeClimate. See the Console for details.'); - // eslint-disable-next-line no-console - console.error('Invalid JSON returned from CodeClimate:', result); - return []; - } - - const open = fetchOpenFilepaths(); - const linterResults = []; - messages.forEach((issue) => { - // Exit early if not an issue - if (issue.type.toLowerCase() !== 'issue') return; - - // Exit early if issued file is not open - const file = join(path.project, issue.location.path); - if (!open[file]) return; - - // Exit early if duplicated issue - if (reportedPreviously(path.project, issue.fingerprint)) return; - - const position = calcRange(open[file], issue.location); - const mapSeverity = { - major: 'error', - minor: 'warning', - }; - linterResults.push({ - severity: mapSeverity[issue.severity] || 'warning', - excerpt: `${issue.engine_name.toUpperCase()}: ${issue.description} [${issue.check_name}]`, - description: (issue.content && issue.content.body) ? issue.content.body : undefined, - location: { file, position }, - }); - }); - - return linterResults; -}; - -/** - * @summary Lints a project. - * @param {Object} path The absolute paths to project/triggering file. - * @return {Promise} An array of issues in the format that AtomLinter expects. - */ -const lintProject = async (path) => { - // Debug the command executed to run the Code Climate CLI to the console - if (atom.inDevMode()) { - // eslint-disable-next-line no-console - console.log(`${logHeader} Analyzing project @ ${path.project}`); - } - - // Start measure for how long the analysis took - measure.start(path.project); - - // Exec cc-cli and handle unique spawning (killed execs will return `null`) - const result = await ccLinter.runCli(path.project); - if (result === null) return null; - - const linterResults = parseIssues(path, result); - - // Log the length of time it took to run analysis - measure.end(path.project); - - reset(path.project); - return linterResults; -}; - -/** - * @summary Debounces the linting to join triggerings from multiple files of same project. - * @param {TextEditor} textEditor The TextEditor instance of the triggering file. - * @return {Promise} An array of issues in the format that AtomLinter expects. - */ -const debouncedLint = async (textEditor) => { - const now = Date.now(); - const path = await track(textEditor); - - // Exit early on `untitled` files (not saved into disk yet) - if (path.file === undefined) return null; - - if (linting[path.project] === undefined) { - linting[path.project] = [now]; - } else { - linting[path.project].push(now); - } - - await delay(debounceTimeout); - linting[path.project].shift(); - - // More lints for the same project have been requested and delayed. - if (linting[path.project].length > 0) return null; - - // This is the last requested lint, so analyze! - return lintProject(path); -}; const ccLinter = { - openOnTextEditor: {}, + cache: {}, + fingerprints: {}, + linting: {}, activate() { // Idle callback to check version @@ -325,6 +209,7 @@ const ccLinter = { 'linter-codeclimate.disableTimeout', (value) => { this.disableTimeout = value; }, ), + atom.workspace.observeTextEditors(textEditor => this.cacheEditor(textEditor)), ); }, @@ -340,29 +225,94 @@ const ccLinter = { grammarScopes: ['*'], scope: 'project', lintsOnChange: false, - lint: debouncedLint, + lint: async textEditor => this.debouncedLint(textEditor), }; }, /** - * @summary Keeps track of open files and cache their project roots. - * @param {TextEditor} textEditor TextEditor instance of the file which triggered the analysis. - * @return {Promise} An object with the absolute paths to project/triggering file. + * @summary Debounces the linting to join triggerings from multiple files of same project. + * @param {TextEditor} textEditor The TextEditor instance of the triggering file. + * @return {Promise} An array of issues in the format that AtomLinter expects. */ - async track(textEditor) { - const path = { file: textEditor.getPath() }; + async debouncedLint(textEditor) { + const path = textEditor.getPath(); // Exit early on `untitled` files (not saved into disk yet) - if (path.file === undefined) return path; + if (path === undefined) return null; - // Fetch previously cached paths when available. - if (this.openOnTextEditor[path.file]) { - path.project = this.openOnTextEditor[path.file].project; - return path; + if (!this.cache[path]) { + // Beware with race condition: textEditor observer and linter fired simultaneously + await this.cacheEditor(textEditor); + } + const { project } = this.cache[path]; + const now = Date.now(); + if (this.linting[project] === undefined) { + this.linting[project] = [now]; + } else { + this.linting[project].push(now); } - path.project = await findProjectRoot(path.file); - return path; + await delay(250); + this.linting[project].shift(); + + // More lints for the same project have been requested and delayed. + if (this.linting[project].length > 0) return null; + + // This is the last requested lint, so analyze! + return this.lintProject(project); + }, + + /** + * @summary Lints a project. + * @param {String} path The absolute path to the project to analyze. + * @return {Promise} An array of issues in the format that AtomLinter expects. + */ + async lintProject(path) { + // Debug the command executed to run the Code Climate CLI to the console + devLog(`Analyzing project @ ${path}`); + + // Start measure for how long the analysis took + measure.start(path); + + // Exec cc-cli and handle unique spawning (killed execs will return `null`) + const result = await this.runCli(path); + if (result === null) return null; + + const linterResults = this.parseIssues(path, result); + + // Log the length of time it took to run analysis + measure.end(path); + + this.reset(path); + return linterResults; + }, + + /** + * @summary Cache and keeps track of open textEditors and cache its file/project paths. + * @param {TextEditor} textEditor TextEditor instance of the file which triggered the analysis. + */ + async cacheEditor(textEditor) { + const path = textEditor.getPath(); + + if (this.cache[path]) return; + + textEditor.onDidDestroy(() => delete this.cache[path]); + textEditor.onDidChangePath((newPath) => { + const cached = this.cache[path]; + delete this.cache[path]; + cached.path = newPath; + this.cache[newPath] = cached; + }); + + this.cache[path] = { + editor: textEditor, + file: path, + project: await findProjectRoot(path), + }; + }, + + findTextEditor(filepath) { + return this.cache[filepath] && this.cache[filepath].editor; }, /** @@ -375,8 +325,8 @@ const ccLinter = { const execArgs = ['analyze', '-f', 'json']; const execOpts = { cwd, - uniqueKey: `linter-codeclimate::${cwd}`, ignoreExitCode: true, + uniqueKey: `linter-codeclimate::${cwd}`, }; if (this.disableTimeout || atom.inSpecMode()) { @@ -386,12 +336,89 @@ const ccLinter = { // Execute the Code Climate CLI, parse the results, and emit them to the // Linter package as warnings. The Linter package handles the styling. try { - return await Helpers.exec(ccLinter.executablePath, execArgs, execOpts); + return await Helpers.exec(this.executablePath, execArgs, execOpts); } catch (e) { notifyError(e); return null; } }, + + /** + * @summary Parses the issues reported by CodeClimate CLI to the format AtomLinter expects. + * @param {String} project The absolute path to the project to analyze. + * @param {Object} result JSON string from the CodeClimate CLI output to parse. + * @return {Object[]} Parsed issues, with following keys per object (array item): + * - description: explanation of the issue. + * - excerpt: summary of the issue. + * - location: { file, position }. + * - severity: the issue severity (one of (info|warning|error)). + */ + parseIssues(project, result) { + let messages; + + try { + messages = JSON.parse(result); + } catch (e) { + notifyError(e, 'Invalid JSON returned from CodeClimate. See the Console for details.'); + // eslint-disable-next-line no-console + console.error('Invalid JSON returned from CodeClimate:', result); + return []; + } + + const linterResults = []; + messages.forEach((issue) => { + // Exit early if not an issue + if (issue.type.toLowerCase() !== 'issue') return; + + // Exit early if duplicated issue + if (this.reportedPreviously(project, issue.fingerprint)) return; + + const file = join(project, issue.location.path); + const textEditor = this.findTextEditor(file); + const position = textEditor + ? calcRange(textEditor, issue.location) + : estimateRange(issue.location); + const mapSeverity = { + major: 'error', + minor: 'warning', + }; + linterResults.push({ + severity: mapSeverity[issue.severity] || 'warning', + excerpt: `${issue.engine_name.toUpperCase()}: ${issue.description} [${issue.check_name}]`, + description: (issue.content && issue.content.body) ? issue.content.body : undefined, + location: { file, position }, + }); + }); + + return linterResults; + }, + + /** + * @summary Checks if the reported issue has been reported previously (duplicated). + * @param {String} projectRoot The project root. + * @param {} + * @return {Boolean} Whether the issue is duplicated (`true`) or not (`false`). + * @todo Remove after fixing https://github.com/phpmd/phpmd/issues/467 + */ + reportedPreviously(projectRoot, fingerprint) { + if (!Object.prototype.hasOwnProperty.call(this.fingerprints, projectRoot)) { + this.fingerprints[projectRoot] = new Set(); + } + + if (this.fingerprints[projectRoot].has(fingerprint)) return true; + + this.fingerprints[projectRoot].add(fingerprint); + return false; + }, + + /** + * @summary Resets the flags for project at `projectRoot`. + * @param {String} projectRoot The absolute path to the project root. + */ + reset(projectRoot) { + delete this.fingerprints[projectRoot]; + delete this.linting[projectRoot]; + }, }; export default ccLinter;