From 8a68fbb00633ae1a94576b6cb5c206250d6c6d32 Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Sat, 20 Aug 2016 15:50:20 -0400 Subject: [PATCH] support linting JS with alt-JS dependencies via `import/parsers` setting! (closes #407) --- CHANGELOG.md | 4 ++++ README.md | 26 +++++++++++++++++++++++ package.json | 6 ++++-- src/core/getExports.js | 7 ++++++- src/core/ignore.js | 17 ++++++++++++++- src/core/parse.js | 25 ++++++++++++++++++++-- tests/files/typescript.ts | 6 +++--- tests/src/core/getExports.js | 40 +++++++++++++++++++++++++++++++++--- tests/src/core/parse.js | 11 ++++++---- 9 files changed, 126 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 575f32559..8d488c41c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ This project adheres to [Semantic Versioning](http://semver.org/). This change log adheres to standards from [Keep a CHANGELOG](http://keepachangelog.com). ## [Unreleased] +### Added +- [`import/parsers` setting]: parse some dependencies (i.e. TypeScript!) with a different parser than the ESLint-configured parser. + ### Fixed - [`namespace`] exception for get property from `namespace` import, which are re-export from commonjs module ([#416]) @@ -260,6 +263,7 @@ for info on changes for earlier releases. [`import/cache` setting]: ./README.md#importcache [`import/ignore` setting]: ./README.md#importignore [`import/extensions` setting]: ./README.md#importextensions +[`import/parsers` setting]: ./README.md#importparsers [`import/core-modules` setting]: ./README.md#importcore-modules [`import/external-module-folders` setting]: ./README.md#importexternal-module-folders diff --git a/README.md b/README.md index 675e98ceb..b396daf3b 100644 --- a/README.md +++ b/README.md @@ -265,6 +265,32 @@ Contribution of more such shared configs for other platforms are welcome! An array of folders. Resolved modules only from those folders will be considered as "external". By default - `["node_modules"]`. Makes sense if you have configured your path or webpack to handle your internal paths differently and want to considered modules from some folders, for example `bower_components` or `jspm_modules`, as "external". +#### `import/parsers` + +A map from parsers to file extension arrays. If a file extension is matched, the +dependency parser will require and use the map key as the parser instead of the +configured ESLint parser. This is useful if you're inter-op-ing with TypeScript +directly using Webpack, for example: + +```yaml +# .eslintrc.yml +settings: + import/parsers: + typescript-eslint-parser: [ .ts, .tsx ] +``` + +In this case, [`typescript-eslint-parser`](https://github.com/eslint/typescript-eslint-parser) must be installed and require-able from +the running `eslint` module's location (i.e., install it as a peer of ESLint). + +This is currently only tested with `typescript-eslint-parser` but should theoretically +work with any moderately ESTree-compliant parser. + +It's difficult to say how well various plugin features will be supported, too, +depending on how far down the rabbit hole goes. Submit an issue if you find strange +behavior beyond here, but steel your heart against the likely outcome of closing +with `wontfix`. + + #### `import/resolver` See [resolvers](#resolvers). diff --git a/package.json b/package.json index 165bdf1d4..f3b448d0e 100644 --- a/package.json +++ b/package.json @@ -53,16 +53,18 @@ "coveralls": "^2.11.4", "cross-env": "^2.0.0", "eslint": "2.x", - "eslint-plugin-import": "next", "eslint-import-resolver-node": "file:./resolvers/node", "eslint-import-resolver-webpack": "file:./resolvers/webpack", + "eslint-plugin-import": "next", "gulp": "^3.9.0", "gulp-babel": "6.1.2", "istanbul": "^0.4.0", "mocha": "^2.2.1", "nyc": "^7.0.0", "redux": "^3.0.4", - "rimraf": "2.5.2" + "rimraf": "2.5.2", + "typescript": "^1.8.10", + "typescript-eslint-parser": "^0.1.1" }, "peerDependencies": { "eslint": "2.x - 3.x" diff --git a/src/core/getExports.js b/src/core/getExports.js index 943eff5c4..56b427e32 100644 --- a/src/core/getExports.js +++ b/src/core/getExports.js @@ -5,12 +5,16 @@ import * as fs from 'fs' import { createHash } from 'crypto' import * as doctrine from 'doctrine' +import debug from 'debug' + import parse from './parse' import resolve, { relative as resolveRelative } from './resolve' import isIgnored, { hasValidExtension } from './ignore' import { hashObject } from './hash' +const log = debug('eslint-plugin-import:ExportMap') + const exportCache = new Map() /** @@ -96,8 +100,9 @@ export default class ExportMap { var m = new ExportMap(path) try { - var ast = parse(content, context) + var ast = parse(path, content, context) } catch (err) { + log('parse error:', path, err) m.errors.push(err) return m // can't continue } diff --git a/src/core/ignore.js b/src/core/ignore.js index 277ae6d87..cc679cf3e 100644 --- a/src/core/ignore.js +++ b/src/core/ignore.js @@ -13,12 +13,27 @@ function validExtensions({ settings }) { // breaking: default to '.js' // cachedSet = new Set(settings['import/extensions'] || [ '.js' ]) cachedSet = 'import/extensions' in settings - ? new Set(settings['import/extensions']) + ? makeValidExtensionSet(settings) : { has: () => true } // the set of all elements return cachedSet } +function makeValidExtensionSet(settings) { + // start with explicit JS-parsed extensions + const exts = new Set(settings['import/extensions']) + + // all alternate parser extensions are also valid + if ('import/parsers' in settings) { + for (let parser in settings['import/parsers']) { + settings['import/parsers'][parser] + .forEach(ext => exts.add(ext)) + } + } + + return exts +} + export default function ignore(path, context) { // ignore node_modules by default const ignoreStrings = context.settings['import/ignore'] diff --git a/src/core/parse.js b/src/core/parse.js index e30e3da13..e793ecb7c 100644 --- a/src/core/parse.js +++ b/src/core/parse.js @@ -1,11 +1,16 @@ import moduleRequire from './module-require' import assign from 'object-assign' +import { extname } from 'path' +import debug from 'debug' -export default function (content, context) { +const log = debug('eslint-plugin-import:parse') + +export default function (path, content, context) { if (context == null) throw new Error('need context to parse properly') - let { parserOptions, parserPath } = context + let { parserOptions } = context + const parserPath = getParserPath(path, context) if (!parserPath) throw new Error('parserPath is required!') @@ -21,3 +26,19 @@ export default function (content, context) { return parser.parse(content, parserOptions) } + +function getParserPath(path, context) { + const parsers = context.settings['import/parsers'] + if (parsers != null) { + const extension = extname(path) + for (let parserPath in parsers) { + if (parsers[parserPath].indexOf(extension) > -1) { + // use this alternate parser + log('using alt parser:', parserPath) + return parserPath + } + } + } + // default to use ESLint parser + return context.parserPath +} diff --git a/tests/files/typescript.ts b/tests/files/typescript.ts index 6e5e1087a..f9d296a2b 100644 --- a/tests/files/typescript.ts +++ b/tests/files/typescript.ts @@ -1,5 +1,5 @@ -type X = { y: string | null } +type X = string -export function getX() : X { - return null +export function getFoo() : X { + return "foo" } diff --git a/tests/src/core/getExports.js b/tests/src/core/getExports.js index 2990a6e3c..1a68fe3c2 100644 --- a/tests/src/core/getExports.js +++ b/tests/src/core/getExports.js @@ -83,11 +83,12 @@ describe('getExports', function () { var imports = ExportMap.parse( path, contents, - { parserPath: 'babel-eslint' } + { parserPath: 'babel-eslint', settings: {} } ) - expect(imports).to.exist - expect(imports.get('default')).to.exist + expect(imports, 'imports').to.exist + expect(imports.errors).to.be.empty + expect(imports.get('default'), 'default export').to.exist expect(imports.has('Bar')).to.be.true }) @@ -187,6 +188,7 @@ describe('getExports', function () { sourceType: 'module', attachComment: true, }, + settings: {}, }) }) @@ -197,6 +199,7 @@ describe('getExports', function () { sourceType: 'module', attachComment: true, }, + settings: {}, }) }) }) @@ -307,4 +310,35 @@ describe('getExports', function () { }) + context('alternate parsers', function () { + const configs = [ + // ['string form', { 'typescript-eslint-parser': '.ts' }], + ['array form', { 'typescript-eslint-parser': ['.ts', '.tsx'] }], + ] + + configs.forEach(([description, parserConfig]) => { + describe(description, function () { + const context = Object.assign({}, fakeContext, + { settings: { + 'import/extensions': ['.js'], + 'import/parsers': parserConfig, + } }) + + let imports + before('load imports', function () { + imports = ExportMap.get('./typescript.ts', context) + }) + + it('returns something for a TypeScript file', function () { + expect(imports).to.exist + }) + + it('has export (getFoo)', function () { + expect(imports.has('getFoo')).to.be.true + }) + }) + }) + + }) + }) diff --git a/tests/src/core/parse.js b/tests/src/core/parse.js index 0f9ba2f64..e7a044bed 100644 --- a/tests/src/core/parse.js +++ b/tests/src/core/parse.js @@ -5,17 +5,20 @@ import parse from 'core/parse' import { getFilename } from '../utils' describe('parse(content, { settings, ecmaFeatures })', function () { + const path = getFilename('jsx.js') let content before((done) => - fs.readFile(getFilename('jsx.js'), { encoding: 'utf8' }, + fs.readFile(path, { encoding: 'utf8' }, (err, f) => { if (err) { done(err) } else { content = f; done() }})) - it("doesn't support JSX by default", function () { - expect(() => parse(content, { parserPath: 'espree' })).to.throw(Error) + it('doesn\'t support JSX by default', function () { + expect(() => parse(path, content, { parserPath: 'espree' })).to.throw(Error) }) + it('infers jsx from ecmaFeatures when using stock parser', function () { - expect(() => parse(content, { parserPath: 'espree', parserOptions: { sourceType: 'module', ecmaFeatures: { jsx: true } } })) + expect(() => parse(path, content, { settings: {}, parserPath: 'espree', parserOptions: { sourceType: 'module', ecmaFeatures: { jsx: true } } })) .not.to.throw(Error) }) + })