@atlaskit/eslint-plugin-ui-styling-standard
Version:
The eslint plugin to enforce and educate on Atlassian's UI Styling Standard
265 lines • 13.8 kB
JavaScript
function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
import { getSourceCode } from '@atlaskit/eslint-utils/context-compat';
import { importSources } from '@atlaskit/eslint-utils/schema';
import { createLintRuleWithTypedConfig } from '../utils/create-rule-with-typed-config';
import { isSimpleSelector } from '../utils/is-simple-selector';
import { parseSelector } from '../utils/parse-selector';
import { getStyleCalls } from '../utils/style-calls';
import { walkStyleCallProperties } from '../utils/walk-style-call-properties';
import { allowedPseudos } from './constants';
import { lintSelector } from './lint-selector';
import { walkCssMapCall } from './walk-css-map';
var ignoredAtRules = new Set(['@container',
// ignored because it's covered by `no-container-queries`
'@media',
// ignored because it's covered by `no-nested-styles`
'@supports', '@property', '@starting-style']);
var canonicalMediaQueries = new Map([['media-above-xxs', '@media all'], ['media-above-xs', '@media (min-width: 30rem)'], ['media-above-sm', '@media (min-width: 48rem)'], ['media-above-md', '@media (min-width: 64rem)'], ['media-above-lg', '@media (min-width: 90rem)'], ['media-above-xl', '@media (min-width: 110.5rem)'], ['media-below-xs', '@media not all and (min-width: 30rem)'], ['media-below-sm', '@media not all and (min-width: 48rem)'], ['media-below-md', '@media not all and (min-width: 64rem)'], ['media-below-lg', '@media not all and (min-width: 90rem)'], ['media-below-xl', '@media not all and (min-width: 110.5rem)'], ['media-only-xxs', '@media (min-width: 0rem) and (max-width: 29.99rem)'], ['media-only-xs', '@media (min-width: 30rem) and (max-width: 47.99rem)'], ['media-only-sm', '@media (min-width: 48rem) and (max-width: 63.99rem)'], ['media-only-md', '@media (min-width: 64rem) and (max-width: 89.99rem)'], ['media-only-lg', '@media (min-width: 90rem) and (max-width: 110.49rem)'], ['media-only-xl', '@media (min-width: 110.5rem)'], ['media-dark-mode', '@media (prefers-color-scheme: dark)'], ['media-light-mode', '@media (prefers-color-scheme: light)'], ['media-reduced-motion', '@media (prefers-reduced-motion: reduce)'], ['media-reduced-transparency', '@media (prefers-reduced-transparency: reduce)'], ['media-forced-colors-active', '@media screen and (forced-colors: active)'], ['media-legacy-high-contrast', '@media screen and (-ms-high-contrast: active)'], ['media-forced-colors-or-legacy-high-contrast', '@media screen and (forced-colors: active), screen and (-ms-high-contrast: active)']]);
var unwrapTypeScriptExpression = function unwrapTypeScriptExpression(node) {
var expression = node;
while (expression.type === 'TSAsExpression' || expression.type === 'TSTypeAssertion' || expression.type === 'TSNonNullExpression' || expression.type === 'TSSatisfiesExpression') {
expression = expression.expression;
}
return expression;
};
var isCanonicalTypedMediaKey = function isCanonicalTypedMediaKey(node, importedMediaTypes) {
if (node.type !== 'TSSatisfiesExpression') {
return false;
}
var expression = unwrapTypeScriptExpression(node);
if (expression.type !== 'Literal' || typeof expression.value !== 'string') {
return false;
}
var satisfiesExpression = node;
while (((_satisfiesExpression = satisfiesExpression) === null || _satisfiesExpression === void 0 ? void 0 : _satisfiesExpression.type) === 'TSSatisfiesExpression') {
var _satisfiesExpression, _satisfiesExpression$, _satisfiesExpression$2;
var typeName = (_satisfiesExpression$ = satisfiesExpression.typeAnnotation) === null || _satisfiesExpression$ === void 0 || (_satisfiesExpression$ = _satisfiesExpression$.typeName) === null || _satisfiesExpression$ === void 0 ? void 0 : _satisfiesExpression$.name;
if (((_satisfiesExpression$2 = satisfiesExpression.typeAnnotation) === null || _satisfiesExpression$2 === void 0 ? void 0 : _satisfiesExpression$2.type) === 'TSTypeReference' && typeName) {
var source = importedMediaTypes.get(typeName);
if (source && canonicalMediaQueries.get(source) === expression.value) {
return true;
}
}
satisfiesExpression = satisfiesExpression.expression;
}
return false;
};
var getImportedMediaTypeSources = function getImportedMediaTypeSources(context) {
var importedMediaTypes = new Map();
var _iterator = _createForOfIteratorHelper(getSourceCode(context).ast.body),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var statement = _step.value;
if (statement.type !== 'ImportDeclaration' || typeof statement.source.value !== 'string') {
continue;
}
var match = statement.source.value.match(/^@atlaskit\/css\/at-rules\/(media-[a-z-]+)$/);
if (match) {
var _iterator2 = _createForOfIteratorHelper(statement.specifiers),
_step2;
try {
for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
var specifier = _step2.value;
if (specifier.type === 'ImportDefaultSpecifier') {
importedMediaTypes.set(specifier.local.name, match[1]);
}
}
} catch (err) {
_iterator2.e(err);
} finally {
_iterator2.f();
}
}
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
return importedMediaTypes;
};
var rule = createLintRuleWithTypedConfig({
meta: {
name: 'no-unsafe-selectors',
docs: {
description: 'Disallows use of nested styles in `css` functions.',
recommended: true,
severity: 'error'
},
fixable: 'code',
hasSuggestions: true,
messages: {
/**
* Selector messages
*/
'no-ambiguous-pseudos': "Prefix the pseudo-selector with & when it targets this element (for example &:hover); apply descendant styles directly to the child instead.",
'no-increased-specificity': "Replace chained nesting selectors such as && with one & selector, then resolve competing styles through local composition.",
'no-restricted-pseudos': "The {{pseudo}} pseudo-selector is unsupported. Use an allowed local pseudo such as &:hover, or pass explicit state as a prop and select a local cssMap variant.",
'no-legacy-pseudo-element-syntax': "Use a double colon for pseudo-elements, for example &::before or &::after, instead of the legacy single-colon syntax.",
'no-unparsable-selectors': "The selector '{{selectorText}}' could not be parsed. Correct its CSS syntax; if it is valid, report the selector and reproduction to #help-ui-styling-standard.",
'no-selectors-object': "Move selectors out of the selectors object into supported local style keys; apply child styles directly to each child element.",
/**
* At-rule messages
*/
'no-keyframes-at-rules': "Replace the @keyframes at-rule with keyframes() from @compiled/react and reference its result through animationName.",
'no-restricted-at-rules': "{{atRule}} is unsupported. Use a supported @media, @supports or @property rule; for runtime conditions select a local style variant in JavaScript.",
'no-grouped-at-rules': "Flatten grouped at-rules into complete keys, for example \"@media (min-width: 48rem)\" instead of an \"@media\" object containing conditions.",
'no-noncanonical-media-query': "Match the literal query to its imported @atlaskit/css/at-rules type, including the exact breakpoint, units and comparison operator.",
/**
* Suggestion messages
*/
'insert-nesting-selector': "Insert & before this pseudo-selector to target the element itself, for example &:hover."
},
schema: {
type: 'object',
properties: {
importSources: importSources,
shouldAlwaysInsertNestingSelectorForAmbiguousPseudos: {
type: 'boolean',
default: true
}
},
additionalProperties: false
}
},
create: function create(context, config) {
return {
Program: function Program() {
var importedMediaTypes = getImportedMediaTypeSources(context);
var _iterator3 = _createForOfIteratorHelper(getStyleCalls(context)),
_step3;
try {
var _loop = function _loop() {
var styleCall = _step3.value;
if (!config.importSources.includes(styleCall.importSource)) {
return 1; // continue
}
if (styleCall.styleFunction === 'cssMap') {
walkCssMapCall(styleCall.node, function (property) {
var type = property.type,
node = property.node;
if (type === 'grouped-at-rules') {
context.report({
node: node.key,
messageId: 'no-grouped-at-rules'
});
} else if (type === 'selectors') {
context.report({
node: node.key,
messageId: 'no-selectors-object'
});
}
});
}
walkStyleCallProperties(styleCall, function (_ref) {
var key = _ref.key,
value = _ref.value;
var keyType = key.type;
if (value.type !== 'ObjectExpression' || keyType !== 'Literal' && keyType !== 'TSSatisfiesExpression') {
return;
}
if (keyType === 'TSSatisfiesExpression') {
var unwrappedKey = unwrapTypeScriptExpression(key);
if (unwrappedKey.type === 'Literal' && typeof unwrappedKey.value === 'string') {
if (unwrappedKey.value.startsWith('@media') && !isCanonicalTypedMediaKey(key, importedMediaTypes)) {
context.report({
node: key,
messageId: 'no-noncanonical-media-query'
});
}
return;
}
return;
}
var selectorText = key.value;
if (typeof selectorText !== 'string') {
return;
}
if (selectorText.includes('@')) {
lintAtRule({
context: context,
sourceNode: key,
atRule: selectorText
});
return;
}
if (isSimpleSelector(selectorText, {
allowedPseudos: allowedPseudos,
allowLeadingPseudo: styleCall.styleFunction === 'xcss'
})) {
return;
}
try {
var selectorList = parseSelector(context, selectorText);
var _iterator4 = _createForOfIteratorHelper(selectorList.nodes),
_step4;
try {
for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
var selector = _step4.value;
lintSelector({
context: context,
sourceNode: key,
selector: selector,
config: config,
isXcssCall: styleCall.styleFunction === 'xcss'
});
}
} catch (err) {
_iterator4.e(err);
} finally {
_iterator4.f();
}
} catch (_unused) {
context.report({
node: key,
messageId: 'no-unparsable-selectors',
data: {
selectorText: selectorText
}
});
}
});
};
for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
if (_loop()) continue;
}
} catch (err) {
_iterator3.e(err);
} finally {
_iterator3.f();
}
}
};
}
});
export default rule;
function lintAtRule(_ref2) {
var context = _ref2.context,
sourceNode = _ref2.sourceNode,
atRule = _ref2.atRule;
var matches = atRule.trim().match(/^@[A-z-]+/);
if (!matches) {
return;
}
var ruleName = matches[0];
if (ignoredAtRules.has(ruleName)) {
return;
}
if (ruleName === '@keyframes') {
context.report({
node: sourceNode,
messageId: 'no-keyframes-at-rules'
});
} else {
context.report({
node: sourceNode,
messageId: 'no-restricted-at-rules',
data: {
atRule: ruleName
}
});
}
}