Skip to content

Commit

Permalink
support linting JS with alt-JS dependencies via import/parsers sett…
Browse files Browse the repository at this point in the history
…ing! (closes #407)
  • Loading branch information
benmosher committed Aug 20, 2016
1 parent 852d14b commit 8a68fbb
Show file tree
Hide file tree
Showing 9 changed files with 126 additions and 16 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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])

Expand Down Expand Up @@ -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

Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
7 changes: 6 additions & 1 deletion src/core/getExports.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()

/**
Expand Down Expand Up @@ -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
}
Expand Down
17 changes: 16 additions & 1 deletion src/core/ignore.js
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down
25 changes: 23 additions & 2 deletions src/core/parse.js
Original file line number Diff line number Diff line change
@@ -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!')

Expand All @@ -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
}
6 changes: 3 additions & 3 deletions tests/files/typescript.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
type X = { y: string | null }
type X = string

export function getX() : X {
return null
export function getFoo() : X {
return "foo"
}
40 changes: 37 additions & 3 deletions tests/src/core/getExports.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
})

Expand Down Expand Up @@ -187,6 +188,7 @@ describe('getExports', function () {
sourceType: 'module',
attachComment: true,
},
settings: {},
})
})

Expand All @@ -197,6 +199,7 @@ describe('getExports', function () {
sourceType: 'module',
attachComment: true,
},
settings: {},
})
})
})
Expand Down Expand Up @@ -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
})
})
})

})

})
11 changes: 7 additions & 4 deletions tests/src/core/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})

})

0 comments on commit 8a68fbb

Please sign in to comment.