eslint-plugin-canonical
Version:
Canonical linting rules for ESLint.
811 lines (810 loc) • 30.9 kB
JavaScript
"use strict";
// @ts-nocheck
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.recursivePatternCapture = recursivePatternCapture;
/**
* Copied from eslint-plugin-import.
*/
const fs_1 = __importDefault(require("fs"));
const path_1 = require("path");
const doctrine_1 = __importDefault(require("doctrine"));
const debug_1 = __importDefault(require("debug"));
const eslint_1 = require("eslint");
const parse_1 = __importDefault(require("eslint-module-utils/parse"));
const visit_1 = __importDefault(require("eslint-module-utils/visit"));
const resolve_1 = __importDefault(require("eslint-module-utils/resolve"));
const ignore_1 = __importStar(require("eslint-module-utils/ignore"));
const hash_1 = require("eslint-module-utils/hash");
const unambiguous = __importStar(require("eslint-module-utils/unambiguous"));
const tsconfig_loader_1 = require("tsconfig-paths/lib/tsconfig-loader");
const array_includes_1 = __importDefault(require("array-includes"));
let ts;
const log = (0, debug_1.default)('eslint-plugin-import:ExportMap');
const exportCache = new Map();
const tsconfigCache = new Map();
class ExportMap {
constructor(path) {
this.path = path;
this.namespace = new Map();
// todo: restructure to key on path, value is resolver + map of names
this.reexports = new Map();
/**
* star-exports
* @type {Set} of () => ExportMap
*/
this.dependencies = new Set();
/**
* dependencies of this module that are not explicitly re-exported
* @type {Map} from path = () => ExportMap
*/
this.imports = new Map();
this.errors = [];
/**
* type {'ambiguous' | 'Module' | 'Script'}
*/
this.parseGoal = 'ambiguous';
}
get hasDefault() { return this.get('default') != null; } // stronger than this.has
get size() {
let size = this.namespace.size + this.reexports.size;
this.dependencies.forEach((dep) => {
const d = dep();
// CJS / ignored dependencies won't exist (#717)
if (d == null) {
return;
}
size += d.size;
});
return size;
}
/**
* Note that this does not check explicitly re-exported names for existence
* in the base namespace, but it will expand all `export * from '...'` exports
* if not found in the explicit namespace.
* @param {string} name
* @return {Boolean} true if `name` is exported by this module.
*/
has(name) {
if (this.namespace.has(name)) {
return true;
}
if (this.reexports.has(name)) {
return true;
}
// default exports must be explicitly re-exported (#328)
if (name !== 'default') {
for (const dep of this.dependencies) {
const innerMap = dep();
// todo: report as unresolved?
if (!innerMap) {
continue;
}
if (innerMap.has(name)) {
return true;
}
}
}
return false;
}
/**
* ensure that imported name fully resolves.
* @param {string} name
* @return {{ found: boolean, path: ExportMap[] }}
*/
hasDeep(name) {
if (this.namespace.has(name)) {
return { found: true, path: [this] };
}
if (this.reexports.has(name)) {
const reexports = this.reexports.get(name);
const imported = reexports.getImport();
// if import is ignored, return explicit 'null'
if (imported == null) {
return { found: true, path: [this] };
}
// safeguard against cycles, only if name matches
if (imported.path === this.path && reexports.local === name) {
return { found: false, path: [this] };
}
const deep = imported.hasDeep(reexports.local);
deep.path.unshift(this);
return deep;
}
// default exports must be explicitly re-exported (#328)
if (name !== 'default') {
for (const dep of this.dependencies) {
const innerMap = dep();
if (innerMap == null) {
return { found: true, path: [this] };
}
// todo: report as unresolved?
if (!innerMap) {
continue;
}
// safeguard against cycles
if (innerMap.path === this.path) {
continue;
}
const innerValue = innerMap.hasDeep(name);
if (innerValue.found) {
innerValue.path.unshift(this);
return innerValue;
}
}
}
return { found: false, path: [this] };
}
get(name) {
if (this.namespace.has(name)) {
return this.namespace.get(name);
}
if (this.reexports.has(name)) {
const reexports = this.reexports.get(name);
const imported = reexports.getImport();
// if import is ignored, return explicit 'null'
if (imported == null) {
return null;
}
// safeguard against cycles, only if name matches
if (imported.path === this.path && reexports.local === name) {
return undefined;
}
return imported.get(reexports.local);
}
// default exports must be explicitly re-exported (#328)
if (name !== 'default') {
for (const dep of this.dependencies) {
const innerMap = dep();
// todo: report as unresolved?
if (!innerMap) {
continue;
}
// safeguard against cycles
if (innerMap.path === this.path) {
continue;
}
const innerValue = innerMap.get(name);
if (innerValue !== undefined) {
return innerValue;
}
}
}
return undefined;
}
forEach(callback, thisArg) {
this.namespace.forEach((v, n) => { callback.call(thisArg, v, n, this); });
this.reexports.forEach((reexports, name) => {
const reexported = reexports.getImport();
// can't look up meta for ignored re-exports (#348)
callback.call(thisArg, reexported && reexported.get(reexports.local), name, this);
});
this.dependencies.forEach((dep) => {
const d = dep();
// CJS / ignored dependencies won't exist (#717)
if (d == null) {
return;
}
d.forEach((v, n) => {
if (n !== 'default') {
callback.call(thisArg, v, n, this);
}
});
});
}
// todo: keys, values, entries?
reportErrors(context, declaration) {
const msg = this.errors
.map((e) => `${e.message} (${e.lineNumber}:${e.column})`)
.join(', ');
context.report({
node: declaration.source,
message: `Parse errors in imported module '${declaration.source.value}': ${msg}`,
});
}
}
exports.default = ExportMap;
/**
* parse docs from the first node that has leading comments
*/
function captureDoc(source, docStyleParsers, ...nodes) {
const metadata = {};
// 'some' short-circuits on first 'true'
nodes.some((n) => {
try {
let leadingComments;
// n.leadingComments is legacy `attachComments` behavior
if ('leadingComments' in n) {
leadingComments = n.leadingComments;
}
else if (n.range) {
leadingComments = source.getCommentsBefore(n);
}
if (!leadingComments || leadingComments.length === 0) {
return false;
}
for (const name in docStyleParsers) {
const doc = docStyleParsers[name](leadingComments);
if (doc) {
metadata.doc = doc;
}
}
return true;
}
catch (err) {
return false;
}
});
return metadata;
}
const availableDocStyleParsers = {
jsdoc: captureJsDoc,
tomdoc: captureTomDoc,
};
/**
* parse JSDoc from leading comments
* @param {object[]} comments
* @return {{ doc: object }}
*/
function captureJsDoc(comments) {
let doc;
// capture XSDoc
comments.forEach((comment) => {
// skip non-block comments
if (comment.type !== 'Block') {
return;
}
try {
doc = doctrine_1.default.parse(comment.value, { unwrap: true });
}
catch (err) {
/* don't care, for now? maybe add to `errors?` */
}
});
return doc;
}
/**
* parse TomDoc section from comments
*/
function captureTomDoc(comments) {
// collect lines up to first paragraph break
const lines = [];
for (let i = 0; i < comments.length; i++) {
const comment = comments[i];
if (comment.value.match(/^\s*$/)) {
break;
}
lines.push(comment.value.trim());
}
// return doctrine-like object
const statusMatch = lines.join(' ').match(/^(Public|Internal|Deprecated):\s*(.+)/);
if (statusMatch) {
return {
description: statusMatch[2],
tags: [{
title: statusMatch[1].toLowerCase(),
description: statusMatch[2],
}],
};
}
}
const supportedImportTypes = new Set(['ImportDefaultSpecifier', 'ImportNamespaceSpecifier']);
ExportMap.get = function (source, context) {
const path = (0, resolve_1.default)(source, context);
if (path == null) {
return null;
}
return ExportMap.for(childContext(path, context));
};
ExportMap.for = function (context) {
const { path } = context;
const cacheKey = context.cacheKey || (0, hash_1.hashObject)(context).digest('hex');
let exportMap = exportCache.get(cacheKey);
// return cached ignore
if (exportMap === null) {
return null;
}
const stats = fs_1.default.statSync(path);
if (exportMap != null) {
// date equality check
if (exportMap.mtime - stats.mtime === 0) {
return exportMap;
}
// future: check content equality?
}
// check valid extensions first
if (!(0, ignore_1.hasValidExtension)(path, context)) {
exportCache.set(cacheKey, null);
return null;
}
// check for and cache ignore
if ((0, ignore_1.default)(path, context)) {
log('ignored path due to ignore settings:', path);
exportCache.set(cacheKey, null);
return null;
}
const content = fs_1.default.readFileSync(path, { encoding: 'utf8' });
// check for and cache unambiguous modules
if (!unambiguous.test(content)) {
log('ignored path due to unambiguous regex:', path);
exportCache.set(cacheKey, null);
return null;
}
log('cache miss', cacheKey, 'for path', path);
exportMap = ExportMap.parse(path, content, context);
// ambiguous modules return null
if (exportMap == null) {
log('ignored path due to ambiguous parse:', path);
exportCache.set(cacheKey, null);
return null;
}
exportMap.mtime = stats.mtime;
exportCache.set(cacheKey, exportMap);
return exportMap;
};
ExportMap.parse = function (path, content, context) {
const m = new ExportMap(path);
const isEsModuleInteropTrue = isEsModuleInterop();
let ast;
let visitorKeys;
try {
const result = (0, parse_1.default)(path, content, context);
ast = result.ast;
visitorKeys = result.visitorKeys;
}
catch (err) {
m.errors.push(err);
return m; // can't continue
}
m.visitorKeys = visitorKeys;
let hasDynamicImports = false;
function processDynamicImport(source) {
hasDynamicImports = true;
if (source.type !== 'Literal') {
return null;
}
const p = remotePath(source.value);
if (p == null) {
return null;
}
const importedSpecifiers = new Set();
importedSpecifiers.add('ImportNamespaceSpecifier');
const getter = thunkFor(p, context);
m.imports.set(p, {
getter,
declarations: new Set([{
source: {
// capturing actual node reference holds full AST in memory!
value: source.value,
loc: source.loc,
},
importedSpecifiers,
dynamic: true,
}]),
});
}
(0, visit_1.default)(ast, visitorKeys, {
ImportExpression(node) {
processDynamicImport(node.source);
},
CallExpression(node) {
if (node.callee.type === 'Import') {
processDynamicImport(node.arguments[0]);
}
},
});
const unambiguouslyESM = unambiguous.isModule(ast);
if (!unambiguouslyESM && !hasDynamicImports) {
return null;
}
const docstyle = context.settings && context.settings['import/docstyle'] || ['jsdoc'];
const docStyleParsers = {};
docstyle.forEach((style) => {
docStyleParsers[style] = availableDocStyleParsers[style];
});
// attempt to collect module doc
if (ast.comments) {
ast.comments.some((c) => {
if (c.type !== 'Block') {
return false;
}
try {
const doc = doctrine_1.default.parse(c.value, { unwrap: true });
if (doc.tags.some((t) => t.title === 'module')) {
m.doc = doc;
return true;
}
}
catch (err) { /* ignore */ }
return false;
});
}
const namespaces = new Map();
function remotePath(value) {
return resolve_1.default.relative(value, path, context.settings);
}
function resolveImport(value) {
const rp = remotePath(value);
if (rp == null) {
return null;
}
return ExportMap.for(childContext(rp, context));
}
function getNamespace(identifier) {
if (!namespaces.has(identifier.name)) {
return;
}
return function () {
return resolveImport(namespaces.get(identifier.name));
};
}
function addNamespace(object, identifier) {
const nsfn = getNamespace(identifier);
if (nsfn) {
Object.defineProperty(object, 'namespace', { get: nsfn });
}
return object;
}
function processSpecifier(s, n, m) {
const nsource = n.source && n.source.value;
const exportMeta = {};
let local;
switch (s.type) {
case 'ExportDefaultSpecifier':
if (!nsource) {
return;
}
local = 'default';
break;
case 'ExportNamespaceSpecifier':
m.namespace.set(s.exported.name, Object.defineProperty(exportMeta, 'namespace', {
get() { return resolveImport(nsource); },
}));
return;
case 'ExportAllDeclaration':
m.namespace.set(s.exported.name || s.exported.value, addNamespace(exportMeta, s.source.value));
return;
case 'ExportSpecifier':
if (!n.source) {
m.namespace.set(s.exported.name || s.exported.value, addNamespace(exportMeta, s.local));
return;
}
// else falls through
default:
local = s.local.name;
break;
}
// todo: JSDoc
m.reexports.set(s.exported.name, { local, getImport: () => resolveImport(nsource) });
}
function captureDependencyWithSpecifiers(n) {
// import type { Foo } (TS and Flow); import typeof { Foo } (Flow)
const declarationIsType = n.importKind === 'type' || n.importKind === 'typeof';
// import './foo' or import {} from './foo' (both 0 specifiers) is a side effect and
// shouldn't be considered to be just importing types
let specifiersOnlyImportingTypes = n.specifiers.length > 0;
const importedSpecifiers = new Set();
n.specifiers.forEach((specifier) => {
if (specifier.type === 'ImportSpecifier') {
importedSpecifiers.add(specifier.imported.name || specifier.imported.value);
}
else if (supportedImportTypes.has(specifier.type)) {
importedSpecifiers.add(specifier.type);
}
// import { type Foo } (Flow); import { typeof Foo } (Flow)
specifiersOnlyImportingTypes = specifiersOnlyImportingTypes
&& (specifier.importKind === 'type' || specifier.importKind === 'typeof');
});
captureDependency(n, declarationIsType || specifiersOnlyImportingTypes, importedSpecifiers);
}
function captureDependency({ source }, isOnlyImportingTypes, importedSpecifiers = new Set()) {
if (source == null) {
return null;
}
const p = remotePath(source.value);
if (p == null) {
return null;
}
const declarationMetadata = {
// capturing actual node reference holds full AST in memory!
source: { value: source.value, loc: source.loc },
isOnlyImportingTypes,
importedSpecifiers,
};
const existing = m.imports.get(p);
if (existing != null) {
existing.declarations.add(declarationMetadata);
return existing.getter;
}
const getter = thunkFor(p, context);
m.imports.set(p, { getter, declarations: new Set([declarationMetadata]) });
return getter;
}
const source = makeSourceCode(content, ast);
function readTsConfig(context) {
var _a, _b;
const parserOptions = (_b = (_a = context.languageOptions) === null || _a === void 0 ? void 0 : _a.parserOptions) !== null && _b !== void 0 ? _b : context.parserOptions;
const tsconfigInfo = (0, tsconfig_loader_1.tsConfigLoader)({
cwd: parserOptions && parserOptions.tsconfigRootDir || process.cwd(),
getEnv: (key) => process.env[key],
});
try {
if (tsconfigInfo.tsConfigPath !== undefined) {
// Projects not using TypeScript won't have `typescript` installed.
if (!ts) {
ts = require('typescript');
} // eslint-disable-line import/no-extraneous-dependencies
const configFile = ts.readConfigFile(tsconfigInfo.tsConfigPath, ts.sys.readFile);
return ts.parseJsonConfigFileContent(configFile.config, ts.sys, (0, path_1.dirname)(tsconfigInfo.tsConfigPath));
}
}
catch (e) {
// Catch any errors
}
return null;
}
function isEsModuleInterop() {
var _a, _b;
const parserOptions = (_b = (_a = context.languageOptions) === null || _a === void 0 ? void 0 : _a.parserOptions) !== null && _b !== void 0 ? _b : context.parserOptions;
const cacheKey = (0, hash_1.hashObject)({
tsconfigRootDir: parserOptions && parserOptions.tsconfigRootDir,
}).digest('hex');
let tsConfig = tsconfigCache.get(cacheKey);
if (typeof tsConfig === 'undefined') {
tsConfig = readTsConfig(context);
tsconfigCache.set(cacheKey, tsConfig);
}
return tsConfig && tsConfig.options ? tsConfig.options.esModuleInterop : false;
}
ast.body.forEach(function (n) {
if (n.type === 'ExportDefaultDeclaration') {
const exportMeta = captureDoc(source, docStyleParsers, n);
if (n.declaration.type === 'Identifier') {
addNamespace(exportMeta, n.declaration);
}
m.namespace.set('default', exportMeta);
return;
}
if (n.type === 'ExportAllDeclaration') {
const getter = captureDependency(n, n.exportKind === 'type');
if (getter) {
m.dependencies.add(getter);
}
if (n.exported) {
processSpecifier(n, n.exported, m);
}
return;
}
// capture namespaces in case of later export
if (n.type === 'ImportDeclaration') {
captureDependencyWithSpecifiers(n);
const ns = n.specifiers.find((s) => s.type === 'ImportNamespaceSpecifier');
if (ns) {
namespaces.set(ns.local.name, n.source.value);
}
return;
}
if (n.type === 'ExportNamedDeclaration') {
captureDependencyWithSpecifiers(n);
// capture declaration
if (n.declaration != null) {
switch (n.declaration.type) {
case 'FunctionDeclaration':
case 'ClassDeclaration':
case 'TypeAlias': // flowtype with babel-eslint parser
case 'InterfaceDeclaration':
case 'DeclareFunction':
case 'TSDeclareFunction':
case 'TSEnumDeclaration':
case 'TSTypeAliasDeclaration':
case 'TSInterfaceDeclaration':
case 'TSAbstractClassDeclaration':
case 'TSModuleDeclaration':
m.namespace.set(n.declaration.id.name, captureDoc(source, docStyleParsers, n));
break;
case 'VariableDeclaration':
n.declaration.declarations.forEach((d) => {
recursivePatternCapture(d.id, (id) => m.namespace.set(id.name, captureDoc(source, docStyleParsers, d, n)));
});
break;
default:
}
}
n.specifiers.forEach((s) => processSpecifier(s, n, m));
}
const exports = ['TSExportAssignment'];
if (isEsModuleInteropTrue) {
exports.push('TSNamespaceExportDeclaration');
}
// This doesn't declare anything, but changes what's being exported.
if ((0, array_includes_1.default)(exports, n.type)) {
const exportedName = n.type === 'TSNamespaceExportDeclaration'
? (n.id || n.name).name
: n.expression && n.expression.name || n.expression.id && n.expression.id.name || null;
const declTypes = [
'VariableDeclaration',
'ClassDeclaration',
'TSDeclareFunction',
'TSEnumDeclaration',
'TSTypeAliasDeclaration',
'TSInterfaceDeclaration',
'TSAbstractClassDeclaration',
'TSModuleDeclaration',
];
const exportedDecls = ast.body.filter(({ type, id, declarations }) => (0, array_includes_1.default)(declTypes, type) && (id && id.name === exportedName || declarations && declarations.find((d) => d.id.name === exportedName)));
if (exportedDecls.length === 0) {
// Export is not referencing any local declaration, must be re-exporting
m.namespace.set('default', captureDoc(source, docStyleParsers, n));
return;
}
if (isEsModuleInteropTrue // esModuleInterop is on in tsconfig
&& !m.namespace.has('default') // and default isn't added already
) {
m.namespace.set('default', {}); // add default export
}
exportedDecls.forEach((decl) => {
if (decl.type === 'TSModuleDeclaration') {
if (decl.body && decl.body.type === 'TSModuleDeclaration') {
m.namespace.set(decl.body.id.name, captureDoc(source, docStyleParsers, decl.body));
}
else if (decl.body && decl.body.body) {
decl.body.body.forEach((moduleBlockNode) => {
// Export-assignment exports all members in the namespace,
// explicitly exported or not.
const namespaceDecl = moduleBlockNode.type === 'ExportNamedDeclaration'
? moduleBlockNode.declaration
: moduleBlockNode;
if (!namespaceDecl) {
// TypeScript can check this for us; we needn't
}
else if (namespaceDecl.type === 'VariableDeclaration') {
namespaceDecl.declarations.forEach((d) => recursivePatternCapture(d.id, (id) => m.namespace.set(id.name, captureDoc(source, docStyleParsers, decl, namespaceDecl, moduleBlockNode))));
}
else {
m.namespace.set(namespaceDecl.id.name, captureDoc(source, docStyleParsers, moduleBlockNode));
}
});
}
}
else {
// Export as default
m.namespace.set('default', captureDoc(source, docStyleParsers, decl));
}
});
}
});
if (isEsModuleInteropTrue // esModuleInterop is on in tsconfig
&& m.namespace.size > 0 // anything is exported
&& !m.namespace.has('default') // and default isn't added already
) {
m.namespace.set('default', {}); // add default export
}
if (unambiguouslyESM) {
m.parseGoal = 'Module';
}
return m;
};
/**
* The creation of this closure is isolated from other scopes
* to avoid over-retention of unrelated variables, which has
* caused memory leaks. See #1266.
*/
function thunkFor(p, context) {
return () => ExportMap.for(childContext(p, context));
}
/**
* Traverse a pattern/identifier node, calling 'callback'
* for each leaf identifier.
* @param {node} pattern
* @param {Function} callback
* @return {void}
*/
function recursivePatternCapture(pattern, callback) {
switch (pattern.type) {
case 'Identifier': // base case
callback(pattern);
break;
case 'ObjectPattern':
pattern.properties.forEach((p) => {
if (p.type === 'ExperimentalRestProperty' || p.type === 'RestElement') {
callback(p.argument);
return;
}
recursivePatternCapture(p.value, callback);
});
break;
case 'ArrayPattern':
pattern.elements.forEach((element) => {
if (element == null) {
return;
}
if (element.type === 'ExperimentalRestProperty' || element.type === 'RestElement') {
callback(element.argument);
return;
}
recursivePatternCapture(element, callback);
});
break;
case 'AssignmentPattern':
callback(pattern.left);
break;
default:
}
}
let parserOptionsHash = '';
let prevParserOptions = '';
let settingsHash = '';
let prevSettings = '';
/**
* don't hold full context object in memory, just grab what we need.
* also calculate a cacheKey, where parts of the cacheKey hash are memoized
*/
function childContext(path, context) {
var _a, _b;
const { settings, parserPath, // = context.languageOptions?.parserOptions?.parserPath, // What is the replacement?
} = context;
const parserOptions = (_b = (_a = context.languageOptions) === null || _a === void 0 ? void 0 : _a.parserOptions) !== null && _b !== void 0 ? _b : context.parserOptions;
if (JSON.stringify(settings) !== prevSettings) {
settingsHash = (0, hash_1.hashObject)({ settings }).digest('hex');
prevSettings = JSON.stringify(settings);
}
if (JSON.stringify(parserOptions) !== prevParserOptions) {
parserOptionsHash = (0, hash_1.hashObject)({ parserOptions }).digest('hex');
prevParserOptions = JSON.stringify(parserOptions);
}
return {
cacheKey: String(parserPath) + parserOptionsHash + settingsHash + String(path),
settings,
parserOptions,
parserPath,
path,
};
}
/**
* sometimes legacy support isn't _that_ hard... right?
*/
function makeSourceCode(text, ast) {
if (eslint_1.SourceCode.length > 1) {
// ESLint 3
return new eslint_1.SourceCode(text, ast);
}
else {
// ESLint 4, 5
return new eslint_1.SourceCode({ text, ast });
}
}