-
Notifications
You must be signed in to change notification settings - Fork 68
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1233 from bryceosterhaus/LPD-39648
feat(eslint-plugin): add rule to sort named exports from a source
- Loading branch information
Showing
6 changed files
with
254 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
171 changes: 171 additions & 0 deletions
171
projects/eslint-plugin/rules/general/lib/rules/sort-exports.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
/** | ||
* SPDX-FileCopyrightText: © 2017 Liferay, Inc. <https://liferay.com> | ||
* SPDX-License-Identifier: MIT | ||
*/ | ||
|
||
const { | ||
getLeadingComments, | ||
getTrailingComments, | ||
isAbsolute, | ||
isRelative, | ||
withScope, | ||
} = require('../common/imports'); | ||
|
||
const DESCRIPTION = 'exports must be sorted'; | ||
|
||
/** | ||
* Given two sort keys `a` and `b`, return -1, 0 or 1 to indicate | ||
* their relative ordering. | ||
*/ | ||
function compare(aKey, bKey) { | ||
const [aPrefix, aName, aTieBreaker] = aKey.split(':'); | ||
const [bPrefix, bName, bTieBreaker] = bKey.split(':'); | ||
|
||
const cmp = (a, b) => { | ||
return a < b ? -1 : a > b ? 1 : 0; | ||
}; | ||
|
||
return ( | ||
cmp(aPrefix, bPrefix) || | ||
cmp(ranking(aName), ranking(bName)) || | ||
cmp(aName, bName) || | ||
cmp(aTieBreaker, bTieBreaker) | ||
); | ||
} | ||
|
||
/** | ||
* Returns a ranking for `source`. Lower-numbered ranks are considered more | ||
* important and will be sorted first in the file. | ||
* | ||
* - 0: NodeJS built-ins and dependencies declared in "package.json" files. | ||
* - 1: Absolute paths. | ||
* - 2: Relative paths. | ||
*/ | ||
function ranking(source) { | ||
return isRelative(source) ? 2 : isAbsolute(source) ? 1 : 0; | ||
} | ||
|
||
module.exports = { | ||
create(context) { | ||
const exportNodes = []; | ||
|
||
const {visitors} = withScope(); | ||
|
||
function getRangeForNode(node) { | ||
const commentsBefore = getLeadingComments(node, context); | ||
const commentsAfter = getTrailingComments(node, context); | ||
|
||
const first = commentsBefore[0] || node; | ||
|
||
const last = commentsAfter[commentsAfter.length - 1] || node; | ||
|
||
return [first.range[0], last.range[1]]; | ||
} | ||
|
||
return { | ||
...visitors, | ||
ExportNamedDeclaration(node) { | ||
|
||
/** | ||
* Only sort exports if they have a source. Skip exports like: | ||
* export function Foo() {} | ||
* export default function Bar() {} | ||
* export {Baz}; | ||
*/ | ||
if (node.source) { | ||
exportNodes.push(node); | ||
} | ||
}, | ||
|
||
['Program:exit'](_node) { | ||
const desired = [...exportNodes].sort((a, b) => | ||
compare(a.source.value, b.source.value) | ||
); | ||
|
||
// Try to make error messages (somewhat) minimal by only | ||
// reporting from the first to the last mismatch (ie. | ||
// not a full Myers diff algorithm). | ||
|
||
let firstMismatch = -1; | ||
let lastMismatch = -1; | ||
|
||
for (let i = 0; i < exportNodes.length; i++) { | ||
if (exportNodes[i] !== desired[i]) { | ||
firstMismatch = i; | ||
break; | ||
} | ||
} | ||
|
||
for (let i = exportNodes.length - 1; i >= 0; i--) { | ||
if (exportNodes[i] !== desired[i]) { | ||
lastMismatch = i; | ||
break; | ||
} | ||
} | ||
|
||
if (firstMismatch === -1) { | ||
return; | ||
} | ||
|
||
const description = desired | ||
.slice(firstMismatch, lastMismatch + 1) | ||
.map((node) => { | ||
const source = JSON.stringify(node.source.value); | ||
|
||
return source; | ||
}) | ||
.join(' << '); | ||
|
||
const message = | ||
'exports must be sorted by module name ' + | ||
`(expected: ${description})`; | ||
|
||
context.report({ | ||
fix: (fixer) => { | ||
const fixings = []; | ||
|
||
const code = context.getSourceCode(); | ||
|
||
const sources = new Map(); | ||
|
||
// Pass 1: Extract copy of text. | ||
|
||
for (let i = firstMismatch; i <= lastMismatch; i++) { | ||
const node = exportNodes[i]; | ||
const range = getRangeForNode(node); | ||
const text = code.getText().slice(...range); | ||
|
||
sources.set(exportNodes[i], {text}); | ||
} | ||
|
||
// Pass 2: Write text into expected positions. | ||
|
||
for (let i = firstMismatch; i <= lastMismatch; i++) { | ||
fixings.push( | ||
fixer.replaceTextRange( | ||
getRangeForNode(exportNodes[i]), | ||
sources.get(desired[i]).text | ||
) | ||
); | ||
} | ||
|
||
return fixings; | ||
}, | ||
message, | ||
node: exportNodes[firstMismatch], | ||
}); | ||
}, | ||
}; | ||
}, | ||
|
||
meta: { | ||
docs: { | ||
category: 'Best Practices', | ||
description: DESCRIPTION, | ||
recommended: false, | ||
}, | ||
fixable: 'code', | ||
schema: [], | ||
type: 'problem', | ||
}, | ||
}; |
54 changes: 54 additions & 0 deletions
54
projects/eslint-plugin/rules/general/tests/lib/rules/sort-exports.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
/** | ||
* SPDX-FileCopyrightText: © 2017 Liferay, Inc. <https://liferay.com> | ||
* SPDX-License-Identifier: MIT | ||
*/ | ||
|
||
const MultiTester = require('../../../../../scripts/MultiTester'); | ||
const rule = require('../../../lib/rules/sort-exports'); | ||
|
||
const parserOptions = { | ||
parserOptions: { | ||
ecmaVersion: 6, | ||
sourceType: 'module', | ||
}, | ||
}; | ||
|
||
const ruleTester = new MultiTester(parserOptions); | ||
|
||
ruleTester.run('sort-exports', rule, { | ||
invalid: [ | ||
{ | ||
|
||
// Basic example. | ||
|
||
code: ` | ||
export {Foo} from './Foo'; | ||
export {Bar} from './Bar'; | ||
`, | ||
errors: [ | ||
{ | ||
message: | ||
'exports must be sorted by module name ' + | ||
'(expected: "./Bar" << "./Foo")', | ||
type: 'ExportNamedDeclaration', | ||
}, | ||
], | ||
output: ` | ||
export {Bar} from './Bar'; | ||
export {Foo} from './Foo'; | ||
`, | ||
}, | ||
], | ||
|
||
valid: [ | ||
{ | ||
|
||
// Well-sorted exports. | ||
|
||
code: ` | ||
export {Bar} from './Bar'; | ||
export {Foo} from './Foo'; | ||
`, | ||
}, | ||
], | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters