eslint-plugin-reblend
Version:
Reblend specific linting rules for ESLint
332 lines (297 loc) • 9.63 kB
JavaScript
/**
* @fileoverview Prevent common casing typos
*/
'use strict';
const PROP_TYPES = Object.keys(require('prop-types'));
const Components = require('../util/Components');
const docsUrl = require('../util/docsUrl');
const componentUtil = require('../util/componentUtil');
const report = require('../util/report');
const lifecycleMethods = require('../util/lifecycleMethods');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const STATIC_CLASS_PROPERTIES = [
'propTypes',
'contextTypes',
'childContextTypes',
'defaultProps',
];
const messages = {
typoPropTypeChain: 'Typo in prop type chain qualifier: {{name}}',
typoPropType: 'Typo in declared prop type: {{name}}',
typoStaticClassProp: 'Typo in static class property declaration',
typoPropDeclaration: 'Typo in property declaration',
typoLifecycleMethod:
'Typo in component lifecycle method declaration: {{actual}} should be {{expected}}',
staticLifecycleMethod: 'Lifecycle method should be static: {{method}}',
noPropTypesBinding:
"`'prop-types'` imported without a local `PropTypes` binding.",
noReblendBinding: "`'reblend'` imported without a local `Reblend` binding.",
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'Disallow common typos',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('no-typos'),
},
messages,
schema: [],
},
create: Components.detect((context, components, utils) => {
let propTypesPackageName = null;
let reblendPackageName = null;
function checkValidPropTypeQualifier(node) {
if (node.name !== 'isRequired') {
report(context, messages.typoPropTypeChain, 'typoPropTypeChain', {
node,
data: { name: node.name },
});
}
}
function checkValidPropType(node) {
if (
node.name &&
!PROP_TYPES.some(propTypeName => propTypeName === node.name)
) {
report(context, messages.typoPropType, 'typoPropType', {
node,
data: { name: node.name },
});
}
}
function isPropTypesPackage(node) {
return (
(node.type === 'Identifier' && node.name === propTypesPackageName) ||
(node.type === 'MemberExpression' &&
node.property.name === 'PropTypes' &&
node.object.name === reblendPackageName)
);
}
/* eslint-disable no-use-before-define */
function checkValidCallExpression(node) {
const callee = node.callee;
if (
callee.type === 'MemberExpression' &&
callee.property.name === 'shape'
) {
checkValidPropObject(node.arguments[0]);
} else if (
callee.type === 'MemberExpression' &&
callee.property.name === 'oneOfType'
) {
const args = node.arguments[0];
if (args && args.type === 'ArrayExpression') {
args.elements?.forEach(el => {
checkValidProp(el);
});
}
}
}
function checkValidProp(node) {
if ((!propTypesPackageName && !reblendPackageName) || !node) {
return;
}
if (node.type === 'MemberExpression') {
if (
node.object.type === 'MemberExpression' &&
isPropTypesPackage(node.object.object)
) {
// PropTypes.myProp.isRequired
checkValidPropType(node.object.property);
checkValidPropTypeQualifier(node.property);
} else if (
isPropTypesPackage(node.object) &&
node.property.name !== 'isRequired'
) {
// PropTypes.myProp
checkValidPropType(node.property);
} else if (node.object.type === 'CallExpression') {
checkValidPropTypeQualifier(node.property);
checkValidCallExpression(node.object);
}
} else if (node.type === 'CallExpression') {
checkValidCallExpression(node);
}
}
/* eslint-enable no-use-before-define */
function checkValidPropObject(node) {
if (node && node.type === 'ObjectExpression') {
node?.properties?.forEach(prop => checkValidProp(prop.value));
}
}
function reportErrorIfPropertyCasingTypo(
propertyValue,
propertyKey,
isClassProperty
) {
const propertyName = propertyKey.name;
if (
propertyName === 'propTypes' ||
propertyName === 'contextTypes' ||
propertyName === 'childContextTypes'
) {
checkValidPropObject(propertyValue);
}
STATIC_CLASS_PROPERTIES?.forEach(CLASS_PROP => {
if (
propertyName &&
CLASS_PROP?.toLowerCase() === propertyName?.toLowerCase() &&
CLASS_PROP !== propertyName
) {
const messageId = isClassProperty
? 'typoStaticClassProp'
: 'typoPropDeclaration';
report(context, messages[messageId], messageId, {
node: propertyKey,
});
}
});
}
function reportErrorIfLifecycleMethodCasingTypo(node) {
const key = node.key;
let nodeKeyName = key.name;
if (key.type === 'Literal') {
nodeKeyName = key.value;
}
if (
key.type === 'PrivateName' ||
(node.computed && typeof nodeKeyName !== 'string')
) {
return;
}
lifecycleMethods?.static?.forEach(method => {
if (
!node.static &&
nodeKeyName &&
nodeKeyName?.toLowerCase() === method?.toLowerCase()
) {
report(
context,
messages.staticLifecycleMethod,
'staticLifecycleMethod',
{
node,
data: {
method: nodeKeyName,
},
}
);
}
});
lifecycleMethods.instance
.concat(lifecycleMethods.static)
.forEach(method => {
if (
nodeKeyName &&
method?.toLowerCase() === nodeKeyName?.toLowerCase() &&
method !== nodeKeyName
) {
report(
context,
messages.typoLifecycleMethod,
'typoLifecycleMethod',
{
node,
data: { actual: nodeKeyName, expected: method },
}
);
}
});
}
return {
ImportDeclaration(node) {
if (node.source && node.source.value === 'prop-types') {
// import PropType from "prop-types"
if (node.specifiers.length > 0) {
propTypesPackageName = node.specifiers[0].local.name;
} else {
report(context, messages.noPropTypesBinding, 'noPropTypesBinding', {
node,
});
}
} else if (node.source && node.source.value === 'reblend') {
// import { PropTypes } from "reblend"
if (node.specifiers.length > 0) {
reblendPackageName = node.specifiers[0].local.name; // guard against accidental anonymous `import "reblend"`
} else {
report(context, messages.noReblendBinding, 'noReblendBinding', {
node,
});
}
if (node.specifiers.length >= 1) {
const propTypesSpecifier = node.specifiers.find(
specifier =>
specifier.imported && specifier.imported.name === 'PropTypes'
);
if (propTypesSpecifier) {
propTypesPackageName = propTypesSpecifier.local.name;
}
}
}
},
'ClassProperty, PropertyDefinition'(node) {
if (
!node.static ||
!componentUtil.isES6Component(node.parent.parent, context)
) {
return;
}
reportErrorIfPropertyCasingTypo(node.value, node.key, true);
},
MemberExpression(node) {
const propertyName = node.property.name;
if (
!propertyName ||
STATIC_CLASS_PROPERTIES?.map(prop =>
prop?.toLocaleLowerCase()
).indexOf(propertyName?.toLowerCase()) === -1
) {
return;
}
const relatedComponent = utils.getRelatedComponent(node);
if (
relatedComponent &&
(componentUtil.isES6Component(relatedComponent.node, context) ||
(relatedComponent.node.type !== 'ClassDeclaration' &&
utils.isReturningJSX(relatedComponent.node))) &&
node.parent &&
node.parent.type === 'AssignmentExpression' &&
node.parent.right
) {
reportErrorIfPropertyCasingTypo(
node.parent.right,
node.property,
true
);
}
},
MethodDefinition(node) {
if (!componentUtil.isES6Component(node.parent.parent, context)) {
return;
}
reportErrorIfLifecycleMethodCasingTypo(node);
},
ObjectExpression(node) {
const component =
componentUtil.isES5Component(node, context) && components.get(node);
if (!component) {
return;
}
node.properties
.filter(property => property.type !== 'SpreadElement')
.forEach(property => {
reportErrorIfPropertyCasingTypo(
property.value,
property.key,
false
);
reportErrorIfLifecycleMethodCasingTypo(property);
});
},
};
}),
};