UNPKG

@atlaskit/eslint-plugin-design-system

Version:

The essential plugin for use with the Atlassian Design System.

143 lines (137 loc) 4.24 kB
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 }; };