@atlaskit/eslint-plugin-design-system
Version:
The essential plugin for use with the Atlassian Design System.
143 lines (137 loc) • 4.24 kB
JavaScript
import { getScope, getSourceCode } from '@atlaskit/eslint-utils/context-compat';
import { isStyledComponent } from './is-styled-component';
const getStack = (context, node) => {
var _scope;
const {
scopeManager
} = getSourceCode(context);
const stack = {
nodes: [],
root: node
};
let scope;
for (let current = node; current.type !== 'Program'; current = current.parent) {
if (!scope) {
const currentScope = scopeManager.acquire(current);
if (currentScope) {
scope = currentScope;
}
}
switch (current.type) {
case 'ExportDefaultDeclaration':
case 'ExportNamedDeclaration':
stack.root = current;
break;
case 'VariableDeclarator':
stack.root = current;
break;
case 'ExportSpecifier':
case 'ObjectExpression':
case 'VariableDeclaration':
break;
default:
stack.nodes.unshift(current);
}
}
return {
...stack,
scope: (_scope = scope) !== null && _scope !== void 0 ? _scope : getScope(context, node)
};
};
const matches = (defs, refs) => {
// When there are no defs, the definition is inlined. This must be a match as we know the refs contain the initial
// definition.
if (!defs.length) {
return true;
}
// When there are no refs, the reference refers to the entire definition and therefore must be a match.
if (!refs.length) {
return true;
}
// When both the references and definitions exist, they should match in length
if (defs.length !== refs.length) {
return false;
}
return defs.every((def, i) => {
const ref = refs[i];
if (def.type === 'Property') {
// There is a match between the def and the ref when both names match:
//
// const fooDef = { bar: '' };
// const barRef = fooDef.bar
//
// There is no match when the ref property does not match the definition key name:
//
// const barRef = fooDef.notFound
return def.key.type === 'Identifier' && ref.type === 'MemberExpression' && ref.property.type === 'Identifier' && ref.property.name === def.key.name;
}
// Anything here is either unsupported or should not match...
return false;
});
};
export const checkIfSupportedExport = (context, node, importSources, scope = getScope(context, node)) => {
// Ignore any expression defined outside of the global or module scope as we have no way of statically analysing them
if (scope.type !== 'global' && scope.type !== 'module') {
return {
isExport: false
};
}
const {
root,
nodes
} = getStack(context, node.parent);
// Exporting a component with a css reference should be allowed
if (isStyledComponent(nodes, context, importSources)) {
return {
isExport: false
};
}
if (root.type === 'ExportDefaultDeclaration' || root.type === 'ExportNamedDeclaration') {
return {
isExport: true,
node: root
};
}
if (root.type !== 'VariableDeclarator') {
return {
isExport: false
};
}
// Find the reference to the variable declarator
const reference = scope.references.find(({
identifier
}) => identifier === root.id);
if (!reference) {
return {
isExport: false
};
}
// Iterate through all of the references to the resolved variable declarator node
const {
resolved
} = reference;
for (const {
identifier
} of (_resolved$references = resolved === null || resolved === void 0 ? void 0 : resolved.references) !== null && _resolved$references !== void 0 ? _resolved$references : []) {
var _resolved$references;
// Skip references to the root, since it has already been processed above
if (identifier === root.id) {
continue;
}
const {
nodes: refs,
scope: nextScope
} = getStack(context, identifier.parent);
// Only validate the resolved reference if it accesses the definition node
if (matches(nodes, refs.reverse())) {
// Now validate the identifier reference as a definition
const validity = checkIfSupportedExport(context, identifier, importSources, nextScope);
if (validity.isExport) {
return validity;
}
}
}
return {
isExport: false
};
};