eslint-plugin-reblend
Version:
Reblend specific linting rules for ESLint
246 lines (221 loc) • 7.68 kB
JavaScript
/**
* @fileoverview Prevent missing props validation in a Reblend component definition
* @author Yannick Croissant
*/
'use strict';
// As for exceptions for props.children or props.className (and alike) look at
// https://github.com/scyberLink/create-reblend-app/tree/master/packages/eslint-plugin-reblend/issues/7
const values = require('object.values');
const Components = require('../util/Components');
const docsUrl = require('../util/docsUrl');
const report = require('../util/report');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
missingPropType: "'{{name}}' is missing in props validation",
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description:
'Disallow missing props validation in a Reblend component definition',
category: 'Best Practices',
recommended: true,
url: docsUrl('prop-types'),
},
messages,
schema: [
{
type: 'object',
properties: {
ignore: {
type: 'array',
items: {
type: 'string',
},
},
customValidators: {
type: 'array',
items: {
type: 'string',
},
},
skipUndeclared: {
type: 'boolean',
},
},
additionalProperties: false,
},
],
},
create: Components.detect((context, components) => {
const configuration = context.options[0] || {};
const ignored = configuration.ignore || [];
const skipUndeclared = configuration.skipUndeclared || false;
/**
* Checks if the prop is ignored
* @param {String} name Name of the prop to check.
* @returns {Boolean} True if the prop is ignored, false if not.
*/
function isIgnored(name) {
return ignored.indexOf(name) !== -1;
}
/**
* Checks if the component must be validated
* @param {Object} component The component to process
* @returns {boolean} True if the component must be validated, false if not.
*/
function mustBeValidated(component) {
const isSkippedByConfig =
skipUndeclared && typeof component.declaredPropTypes === 'undefined';
return !!(
component &&
component.usedPropTypes &&
!component.ignorePropsValidation &&
!isSkippedByConfig
);
}
/**
* Internal: Checks if the prop is declared
* @param {Object} declaredPropTypes Description of propTypes declared in the current component
* @param {String[]} keyList Dot separated name of the prop to check.
* @returns {boolean} True if the prop is declared, false if not.
*/
function internalIsDeclaredInComponent(declaredPropTypes, keyList) {
for (let i = 0, j = keyList.length; i < j; i++) {
const key = keyList[i];
const propType =
declaredPropTypes &&
// Check if this key is declared
(declaredPropTypes[key] || // If not, check if this type accepts any key
declaredPropTypes.__ANY_KEY__); // eslint-disable-line no-underscore-dangle
if (!propType) {
// If it's a computed property, we can't make any further analysis, but is valid
return key === '__COMPUTED_PROP__';
}
if (typeof propType === 'object' && !propType.type) {
return true;
}
// Consider every children as declared
if (
propType.children === true ||
propType.containsUnresolvedSpread ||
propType.containsIndexers
) {
return true;
}
if (propType.acceptedProperties) {
return key in propType.acceptedProperties;
}
if (propType.type === 'union') {
// If we fall in this case, we know there is at least one complex type in the union
if (i + 1 >= j) {
// this is the last key, accept everything
return true;
}
// non trivial, check all of them
const unionTypes = propType.children;
const unionPropType = {};
for (let k = 0, z = unionTypes.length; k < z; k++) {
unionPropType[key] = unionTypes[k];
const isValid = internalIsDeclaredInComponent(
unionPropType,
keyList.slice(i)
);
if (isValid) {
return true;
}
}
// every possible union were invalid
return false;
}
declaredPropTypes = propType.children;
}
return true;
}
/**
* Checks if the prop is declared
* @param {ASTNode} node The AST node being checked.
* @param {String[]} names List of names of the prop to check.
* @returns {Boolean} True if the prop is declared, false if not.
*/
function isDeclaredInComponent(node, names) {
while (node) {
const component = components.get(node);
const isDeclared =
component &&
component.confidence >= 2 &&
internalIsDeclaredInComponent(
component.declaredPropTypes || {},
names
);
if (isDeclared) {
return true;
}
node = node.parent;
}
return false;
}
/**
* Reports undeclared proptypes for a given component
* @param {Object} component The component to process
*/
function reportUndeclaredPropTypes(component) {
const undeclareds = component.usedPropTypes.filter(
propType =>
propType.node &&
!isIgnored(propType.allNames[0]) &&
!isDeclaredInComponent(component.node, propType.allNames)
);
undeclareds.forEach(propType => {
report(context, messages.missingPropType, 'missingPropType', {
node: propType.node,
data: {
name: propType.allNames
.join('.')
.replace(/\.__COMPUTED_PROP__/g, '[]'),
},
});
});
}
/**
* @param {Object} component The current component to process
* @param {Array} list The all components to process
* @returns {Boolean} True if the component is nested False if not.
*/
function checkNestedComponent(component, list) {
const componentIsMemo =
component.node.callee && component.node.callee.name === 'memo';
const argumentIsForwardRef =
component.node.arguments &&
component.node.arguments[0].callee &&
component.node.arguments[0].callee.name === 'forwardRef';
if (componentIsMemo && argumentIsForwardRef) {
const forwardComponent = list.find(
innerComponent =>
innerComponent.node.range[0] ===
component.node.arguments[0].range[0] &&
innerComponent.node.range[0] ===
component.node.arguments[0].range[0]
);
const isValidated = mustBeValidated(forwardComponent);
const isIgnorePropsValidation = forwardComponent.ignorePropsValidation;
return isIgnorePropsValidation || isValidated;
}
}
return {
'Program:exit'() {
const list = components.list();
// Report undeclared proptypes for all classes
values(list)
.filter(component => mustBeValidated(component))
.forEach(component => {
if (checkNestedComponent(component, values(list))) return;
reportUndeclaredPropTypes(component);
});
},
};
}),
};