@atlaskit/eslint-plugin-design-system
Version:
The essential plugin for use with the Atlassian Design System.
115 lines (114 loc) • 4.79 kB
JavaScript
import { isNodeOfType } from 'eslint-codemod-utils';
import { createLintRule } from '../utils/create-lint-rule';
var MODAL_DIALOG_IMPORT_SOURCES = new Set(['@atlaskit/modal-dialog', '@atlaskit/modal-dialog/modal-title', '@atlaskit/modal-dialog/modal-header']);
var rule = createLintRule({
meta: {
name: 'use-modal-title',
type: 'suggestion',
docs: {
description: 'Encourages makers to include `ModalTitle` within `ModalHeader` when using Atlassian Design System modal dialog.',
recommended: true,
severity: 'error'
},
messages: {
modalHeaderMissingModalTitle: '`ModalHeader` should include `ModalTitle`.'
}
},
create: function create(context) {
var localImports = {
modalHeader: new Set(),
modalTitle: new Set()
};
var defaultImports = {
modalHeader: new Set(),
modalTitle: new Set()
};
/**
* Stack of open ModalHeader elements awaiting a ModalTitle descendant.
*
* Each frame records whether a ModalTitle has been found anywhere inside
* the corresponding ModalHeader's subtree. We use ESLint's own DFS traversal
* rather than a hand-rolled recursive search, which avoids the O(subtree)
* double-traversal that `containsModalTitle` caused.
*
* Invariant: frames are pushed in DFS enter-order and popped in exit-order,
* so the array is always a valid ancestor chain (outermost frame at index 0).
*/
var modalHeaderStack = [];
return {
// Keeping this for barrel imports, though we are moving away from them.
// This is mostly for non-internal usage.
ImportDeclaration: function ImportDeclaration(node) {
var importSource = String(node.source.value);
if (!MODAL_DIALOG_IMPORT_SOURCES.has(importSource)) {
return;
}
node.specifiers.forEach(function (specifier) {
if (isNodeOfType(specifier, 'ImportSpecifier') && 'name' in specifier.imported) {
if (specifier.imported.name === 'ModalHeader') {
localImports.modalHeader.add(specifier.local.name);
}
if (specifier.imported.name === 'ModalTitle') {
localImports.modalTitle.add(specifier.local.name);
}
}
if (isNodeOfType(specifier, 'ImportDefaultSpecifier')) {
if (importSource === '@atlaskit/modal-dialog/modal-header') {
defaultImports.modalHeader.add(specifier.local.name);
}
if (importSource === '@atlaskit/modal-dialog/modal-title') {
defaultImports.modalTitle.add(specifier.local.name);
}
}
});
},
JSXElement: function JSXElement(node) {
if (!isNodeOfType(node, 'JSXElement')) {
return;
}
if (!isNodeOfType(node.openingElement.name, 'JSXIdentifier')) {
return;
}
var elementName = node.openingElement.name.name;
if (localImports.modalHeader.has(elementName) || defaultImports.modalHeader.has(elementName)) {
// Push a new frame when entering a ModalHeader. It starts unsatisfied
// and will be marked satisfied if a ModalTitle is found in its subtree.
modalHeaderStack.push({
satisfied: false
});
return;
}
if (modalHeaderStack.length > 0 && (localImports.modalTitle.has(elementName) || defaultImports.modalTitle.has(elementName))) {
// A ModalTitle anywhere inside the current ModalHeader's subtree satisfies
// the requirement. Mark all enclosing ModalHeaders satisfied so that
// ModalTitle inside a nested ModalHeader also satisfies outer ones — matching
// the original containsModalTitle full-subtree-search semantics.
for (var _i = 0, _modalHeaderStack = modalHeaderStack; _i < _modalHeaderStack.length; _i++) {
var entry = _modalHeaderStack[_i];
entry.satisfied = true;
}
}
},
'JSXElement:exit': function JSXElementExit(node) {
if (!isNodeOfType(node, 'JSXElement')) {
return;
}
if (!isNodeOfType(node.openingElement.name, 'JSXIdentifier')) {
return;
}
var elementName = node.openingElement.name.name;
if (localImports.modalHeader.has(elementName) || defaultImports.modalHeader.has(elementName)) {
// Pop on exit (handles both regular and self-closing ModalHeader elements).
var entry = modalHeaderStack.pop();
if (entry && !entry.satisfied) {
context.report({
node: node.openingElement,
messageId: 'modalHeaderMissingModalTitle'
});
}
}
}
};
}
});
export default rule;