@atlaskit/eslint-plugin-design-system
Version:
The essential plugin for use with the Atlassian Design System.
185 lines (178 loc) • 8.15 kB
JavaScript
import { isNodeOfType } from 'eslint-codemod-utils';
import { JSXAttribute } from '../../ast-nodes/jsx-attribute';
import { JSXElementHelper } from '../../ast-nodes/jsx-element-helper';
import { createLintRule } from '../utils/create-lint-rule';
var DATE_PICKER = 'DatePicker';
var DATETIME_PICKER = 'DateTimePicker';
var PROP_NAME = 'shouldShowCalendarButton';
var DATETIME_PICKER_PACKAGE = '@atlaskit/datetime-picker';
var DATE_PICKER_IMPORT_SOURCE = '@atlaskit/datetime-picker/date-picker';
var DATE_TIME_PICKER_IMPORT_SOURCE = '@atlaskit/datetime-picker/date-time-picker';
// Lint rule message
var message = '`shouldShowCalendarButton` should be set to `true` to make date picker accessible.';
// Fix messages
export var addCalendarButtonProp = 'Add `shouldShowCalendarButton` prop.';
export var setCalendarButtonPropToTrue = 'Set `shouldShowCalendarButton` prop to `true`.';
export var addCalendarButtonProperty = 'Add `shouldShowCalendarButton: true` to `datePickerProps`.';
export var setCalendarButtonPropertyToTrue = 'Set `shouldShowCalendarButton` property in `datePickerProps` to `true`.';
var datePickerJSXElement = function datePickerJSXElement(node, context) {
var prop = JSXElementHelper.getAttributeByName(node, PROP_NAME);
// If the prop exists
if (prop) {
var attrValue = JSXAttribute.getValue(prop);
// If the value is a boolean with value `false`
if ((attrValue === null || attrValue === void 0 ? void 0 : attrValue.type) === 'ExpressionStatement Literal' && (attrValue === null || attrValue === void 0 ? void 0 : attrValue.value) === false) {
return context.report({
node: prop,
messageId: 'datePickerCalendarButtonShouldBeShown',
suggest: [{
desc: setCalendarButtonPropToTrue,
fix: function fix(fixer) {
return [fixer.replaceText(prop, PROP_NAME)];
}
}]
});
}
// If the prop does not exist
} else {
return context.report({
node: node.openingElement,
messageId: 'datePickerMissingCalendarButtonProp',
suggest: [{
desc: addCalendarButtonProp,
fix: function fix(fixer) {
return [fixer.insertTextAfter(node.openingElement.name, " ".concat(PROP_NAME))];
}
}]
});
}
};
var dateTimePickerJSXElement = function dateTimePickerJSXElement(node, context) {
var _datePickerProp$value, _expression$propertie;
var datePickerProp = JSXElementHelper.getAttributeByName(node, 'datePickerProps');
// If the `datePickerProps` prop does not exist or is not an expression
// container (the latter being essentially unprecedented, against type
// guidelines, and useless)
if (!datePickerProp || ((_datePickerProp$value = datePickerProp.value) === null || _datePickerProp$value === void 0 ? void 0 : _datePickerProp$value.type) !== 'JSXExpressionContainer') {
return context.report({
node: node.openingElement,
messageId: 'dateTimePickerMissingCalendarButtonProp',
suggest: [{
desc: addCalendarButtonProperty,
fix: function fix(fixer) {
return [fixer.insertTextAfter(node.openingElement.name, " datePickerProps={{ ".concat(PROP_NAME, ": true }}"))];
}
}]
});
}
// Had to cast all these things because ESLint can't hang here. The
// types are just abjectly wrong from what I can log.
var expression = datePickerProp.value.expression;
// If it is not an analyzable expression, like a variable or something, skip
// it.
if (!isNodeOfType(datePickerProp.value.expression, 'ObjectExpression')) {
return;
}
var prop = expression === null || expression === void 0 || (_expression$propertie = expression.properties) === null || _expression$propertie === void 0 ? void 0 : _expression$propertie.find(function (property) {
return property.type === 'Property' && property.key.name === PROP_NAME;
});
// If the `shouldShowCalendarButton` prop does not exist in `datePickerProps`
if (!prop) {
return context.report({
node: datePickerProp,
messageId: 'dateTimePickerMissingCalendarButtonProp',
suggest: [{
desc: addCalendarButtonProperty,
fix: function fix(fixer) {
// If it has existing properties
if (expression.properties.length > 0) {
// Needs following comma to not disrupt existing properties inside `datePickerProps`
return [fixer.insertTextBefore(expression.properties[0], "".concat(PROP_NAME, ": true,"))];
// Else it's an empty object
} else {
return [fixer.replaceText(expression, "{ ".concat(PROP_NAME, ": true }"))];
}
}
}]
});
// If the `shouldShowCalendarButton` property exists and it's value is `false`
} else if (isNodeOfType(prop.value, 'Literal') && prop.value.value !== true) {
return context.report({
node: datePickerProp,
messageId: 'dateTimePickerCalendarButtonShouldBeShown',
suggest: [{
desc: setCalendarButtonPropertyToTrue,
// Needs following comma to not disrupt existing properties inside `datePickerProps`
fix: function fix(fixer) {
return [fixer.replaceText(prop, "".concat(PROP_NAME, ": true,"))];
}
}]
});
}
};
var rule = createLintRule({
meta: {
name: 'use-datetime-picker-calendar-button',
type: 'suggestion',
fixable: 'code',
hasSuggestions: true,
docs: {
description: "Encourages makers to use calendar button in Atlassian Design System's date picker and datetime picker components.",
recommended: true,
severity: 'warn'
},
messages: {
dateTimePickerMissingCalendarButtonProp: "In `datePickerProps`, ".concat(message),
datePickerMissingCalendarButtonProp: message,
dateTimePickerCalendarButtonShouldBeShown: "In `datePickerProps`, ".concat(message),
datePickerCalendarButtonShouldBeShown: message
}
},
create: function create(context) {
// List of component's locally imported names that match
var contextLocalIdentifier = [];
var contextImportedIdentifier = [];
return {
// Only run rule in files where the package is imported
ImportDeclaration: function ImportDeclaration(node) {
var _node$specifiers;
(_node$specifiers = node.specifiers) === null || _node$specifiers === void 0 || _node$specifiers.forEach(function (spec) {
var _spec$imported;
if (node.source.value === DATETIME_PICKER_PACKAGE && isNodeOfType(spec, 'ImportSpecifier') && 'name' in spec.imported && [DATE_PICKER, DATETIME_PICKER].includes((_spec$imported = spec.imported) === null || _spec$imported === void 0 ? void 0 : _spec$imported.name)) {
contextLocalIdentifier.push(spec.local.name);
contextImportedIdentifier.push(spec.imported.name);
}
if (node.source.value === DATE_PICKER_IMPORT_SOURCE && isNodeOfType(spec, 'ImportDefaultSpecifier')) {
contextLocalIdentifier.push(spec.local.name);
contextImportedIdentifier.push(DATE_PICKER);
}
if (node.source.value === DATE_TIME_PICKER_IMPORT_SOURCE && isNodeOfType(spec, 'ImportDefaultSpecifier')) {
contextLocalIdentifier.push(spec.local.name);
contextImportedIdentifier.push(DATETIME_PICKER);
}
});
},
JSXElement: function JSXElement(node) {
if (!isNodeOfType(node, 'JSXElement')) {
return;
}
if (!isNodeOfType(node.openingElement.name, 'JSXIdentifier')) {
return;
}
var name = node.openingElement.name.name;
var localIndex = contextLocalIdentifier.indexOf(name);
// If this component does not match what we're looking for, quit early
if (localIndex === -1) {
return;
}
var importedName = contextImportedIdentifier[localIndex];
if (importedName === DATE_PICKER) {
return datePickerJSXElement(node, context);
} else if (importedName === DATETIME_PICKER) {
return dateTimePickerJSXElement(node, context);
}
}
};
}
});
export default rule;