diff --git a/.eslintrc b/.eslintrc index ddf7bc562..8dc0b2637 100644 --- a/.eslintrc +++ b/.eslintrc @@ -96,6 +96,7 @@ "no-multiple-empty-lines": [2, { "max": 1, "maxEOF": 1, "maxBOF": 0 }], "no-return-assign": [2, "always"], "no-trailing-spaces": 2, + "no-use-before-define": [2, { "functions": true, "classes": true, "variables": true }], "no-var": 2, "object-curly-spacing": [2, "always"], "object-shorthand": ["error", "always", { @@ -225,6 +226,15 @@ "no-console": 1, }, }, + { + "files": [ + "utils/**", // TODO + "src/ExportMap.js", // TODO + ], + "rules": { + "no-use-before-define": "off", + }, + }, { "files": [ "resolvers/*/test/**/*", diff --git a/src/core/importType.js b/src/core/importType.js index 6a37d1bb1..32e200f1d 100644 --- a/src/core/importType.js +++ b/src/core/importType.js @@ -4,6 +4,11 @@ import isCoreModule from 'is-core-module'; import resolve from 'eslint-module-utils/resolve'; import { getContextPackagePath } from './packagePath'; +const scopedRegExp = /^@[^/]+\/?[^/]+/; +export function isScoped(name) { + return name && scopedRegExp.test(name); +} + function baseModule(name) { if (isScoped(name)) { const [scope, pkg] = name.split('/'); @@ -30,20 +35,6 @@ export function isBuiltIn(name, settings, path) { return isCoreModule(base) || extras.indexOf(base) > -1; } -export function isExternalModule(name, path, context) { - if (arguments.length < 3) { - throw new TypeError('isExternalModule: name, path, and context are all required'); - } - return (isModule(name) || isScoped(name)) && typeTest(name, context, path) === 'external'; -} - -export function isExternalModuleMain(name, path, context) { - if (arguments.length < 3) { - throw new TypeError('isExternalModule: name, path, and context are all required'); - } - return isModuleMain(name) && typeTest(name, context, path) === 'external'; -} - const moduleRegExp = /^\w/; function isModule(name) { return name && moduleRegExp.test(name); @@ -54,20 +45,9 @@ function isModuleMain(name) { return name && moduleMainRegExp.test(name); } -const scopedRegExp = /^@[^/]+\/?[^/]+/; -export function isScoped(name) { - return name && scopedRegExp.test(name); -} - -const scopedMainRegExp = /^@[^/]+\/?[^/]+$/; -export function isScopedMain(name) { - return name && scopedMainRegExp.test(name); -} - function isRelativeToParent(name) { return (/^\.\.$|^\.\.[\\/]/).test(name); } - const indexFiles = ['.', './', './index', './index.js']; function isIndex(name) { return indexFiles.indexOf(name) !== -1; @@ -123,6 +103,25 @@ function typeTest(name, context, path) { return 'unknown'; } +export function isExternalModule(name, path, context) { + if (arguments.length < 3) { + throw new TypeError('isExternalModule: name, path, and context are all required'); + } + return (isModule(name) || isScoped(name)) && typeTest(name, context, path) === 'external'; +} + +export function isExternalModuleMain(name, path, context) { + if (arguments.length < 3) { + throw new TypeError('isExternalModule: name, path, and context are all required'); + } + return isModuleMain(name) && typeTest(name, context, path) === 'external'; +} + +const scopedMainRegExp = /^@[^/]+\/?[^/]+$/; +export function isScopedMain(name) { + return name && scopedMainRegExp.test(name); +} + export default function resolveImportType(name, context) { return typeTest(name, context, resolve(name, context)); } diff --git a/src/core/packagePath.js b/src/core/packagePath.js index 1a7a28f4b..142f44aa4 100644 --- a/src/core/packagePath.js +++ b/src/core/packagePath.js @@ -2,15 +2,15 @@ import { dirname } from 'path'; import pkgUp from 'eslint-module-utils/pkgUp'; import readPkgUp from 'eslint-module-utils/readPkgUp'; -export function getContextPackagePath(context) { - return getFilePackagePath(context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename()); -} - export function getFilePackagePath(filePath) { const fp = pkgUp({ cwd: filePath }); return dirname(fp); } +export function getContextPackagePath(context) { + return getFilePackagePath(context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename()); +} + export function getFilePackageName(filePath) { const { pkg, path } = readPkgUp({ cwd: filePath, normalize: false }); if (pkg) { diff --git a/src/rules/no-cycle.js b/src/rules/no-cycle.js index 5b9d8c070..11c2f44fc 100644 --- a/src/rules/no-cycle.js +++ b/src/rules/no-cycle.js @@ -11,6 +11,10 @@ import docsUrl from '../docsUrl'; const traversed = new Set(); +function routeString(route) { + return route.map((s) => `${s.value}:${s.loc.start.line}`).join('=>'); +} + module.exports = { meta: { type: 'suggestion', @@ -151,7 +155,3 @@ module.exports = { }); }, }; - -function routeString(route) { - return route.map((s) => `${s.value}:${s.loc.start.line}`).join('=>'); -} diff --git a/src/rules/no-duplicates.js b/src/rules/no-duplicates.js index 6b4f4d559..033e854e0 100644 --- a/src/rules/no-duplicates.js +++ b/src/rules/no-duplicates.js @@ -9,28 +9,68 @@ try { typescriptPkg = require('typescript/package.json'); // eslint-disable-line import/no-extraneous-dependencies } catch (e) { /**/ } -function checkImports(imported, context) { - for (const [module, nodes] of imported.entries()) { - if (nodes.length > 1) { - const message = `'${module}' imported multiple times.`; - const [first, ...rest] = nodes; - const sourceCode = context.getSourceCode(); - const fix = getFix(first, rest, sourceCode, context); +function isPunctuator(node, value) { + return node.type === 'Punctuator' && node.value === value; +} - context.report({ - node: first.source, - message, - fix, // Attach the autofix (if any) to the first import. - }); +// Get the name of the default import of `node`, if any. +function getDefaultImportName(node) { + const defaultSpecifier = node.specifiers + .find((specifier) => specifier.type === 'ImportDefaultSpecifier'); + return defaultSpecifier != null ? defaultSpecifier.local.name : undefined; +} - for (const node of rest) { - context.report({ - node: node.source, - message, - }); - } - } - } +// Checks whether `node` has a namespace import. +function hasNamespace(node) { + const specifiers = node.specifiers + .filter((specifier) => specifier.type === 'ImportNamespaceSpecifier'); + return specifiers.length > 0; +} + +// Checks whether `node` has any non-default specifiers. +function hasSpecifiers(node) { + const specifiers = node.specifiers + .filter((specifier) => specifier.type === 'ImportSpecifier'); + return specifiers.length > 0; +} + +// Checks whether `node` has a comment (that ends) on the previous line or on +// the same line as `node` (starts). +function hasCommentBefore(node, sourceCode) { + return sourceCode.getCommentsBefore(node) + .some((comment) => comment.loc.end.line >= node.loc.start.line - 1); +} + +// Checks whether `node` has a comment (that starts) on the same line as `node` +// (ends). +function hasCommentAfter(node, sourceCode) { + return sourceCode.getCommentsAfter(node) + .some((comment) => comment.loc.start.line === node.loc.end.line); +} + +// Checks whether `node` has any comments _inside,_ except inside the `{...}` +// part (if any). +function hasCommentInsideNonSpecifiers(node, sourceCode) { + const tokens = sourceCode.getTokens(node); + const openBraceIndex = tokens.findIndex((token) => isPunctuator(token, '{')); + const closeBraceIndex = tokens.findIndex((token) => isPunctuator(token, '}')); + // Slice away the first token, since we're no looking for comments _before_ + // `node` (only inside). If there's a `{...}` part, look for comments before + // the `{`, but not before the `}` (hence the `+1`s). + const someTokens = openBraceIndex >= 0 && closeBraceIndex >= 0 + ? tokens.slice(1, openBraceIndex + 1).concat(tokens.slice(closeBraceIndex + 1)) + : tokens.slice(1); + return someTokens.some((token) => sourceCode.getCommentsBefore(token).length > 0); +} + +// It's not obvious what the user wants to do with comments associated with +// duplicate imports, so skip imports with comments when autofixing. +function hasProblematicComments(node, sourceCode) { + return ( + hasCommentBefore(node, sourceCode) + || hasCommentAfter(node, sourceCode) + || hasCommentInsideNonSpecifiers(node, sourceCode) + ); } function getFix(first, rest, sourceCode, context) { @@ -203,68 +243,28 @@ function getFix(first, rest, sourceCode, context) { }; } -function isPunctuator(node, value) { - return node.type === 'Punctuator' && node.value === value; -} - -// Get the name of the default import of `node`, if any. -function getDefaultImportName(node) { - const defaultSpecifier = node.specifiers - .find((specifier) => specifier.type === 'ImportDefaultSpecifier'); - return defaultSpecifier != null ? defaultSpecifier.local.name : undefined; -} - -// Checks whether `node` has a namespace import. -function hasNamespace(node) { - const specifiers = node.specifiers - .filter((specifier) => specifier.type === 'ImportNamespaceSpecifier'); - return specifiers.length > 0; -} - -// Checks whether `node` has any non-default specifiers. -function hasSpecifiers(node) { - const specifiers = node.specifiers - .filter((specifier) => specifier.type === 'ImportSpecifier'); - return specifiers.length > 0; -} - -// It's not obvious what the user wants to do with comments associated with -// duplicate imports, so skip imports with comments when autofixing. -function hasProblematicComments(node, sourceCode) { - return ( - hasCommentBefore(node, sourceCode) - || hasCommentAfter(node, sourceCode) - || hasCommentInsideNonSpecifiers(node, sourceCode) - ); -} - -// Checks whether `node` has a comment (that ends) on the previous line or on -// the same line as `node` (starts). -function hasCommentBefore(node, sourceCode) { - return sourceCode.getCommentsBefore(node) - .some((comment) => comment.loc.end.line >= node.loc.start.line - 1); -} +function checkImports(imported, context) { + for (const [module, nodes] of imported.entries()) { + if (nodes.length > 1) { + const message = `'${module}' imported multiple times.`; + const [first, ...rest] = nodes; + const sourceCode = context.getSourceCode(); + const fix = getFix(first, rest, sourceCode, context); -// Checks whether `node` has a comment (that starts) on the same line as `node` -// (ends). -function hasCommentAfter(node, sourceCode) { - return sourceCode.getCommentsAfter(node) - .some((comment) => comment.loc.start.line === node.loc.end.line); -} + context.report({ + node: first.source, + message, + fix, // Attach the autofix (if any) to the first import. + }); -// Checks whether `node` has any comments _inside,_ except inside the `{...}` -// part (if any). -function hasCommentInsideNonSpecifiers(node, sourceCode) { - const tokens = sourceCode.getTokens(node); - const openBraceIndex = tokens.findIndex((token) => isPunctuator(token, '{')); - const closeBraceIndex = tokens.findIndex((token) => isPunctuator(token, '}')); - // Slice away the first token, since we're no looking for comments _before_ - // `node` (only inside). If there's a `{...}` part, look for comments before - // the `{`, but not before the `}` (hence the `+1`s). - const someTokens = openBraceIndex >= 0 && closeBraceIndex >= 0 - ? tokens.slice(1, openBraceIndex + 1).concat(tokens.slice(closeBraceIndex + 1)) - : tokens.slice(1); - return someTokens.some((token) => sourceCode.getCommentsBefore(token).length > 0); + for (const node of rest) { + context.report({ + node: node.source, + message, + }); + } + } + } } module.exports = { diff --git a/src/rules/no-namespace.js b/src/rules/no-namespace.js index d3e591876..3c6617a41 100644 --- a/src/rules/no-namespace.js +++ b/src/rules/no-namespace.js @@ -6,9 +6,74 @@ import minimatch from 'minimatch'; import docsUrl from '../docsUrl'; -//------------------------------------------------------------------------------ -// Rule Definition -//------------------------------------------------------------------------------ +/** + * @param {MemberExpression} memberExpression + * @returns {string} the name of the member in the object expression, e.g. the `x` in `namespace.x` + */ +function getMemberPropertyName(memberExpression) { + return memberExpression.property.type === 'Identifier' + ? memberExpression.property.name + : memberExpression.property.value; +} + +/** + * @param {ScopeManager} scopeManager + * @param {ASTNode} node + * @return {Set} + */ +function getVariableNamesInScope(scopeManager, node) { + let currentNode = node; + let scope = scopeManager.acquire(currentNode); + while (scope == null) { + currentNode = currentNode.parent; + scope = scopeManager.acquire(currentNode, true); + } + return new Set(scope.variables.concat(scope.upper.variables).map((variable) => variable.name)); +} + +/** + * + * @param {*} names + * @param {*} nameConflicts + * @param {*} namespaceName + */ +function generateLocalNames(names, nameConflicts, namespaceName) { + const localNames = {}; + names.forEach((name) => { + let localName; + if (!nameConflicts[name].has(name)) { + localName = name; + } else if (!nameConflicts[name].has(`${namespaceName}_${name}`)) { + localName = `${namespaceName}_${name}`; + } else { + for (let i = 1; i < Infinity; i++) { + if (!nameConflicts[name].has(`${namespaceName}_${name}_${i}`)) { + localName = `${namespaceName}_${name}_${i}`; + break; + } + } + } + localNames[name] = localName; + }); + return localNames; +} + +/** + * @param {Identifier[]} namespaceIdentifiers + * @returns {boolean} `true` if the namespace variable is more than just a glorified constant + */ +function usesNamespaceAsObject(namespaceIdentifiers) { + return !namespaceIdentifiers.every((identifier) => { + const parent = identifier.parent; + + // `namespace.x` or `namespace['x']` + return ( + parent + && parent.type === 'MemberExpression' + && (parent.property.type === 'Identifier' || parent.property.type === 'Literal') + ); + }); +} module.exports = { meta: { @@ -103,72 +168,3 @@ module.exports = { }; }, }; - -/** - * @param {Identifier[]} namespaceIdentifiers - * @returns {boolean} `true` if the namespace variable is more than just a glorified constant - */ -function usesNamespaceAsObject(namespaceIdentifiers) { - return !namespaceIdentifiers.every((identifier) => { - const parent = identifier.parent; - - // `namespace.x` or `namespace['x']` - return ( - parent - && parent.type === 'MemberExpression' - && (parent.property.type === 'Identifier' || parent.property.type === 'Literal') - ); - }); -} - -/** - * @param {MemberExpression} memberExpression - * @returns {string} the name of the member in the object expression, e.g. the `x` in `namespace.x` - */ -function getMemberPropertyName(memberExpression) { - return memberExpression.property.type === 'Identifier' - ? memberExpression.property.name - : memberExpression.property.value; -} - -/** - * @param {ScopeManager} scopeManager - * @param {ASTNode} node - * @return {Set} - */ -function getVariableNamesInScope(scopeManager, node) { - let currentNode = node; - let scope = scopeManager.acquire(currentNode); - while (scope == null) { - currentNode = currentNode.parent; - scope = scopeManager.acquire(currentNode, true); - } - return new Set(scope.variables.concat(scope.upper.variables).map((variable) => variable.name)); -} - -/** - * - * @param {*} names - * @param {*} nameConflicts - * @param {*} namespaceName - */ -function generateLocalNames(names, nameConflicts, namespaceName) { - const localNames = {}; - names.forEach((name) => { - let localName; - if (!nameConflicts[name].has(name)) { - localName = name; - } else if (!nameConflicts[name].has(`${namespaceName}_${name}`)) { - localName = `${namespaceName}_${name}`; - } else { - for (let i = 1; i < Infinity; i++) { - if (!nameConflicts[name].has(`${namespaceName}_${name}_${i}`)) { - localName = `${namespaceName}_${name}_${i}`; - break; - } - } - } - localNames[name] = localName; - }); - return localNames; -} diff --git a/src/rules/no-restricted-paths.js b/src/rules/no-restricted-paths.js index cd680a194..75952dd05 100644 --- a/src/rules/no-restricted-paths.js +++ b/src/rules/no-restricted-paths.js @@ -12,6 +12,15 @@ const containsPath = (filepath, target) => { return relative === '' || !relative.startsWith('..'); }; +function isMatchingTargetPath(filename, targetPath) { + if (isGlob(targetPath)) { + const mm = new Minimatch(targetPath); + return mm.match(filename); + } + + return containsPath(filename, targetPath); +} + module.exports = { meta: { type: 'problem', @@ -83,15 +92,6 @@ module.exports = { .some((targetPath) => isMatchingTargetPath(currentFilename, targetPath)), ); - function isMatchingTargetPath(filename, targetPath) { - if (isGlob(targetPath)) { - const mm = new Minimatch(targetPath); - return mm.match(filename); - } - - return containsPath(filename, targetPath); - } - function isValidExceptionPath(absoluteFromPath, absoluteExceptionPath) { const relativeExceptionPath = path.relative(absoluteFromPath, absoluteExceptionPath); diff --git a/src/rules/order.js b/src/rules/order.js index 44d25be63..7071513bf 100644 --- a/src/rules/order.js +++ b/src/rules/order.js @@ -92,6 +92,12 @@ function findRootNode(node) { return parent; } +function commentOnSameLineAs(node) { + return (token) => (token.type === 'Block' || token.type === 'Line') + && token.loc.start.line === token.loc.end.line + && token.loc.end.line === node.loc.end.line; +} + function findEndOfLineWithComments(sourceCode, node) { const tokensToEndOfLine = takeTokensAfterWhile(sourceCode, node, commentOnSameLineAs(node)); const endOfTokens = tokensToEndOfLine.length > 0 @@ -111,12 +117,6 @@ function findEndOfLineWithComments(sourceCode, node) { return result; } -function commentOnSameLineAs(node) { - return (token) => (token.type === 'Block' || token.type === 'Line') - && token.loc.start.line === token.loc.end.line - && token.loc.end.line === node.loc.end.line; -} - function findStartOfLineWithComments(sourceCode, node) { const tokensToEndOfLine = takeTokensBeforeWhile(sourceCode, node, commentOnSameLineAs(node)); const startOfTokens = tokensToEndOfLine.length > 0 ? tokensToEndOfLine[0].range[0] : node.range[0]; diff --git a/tests/src/package.js b/tests/src/package.js index 08138084c..c56bd1333 100644 --- a/tests/src/package.js +++ b/tests/src/package.js @@ -45,6 +45,13 @@ describe('package', function () { }); }); + function getRulePath(ruleName) { + // 'require' does not work with dynamic paths because of the compilation step by babel + // (which resolves paths according to the root folder configuration) + // the usage of require.resolve on a static path gets around this + return path.resolve(require.resolve('rules/no-unresolved'), '..', ruleName); + } + it('has configs only for rules that exist', function () { for (const configFile in module.configs) { const preamble = 'import/'; @@ -54,13 +61,6 @@ describe('package', function () { .not.to.throw(Error); } } - - function getRulePath(ruleName) { - // 'require' does not work with dynamic paths because of the compilation step by babel - // (which resolves paths according to the root folder configuration) - // the usage of require.resolve on a static path gets around this - return path.resolve(require.resolve('rules/no-unresolved'), '..', ruleName); - } }); it('marks deprecated rules in their metadata', function () { diff --git a/tests/src/utils.js b/tests/src/utils.js index d5215b02e..24d5504a7 100644 --- a/tests/src/utils.js +++ b/tests/src/utils.js @@ -42,10 +42,6 @@ export function eslintVersionSatisfies(specifier) { return semver.satisfies(eslintPkg.version, specifier); } -export function testVersion(specifier, t) { - return eslintVersionSatisfies(specifier) ? test(t()) : []; -} - export function test(t) { if (arguments.length !== 1) { throw new SyntaxError('`test` requires exactly one object argument'); @@ -61,6 +57,10 @@ export function test(t) { }; } +export function testVersion(specifier, t) { + return eslintVersionSatisfies(specifier) ? test(t()) : []; +} + export function testContext(settings) { return { getFilename() { return FILENAME; }, settings: settings || {} };