Initial commit

This commit is contained in:
2025-03-07 19:22:02 +01:00
commit 4a98255d83
55743 changed files with 5280367 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
/**
* @fileoverview Detects color literals
* @author Aaron Greenwald
*/
'use strict';
const util = require('util');
const Components = require('../util/Components');
const styleSheet = require('../util/stylesheet');
const { StyleSheets } = styleSheet;
const { astHelpers } = styleSheet;
const create = Components.detect((context) => {
const styleSheets = new StyleSheets();
function reportColorLiterals(colorLiterals) {
if (colorLiterals) {
colorLiterals.forEach((style) => {
if (style) {
const expression = util.inspect(style.expression);
context.report({
node: style.node,
message: 'Color literal: {{expression}}',
data: { expression },
});
}
});
}
}
return {
CallExpression: (node) => {
if (astHelpers.isStyleSheetDeclaration(node, context.settings)) {
const styles = astHelpers.getStyleDeclarations(node);
if (styles) {
styles.forEach((style) => {
const literals = astHelpers.collectColorLiterals(style.value, context);
styleSheets.addColorLiterals(literals);
});
}
}
},
JSXAttribute: (node) => {
if (astHelpers.isStyleAttribute(node)) {
const literals = astHelpers.collectColorLiterals(node.value, context);
styleSheets.addColorLiterals(literals);
}
},
'Program:exit': () => reportColorLiterals(styleSheets.getColorLiterals()),
};
});
module.exports = {
meta: {
schema: [],
},
create,
};
+50
View File
@@ -0,0 +1,50 @@
/**
* @fileoverview Detects inline styles
* @author Aaron Greenwald
*/
'use strict';
const util = require('util');
const Components = require('../util/Components');
const styleSheet = require('../util/stylesheet');
const { StyleSheets } = styleSheet;
const { astHelpers } = styleSheet;
const create = Components.detect((context) => {
const styleSheets = new StyleSheets();
function reportInlineStyles(inlineStyles) {
if (inlineStyles) {
inlineStyles.forEach((style) => {
if (style) {
const expression = util.inspect(style.expression);
context.report({
node: style.node,
message: 'Inline style: {{expression}}',
data: { expression },
});
}
});
}
}
return {
JSXAttribute: (node) => {
if (astHelpers.isStyleAttribute(node)) {
const styles = astHelpers.collectStyleObjectExpressions(node.value, context);
styleSheets.addObjectExpressions(styles);
}
},
'Program:exit': () => reportInlineStyles(styleSheets.getObjectExpressions()),
};
});
module.exports = {
meta: {
schema: [],
},
create,
};
+125
View File
@@ -0,0 +1,125 @@
/**
* @fileoverview Detects raw text outside of Text component
* @author Alex Zhukov
*/
'use strict';
const elementName = (node) => {
const reversedIdentifiers = [];
if (
node.type === 'JSXElement'
&& node.openingElement.type === 'JSXOpeningElement'
) {
let object = node.openingElement.name;
while (object.type === 'JSXMemberExpression') {
if (object.property.type === 'JSXIdentifier') {
reversedIdentifiers.push(object.property.name);
}
object = object.object;
}
if (object.type === 'JSXIdentifier') {
reversedIdentifiers.push(object.name);
}
}
return reversedIdentifiers.reverse().join('.');
};
const hasAllowedParent = (parent, allowedElements) => {
let curNode = parent;
while (curNode) {
if (curNode.type === 'JSXElement') {
const name = elementName(curNode);
if (allowedElements.includes(name)) {
return true;
}
}
curNode = curNode.parent;
}
return false;
};
function create(context) {
const options = context.options[0] || {};
const report = (node) => {
const errorValue = node.type === 'TemplateLiteral'
? `TemplateLiteral: ${node.expressions[0].name}`
: node.value.trim();
const formattedErrorValue = errorValue.length > 0
? `Raw text (${errorValue})`
: 'Whitespace(s)';
context.report({
node,
message: `${formattedErrorValue} cannot be used outside of a <Text> tag`,
});
};
const skippedElements = options.skip ? options.skip : [];
const allowedElements = ['Text', 'TSpan', 'StyledText', 'Animated.Text'].concat(skippedElements);
const hasOnlyLineBreak = (value) => /^[\r\n\t\f\v]+$/.test(value.replace(/ /g, ''));
const getValidation = (node) => !hasAllowedParent(node.parent, allowedElements);
return {
Literal(node) {
const parentType = node.parent.type;
const onlyFor = ['JSXExpressionContainer', 'JSXElement'];
if (typeof node.value !== 'string'
|| hasOnlyLineBreak(node.value)
|| !onlyFor.includes(parentType)
|| (node.parent.parent && node.parent.parent.type === 'JSXAttribute')
) return;
const isStringLiteral = parentType === 'JSXExpressionContainer';
if (getValidation(isStringLiteral ? node.parent : node)) {
report(node);
}
},
JSXText(node) {
if (typeof node.value !== 'string' || hasOnlyLineBreak(node.value)) return;
if (getValidation(node)) {
report(node);
}
},
TemplateLiteral(node) {
if (
node.parent.type !== 'JSXExpressionContainer'
|| (node.parent.parent && node.parent.parent.type === 'JSXAttribute')
) return;
if (getValidation(node.parent)) {
report(node);
}
},
};
}
module.exports = {
meta: {
schema: [
{
type: 'object',
properties: {
skip: {
type: 'array',
items: {
type: 'string',
},
},
},
additionalProperties: false,
},
],
},
create,
};
@@ -0,0 +1,48 @@
/**
* @fileoverview Enforce no single element style arrays
* @author Michael Gall
*/
'use strict';
module.exports = {
meta: {
docs: {
description:
'Disallow single element style arrays. These cause unnecessary re-renders as the identity of the array always changes',
category: 'Stylistic Issues',
recommended: false,
url: '',
},
fixable: 'code',
},
create(context) {
function reportNode(JSXExpressionNode) {
context.report({
node: JSXExpressionNode,
message:
'Single element style arrays are not necessary and cause unnecessary re-renders',
fix(fixer) {
const realStyleNode = JSXExpressionNode.value.expression.elements[0];
const styleSource = context.getSourceCode().getText(realStyleNode);
return fixer.replaceText(JSXExpressionNode.value.expression, styleSource);
},
});
}
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
JSXAttribute(node) {
if (node.name.name !== 'style') return;
if (!node.value.expression) return;
if (node.value.expression.type !== 'ArrayExpression') return;
if (node.value.expression.elements.length === 1) {
reportNode(node);
}
},
};
},
};
+70
View File
@@ -0,0 +1,70 @@
/**
* @fileoverview Detects unused styles
* @author Tom Hastjarjanto
*/
'use strict';
const Components = require('../util/Components');
const styleSheet = require('../util/stylesheet');
const { StyleSheets } = styleSheet;
const { astHelpers } = styleSheet;
const create = Components.detect((context, components) => {
const styleSheets = new StyleSheets();
const styleReferences = new Set();
function reportUnusedStyles(unusedStyles) {
Object.keys(unusedStyles).forEach((key) => {
if ({}.hasOwnProperty.call(unusedStyles, key)) {
const styles = unusedStyles[key];
styles.forEach((node) => {
const message = [
'Unused style detected: ',
key,
'.',
node.key.name,
].join('');
context.report(node, message);
});
}
});
}
return {
MemberExpression: function (node) {
const styleRef = astHelpers.getPotentialStyleReferenceFromMemberExpression(node);
if (styleRef) {
styleReferences.add(styleRef);
}
},
CallExpression: function (node) {
if (astHelpers.isStyleSheetDeclaration(node, context.settings)) {
const styleSheetName = astHelpers.getStyleSheetName(node);
const styles = astHelpers.getStyleDeclarations(node);
styleSheets.add(styleSheetName, styles);
}
},
'Program:exit': function () {
const list = components.all();
if (Object.keys(list).length > 0) {
styleReferences.forEach((reference) => {
styleSheets.markAsUsed(reference);
});
reportUnusedStyles(styleSheets.getUnusedReferences());
}
},
};
});
module.exports = {
meta: {
schema: [],
},
create,
};
+157
View File
@@ -0,0 +1,157 @@
/**
* @fileoverview Rule to require StyleSheet object keys to be sorted
* @author Mats Byrkjeland
*/
'use strict';
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const { astHelpers } = require('../util/stylesheet');
const {
getStyleDeclarationsChunks,
getPropertiesChunks,
getStylePropertyIdentifier,
isStyleSheetDeclaration,
isEitherShortHand,
} = astHelpers;
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
function create(context) {
const order = context.options[0] || 'asc';
const options = context.options[1] || {};
const { ignoreClassNames } = options;
const { ignoreStyleProperties } = options;
const isValidOrder = order === 'asc' ? (a, b) => a <= b : (a, b) => a >= b;
const sourceCode = context.getSourceCode();
function sort(array) {
return [].concat(array).sort((a, b) => {
const identifierA = getStylePropertyIdentifier(a);
const identifierB = getStylePropertyIdentifier(b);
let sortOrder = 0;
if (isEitherShortHand(identifierA, identifierB)) {
return a.range[0] - b.range[0];
} if (identifierA < identifierB) {
sortOrder = -1;
} else if (identifierA > identifierB) {
sortOrder = 1;
}
return sortOrder * (order === 'asc' ? 1 : -1);
});
}
function report(array, type, node, prev, current) {
const currentName = getStylePropertyIdentifier(current);
const prevName = getStylePropertyIdentifier(prev);
const hasComments = array
.map((prop) => [...sourceCode.getCommentsBefore(prop), ...sourceCode.getCommentsAfter(prop)])
.reduce(
(hasComment, comment) => hasComment || comment.length > 0,
false
);
context.report({
node,
message: `Expected ${type} to be in ${order}ending order. '${currentName}' should be before '${prevName}'.`,
loc: current.key.loc,
fix: hasComments ? undefined : (fixer) => {
const sortedArray = sort(array);
return array
.map((item, i) => {
if (item !== sortedArray[i]) {
return fixer.replaceText(
item,
sourceCode.getText(sortedArray[i])
);
}
return null;
})
.filter(Boolean);
},
});
}
function checkIsSorted(array, arrayName, node) {
for (let i = 1; i < array.length; i += 1) {
const previous = array[i - 1];
const current = array[i];
if (previous.type !== 'Property' || current.type !== 'Property') {
return;
}
const prevName = getStylePropertyIdentifier(previous);
const currentName = getStylePropertyIdentifier(current);
const oneIsShorthandForTheOther = arrayName === 'style properties' && isEitherShortHand(prevName, currentName);
if (!oneIsShorthandForTheOther && !isValidOrder(prevName, currentName)) {
return report(array, arrayName, node, previous, current);
}
}
}
return {
CallExpression: function (node) {
if (!isStyleSheetDeclaration(node, context.settings)) {
return;
}
const classDefinitionsChunks = getStyleDeclarationsChunks(node);
if (!ignoreClassNames) {
classDefinitionsChunks.forEach((classDefinitions) => {
checkIsSorted(classDefinitions, 'class names', node);
});
}
if (ignoreStyleProperties) return;
classDefinitionsChunks.forEach((classDefinitions) => {
classDefinitions.forEach((classDefinition) => {
const styleProperties = classDefinition.value.properties;
if (!styleProperties || styleProperties.length < 2) {
return;
}
const stylePropertyChunks = getPropertiesChunks(styleProperties);
stylePropertyChunks.forEach((stylePropertyChunk) => {
checkIsSorted(stylePropertyChunk, 'style properties', node);
});
});
});
},
};
}
module.exports = {
meta: {
fixable: 'code',
schema: [
{
enum: ['asc', 'desc'],
},
{
type: 'object',
properties: {
ignoreClassNames: {
type: 'boolean',
},
ignoreStyleProperties: {
type: 'boolean',
},
},
additionalProperties: false,
},
],
},
create,
};
@@ -0,0 +1,97 @@
/**
* @fileoverview Android and IOS components should be
* used in platform specific React Native components.
* @author Tom Hastjarjanto
*/
'use strict';
function create(context) {
let reactComponents = [];
const androidMessage = 'Android components should be placed in android files';
const iosMessage = 'IOS components should be placed in ios files';
const conflictMessage = 'IOS and Android components can\'t be mixed';
const iosPathRegex = context.options[0] && context.options[0].iosPathRegex
? new RegExp(context.options[0].iosPathRegex)
: /\.ios\.[j|t]sx?$/;
const androidPathRegex = context.options[0] && context.options[0].androidPathRegex
? new RegExp(context.options[0].androidPathRegex)
: /\.android\.[j|t]sx?$/;
function getName(node) {
if (node.type === 'Property') {
const key = node.key || node.argument;
return key.type === 'Identifier' ? key.name : key.value;
} if (node.type === 'Identifier') {
return node.name;
}
}
function hasNodeWithName(nodes, name) {
return nodes.some((node) => {
const nodeName = getName(node);
return nodeName && nodeName.includes(name);
});
}
function reportErrors(components, filename) {
const containsAndroidAndIOS = (
hasNodeWithName(components, 'IOS')
&& hasNodeWithName(components, 'Android')
);
components.forEach((node) => {
const propName = getName(node);
if (propName.includes('IOS') && !filename.match(iosPathRegex)) {
context.report(node, containsAndroidAndIOS ? conflictMessage : iosMessage);
}
if (propName.includes('Android') && !filename.match(androidPathRegex)) {
context.report(node, containsAndroidAndIOS ? conflictMessage : androidMessage);
}
});
}
return {
VariableDeclarator: function (node) {
const destructuring = node.init && node.id && node.id.type === 'ObjectPattern';
const statelessDestructuring = destructuring && node.init.name === 'React';
if (destructuring && statelessDestructuring) {
reactComponents = reactComponents.concat(node.id.properties);
}
},
ImportDeclaration: function (node) {
if (node.source.value === 'react-native') {
node.specifiers.forEach((importSpecifier) => {
if (importSpecifier.type === 'ImportSpecifier') {
reactComponents = reactComponents.concat(importSpecifier.imported);
}
});
}
},
'Program:exit': function () {
const filename = context.getFilename();
reportErrors(reactComponents, filename);
},
};
}
module.exports = {
meta: {
fixable: 'code',
schema: [{
type: 'object',
properties: {
androidPathRegex: {
type: 'string',
},
iosPathRegex: {
type: 'string',
},
},
additionalProperties: false,
}],
},
create,
};
+407
View File
@@ -0,0 +1,407 @@
/**
* @fileoverview Utility class and functions for React components detection
* @author Yannick Croissant
*/
'use strict';
/**
* Components
* @class
*/
function Components() {
this.list = {};
this.getId = function (node) {
return node && node.range.join(':');
};
}
/**
* Add a node to the components list, or update it if it's already in the list
*
* @param {ASTNode} node The AST node being added.
* @param {Number} confidence Confidence in the component detection (0=banned, 1=maybe, 2=yes)
*/
Components.prototype.add = function (node, confidence) {
const id = this.getId(node);
if (this.list[id]) {
if (confidence === 0 || this.list[id].confidence === 0) {
this.list[id].confidence = 0;
} else {
this.list[id].confidence = Math.max(this.list[id].confidence, confidence);
}
return;
}
this.list[id] = {
node: node,
confidence: confidence,
};
};
/**
* Find a component in the list using its node
*
* @param {ASTNode} node The AST node being searched.
* @returns {Object} Component object, undefined if the component is not found
*/
Components.prototype.get = function (node) {
const id = this.getId(node);
return this.list[id];
};
/**
* Update a component in the list
*
* @param {ASTNode} node The AST node being updated.
* @param {Object} props Additional properties to add to the component.
*/
Components.prototype.set = function (node, props) {
let currentNode = node;
while (currentNode && !this.list[this.getId(currentNode)]) {
currentNode = node.parent;
}
if (!currentNode) {
return;
}
const id = this.getId(currentNode);
this.list[id] = { ...this.list[id], ...props };
};
/**
* Return the components list
* Components for which we are not confident are not returned
*
* @returns {Object} Components list
*/
Components.prototype.all = function () {
const list = {};
Object.keys(this.list).forEach((i) => {
if ({}.hasOwnProperty.call(this.list, i) && this.list[i].confidence >= 2) {
list[i] = this.list[i];
}
});
return list;
};
/**
* Return the length of the components list
* Components for which we are not confident are not counted
*
* @returns {Number} Components list length
*/
Components.prototype.length = function () {
let length = 0;
Object.keys(this.list).forEach((i) => {
if ({}.hasOwnProperty.call(this.list, i) && this.list[i].confidence >= 2) {
length += 1;
}
});
return length;
};
function componentRule(rule, context, _node) {
const sourceCode = context.getSourceCode();
const components = new Components();
// Utilities for component detection
const utils = {
/**
* Check if the node is a React ES5 component
*
* @param {ASTNode} node The AST node being checked.
* @returns {Boolean} True if the node is a React ES5 component, false if not
*/
isES5Component: function (node) {
if (!node.parent) {
return false;
}
return /^(React\.)?createClass$/.test(sourceCode.getText(node.parent.callee));
},
/**
* Check if the node is a React ES6 component
*
* @param {ASTNode} node The AST node being checked.
* @returns {Boolean} True if the node is a React ES6 component, false if not
*/
isES6Component: function (node) {
if (!node.superClass) {
return false;
}
return /^(React\.)?(Pure)?Component$/.test(sourceCode.getText(node.superClass));
},
/**
* Check if the node is returning JSX
*
* @param {ASTNode} node The AST node being checked (must be a ReturnStatement).
* @returns {Boolean} True if the node is returning JSX, false if not
*/
isReturningJSX: function (node) {
let property;
switch (node.type) {
case 'ReturnStatement':
property = 'argument';
break;
case 'ArrowFunctionExpression':
property = 'body';
break;
default:
return false;
}
const returnsJSX = node[property]
&& (node[property].type === 'JSXElement' || node[property].type === 'JSXFragment');
const returnsReactCreateElement = node[property]
&& node[property].callee
&& node[property].callee.property
&& node[property].callee.property.name === 'createElement';
return Boolean(returnsJSX || returnsReactCreateElement);
},
/**
* Get the parent component node from the current scope
*
* @returns {ASTNode} component node, null if we are not in a component
*/
getParentComponent: function (_n) {
return (
utils.getParentES6Component(_n)
|| utils.getParentES5Component(_n)
|| utils.getParentStatelessComponent(_n)
);
},
/**
* Get the parent ES5 component node from the current scope
*
* @returns {ASTNode} component node, null if we are not in a component
*/
getParentES5Component: function (_n) {
// eslint-disable-next-line react/destructuring-assignment
let scope = (context.sourceCode || context).getScope(_n);
while (scope) {
const node = scope.block && scope.block.parent && scope.block.parent.parent;
if (node && utils.isES5Component(node)) {
return node;
}
scope = scope.upper;
}
return null;
},
/**
* Get the parent ES6 component node from the current scope
*
* @returns {ASTNode} component node, null if we are not in a component
*/
getParentES6Component: function (_n) {
let scope = (context.sourceCode || context).getScope(_n);
while (scope && scope.type !== 'class') {
scope = scope.upper;
}
const node = scope && scope.block;
if (!node || !utils.isES6Component(node)) {
return null;
}
return node;
},
/**
* Get the parent stateless component node from the current scope
*
* @returns {ASTNode} component node, null if we are not in a component
*/
getParentStatelessComponent: function (_n) {
// eslint-disable-next-line react/destructuring-assignment
let scope = (context.sourceCode || context).getScope(_n);
while (scope) {
const node = scope.block;
// Ignore non functions
const isFunction = /Function/.test(node.type);
// Ignore classes methods
const isNotMethod = !node.parent || node.parent.type !== 'MethodDefinition';
// Ignore arguments (callback, etc.)
const isNotArgument = !node.parent || node.parent.type !== 'CallExpression';
if (isFunction && isNotMethod && isNotArgument) {
return node;
}
scope = scope.upper;
}
return null;
},
/**
* Get the related component from a node
*
* @param {ASTNode} node The AST node being checked (must be a MemberExpression).
* @returns {ASTNode} component node, null if we cannot find the component
*/
getRelatedComponent: function (node) {
let currentNode = node;
let i;
let j;
let k;
let l;
// Get the component path
const componentPath = [];
while (currentNode) {
if (currentNode.property && currentNode.property.type === 'Identifier') {
componentPath.push(currentNode.property.name);
}
if (currentNode.object && currentNode.object.type === 'Identifier') {
componentPath.push(currentNode.object.name);
}
currentNode = currentNode.object;
}
componentPath.reverse();
// Find the variable in the current scope
const variableName = componentPath.shift();
if (!variableName) {
return null;
}
let variableInScope;
const { variables } = (context.sourceCode || context).getScope(_node);
for (i = 0, j = variables.length; i < j; i++) { // eslint-disable-line no-plusplus
if (variables[i].name === variableName) {
variableInScope = variables[i];
break;
}
}
if (!variableInScope) {
return null;
}
// Find the variable declaration
let defInScope;
const { defs } = variableInScope;
for (i = 0, j = defs.length; i < j; i++) { // eslint-disable-line no-plusplus
if (
defs[i].type === 'ClassName'
|| defs[i].type === 'FunctionName'
|| defs[i].type === 'Variable'
) {
defInScope = defs[i];
break;
}
}
if (!defInScope) {
return null;
}
currentNode = defInScope.node.init || defInScope.node;
// Traverse the node properties to the component declaration
for (i = 0, j = componentPath.length; i < j; i++) { // eslint-disable-line no-plusplus
if (!currentNode.properties) {
continue; // eslint-disable-line no-continue
}
for (k = 0, l = currentNode.properties.length; k < l; k++) { // eslint-disable-line no-plusplus, max-len
if (currentNode.properties[k].key.name === componentPath[i]) {
currentNode = currentNode.properties[k];
break;
}
}
if (!currentNode) {
return null;
}
currentNode = currentNode.value;
}
// Return the component
return components.get(currentNode);
},
};
// Component detection instructions
const detectionInstructions = {
ClassDeclaration: function (node) {
if (!utils.isES6Component(node)) {
return;
}
components.add(node, 2);
},
ClassProperty: function (_n) {
const node = utils.getParentComponent(_n);
if (!node) {
return;
}
components.add(node, 2);
},
ObjectExpression: function (node) {
if (!utils.isES5Component(node)) {
return;
}
components.add(node, 2);
},
FunctionExpression: function (_n) {
const node = utils.getParentComponent(_n);
if (!node) {
return;
}
components.add(node, 1);
},
FunctionDeclaration: function (_n) {
const node = utils.getParentComponent(_n);
if (!node) {
return;
}
components.add(node, 1);
},
ArrowFunctionExpression: function (_n) {
const node = utils.getParentComponent(_n);
if (!node) {
return;
}
if (node.expression && utils.isReturningJSX(node)) {
components.add(node, 2);
} else {
components.add(node, 1);
}
},
ThisExpression: function (_n) {
const node = utils.getParentComponent(_n);
if (!node || !/Function/.test(node.type)) {
return;
}
// Ban functions with a ThisExpression
components.add(node, 0);
},
ReturnStatement: function (node) {
if (!utils.isReturningJSX(node)) {
return;
}
const parentNode = utils.getParentComponent(node);
if (!parentNode) {
return;
}
components.add(parentNode, 2);
},
};
// Update the provided rule instructions to add the component detection
const ruleInstructions = rule(context, components, utils);
const updatedRuleInstructions = { ...ruleInstructions };
Object.keys(detectionInstructions).forEach((instruction) => {
updatedRuleInstructions[instruction] = (node) => {
detectionInstructions[instruction](node);
return ruleInstructions[instruction] ? ruleInstructions[instruction](node) : undefined;
};
});
// Return the updated rule instructions
return updatedRuleInstructions;
}
Components.detect = function (rule) {
return componentRule.bind(this, rule);
};
module.exports = Components;
+492
View File
@@ -0,0 +1,492 @@
'use strict';
/**
* StyleSheets represents the StyleSheets found in the source code.
* @constructor
*/
function StyleSheets() {
this.styleSheets = {};
}
/**
* Add adds a StyleSheet to our StyleSheets collections.
*
* @param {string} styleSheetName - The name of the StyleSheet.
* @param {object} properties - The collection of rules in the styleSheet.
*/
StyleSheets.prototype.add = function (styleSheetName, properties) {
this.styleSheets[styleSheetName] = properties;
};
/**
* MarkAsUsed marks a rule as used in our source code by removing it from the
* specified StyleSheet rules.
*
* @param {string} fullyQualifiedName - The fully qualified name of the rule.
* for example 'styles.text'
*/
StyleSheets.prototype.markAsUsed = function (fullyQualifiedName) {
const nameSplit = fullyQualifiedName.split('.');
const styleSheetName = nameSplit[0];
const styleSheetProperty = nameSplit[1];
if (this.styleSheets[styleSheetName]) {
this.styleSheets[styleSheetName] = this
.styleSheets[styleSheetName]
.filter((property) => property.key.name !== styleSheetProperty);
}
};
/**
* GetUnusedReferences returns all collected StyleSheets and their
* unmarked rules.
*/
StyleSheets.prototype.getUnusedReferences = function () {
return this.styleSheets;
};
/**
* AddColorLiterals adds an array of expressions that contain color literals
* to the ColorLiterals collection
* @param {array} expressions - an array of expressions containing color literals
*/
StyleSheets.prototype.addColorLiterals = function (expressions) {
if (!this.colorLiterals) {
this.colorLiterals = [];
}
this.colorLiterals = this.colorLiterals.concat(expressions);
};
/**
* GetColorLiterals returns an array of collected color literals expressions
* @returns {Array}
*/
StyleSheets.prototype.getColorLiterals = function () {
return this.colorLiterals;
};
/**
* AddObjectexpressions adds an array of expressions to the ObjectExpressions collection
* @param {Array} expressions - an array of expressions containing ObjectExpressions in
* inline styles
*/
StyleSheets.prototype.addObjectExpressions = function (expressions) {
if (!this.objectExpressions) {
this.objectExpressions = [];
}
this.objectExpressions = this.objectExpressions.concat(expressions);
};
/**
* GetObjectExpressions returns an array of collected object expressiosn used in inline styles
* @returns {Array}
*/
StyleSheets.prototype.getObjectExpressions = function () {
return this.objectExpressions;
};
let currentContent;
const getSourceCode = (node) => currentContent
.getSourceCode(node)
.getText(node);
const getStyleSheetObjectNames = (settings) => settings['react-native/style-sheet-object-names'] || ['StyleSheet'];
const astHelpers = {
containsStyleSheetObject: function (node, objectNames) {
return Boolean(
node
&& node.type === 'CallExpression'
&& node.callee
&& node.callee.object
&& node.callee.object.name
&& objectNames.includes(node.callee.object.name)
);
},
containsCreateCall: function (node) {
return Boolean(
node
&& node.callee
&& node.callee.property
&& node.callee.property.name === 'create'
);
},
isStyleSheetDeclaration: function (node, settings) {
const objectNames = getStyleSheetObjectNames(settings);
return Boolean(
astHelpers.containsStyleSheetObject(node, objectNames)
&& astHelpers.containsCreateCall(node)
);
},
getStyleSheetName: function (node) {
if (node && node.parent && node.parent.id) {
return node.parent.id.name;
}
},
getStyleDeclarations: function (node) {
if (
node
&& node.type === 'CallExpression'
&& node.arguments
&& node.arguments[0]
&& node.arguments[0].properties
) {
return node.arguments[0].properties.filter((property) => property.type === 'Property');
}
return [];
},
getStyleDeclarationsChunks: function (node) {
if (
node
&& node.type === 'CallExpression'
&& node.arguments
&& node.arguments[0]
&& node.arguments[0].properties
) {
const { properties } = node.arguments[0];
const result = [];
let chunk = [];
for (let i = 0; i < properties.length; i += 1) {
const property = properties[i];
if (property.type === 'Property') {
chunk.push(property);
} else if (chunk.length) {
result.push(chunk);
chunk = [];
}
}
if (chunk.length) {
result.push(chunk);
}
return result;
}
return [];
},
getPropertiesChunks: function (properties) {
const result = [];
let chunk = [];
for (let i = 0; i < properties.length; i += 1) {
const property = properties[i];
if (property.type === 'Property') {
chunk.push(property);
} else if (chunk.length) {
result.push(chunk);
chunk = [];
}
}
if (chunk.length) {
result.push(chunk);
}
return result;
},
getExpressionIdentifier: function (node) {
if (node) {
switch (node.type) {
case 'Identifier':
return node.name;
case 'Literal':
return node.value;
case 'TemplateLiteral':
return node.quasis.reduce(
(result, quasi, index) => result
+ quasi.value.cooked
+ astHelpers.getExpressionIdentifier(node.expressions[index]),
''
);
default:
return '';
}
}
return '';
},
getStylePropertyIdentifier: function (node) {
if (
node
&& node.key
) {
return astHelpers.getExpressionIdentifier(node.key);
}
},
isStyleAttribute: function (node) {
return Boolean(
node.type === 'JSXAttribute'
&& node.name
&& node.name.name
&& node.name.name.toLowerCase().includes('style')
);
},
collectStyleObjectExpressions: function (node, context) {
currentContent = context;
if (astHelpers.hasArrayOfStyleReferences(node)) {
const styleReferenceContainers = node
.expression
.elements;
return astHelpers.collectStyleObjectExpressionFromContainers(
styleReferenceContainers
);
} if (node && node.expression) {
return astHelpers.getStyleObjectExpressionFromNode(node.expression);
}
return [];
},
collectColorLiterals: function (node, context) {
if (!node) {
return [];
}
currentContent = context;
if (astHelpers.hasArrayOfStyleReferences(node)) {
const styleReferenceContainers = node
.expression
.elements;
return astHelpers.collectColorLiteralsFromContainers(
styleReferenceContainers
);
}
if (node.type === 'ObjectExpression') {
return astHelpers.getColorLiteralsFromNode(node);
}
return astHelpers.getColorLiteralsFromNode(node.expression);
},
collectStyleObjectExpressionFromContainers: function (nodes) {
let objectExpressions = [];
nodes.forEach((node) => {
objectExpressions = objectExpressions
.concat(astHelpers.getStyleObjectExpressionFromNode(node));
});
return objectExpressions;
},
collectColorLiteralsFromContainers: function (nodes) {
let colorLiterals = [];
nodes.forEach((node) => {
colorLiterals = colorLiterals
.concat(astHelpers.getColorLiteralsFromNode(node));
});
return colorLiterals;
},
getStyleReferenceFromNode: function (node) {
let styleReference;
let leftStyleReferences;
let rightStyleReferences;
if (!node) {
return [];
}
switch (node.type) {
case 'MemberExpression':
styleReference = astHelpers.getStyleReferenceFromExpression(node);
return [styleReference];
case 'LogicalExpression':
leftStyleReferences = astHelpers.getStyleReferenceFromNode(node.left);
rightStyleReferences = astHelpers.getStyleReferenceFromNode(node.right);
return [].concat(leftStyleReferences).concat(rightStyleReferences);
case 'ConditionalExpression':
leftStyleReferences = astHelpers.getStyleReferenceFromNode(node.consequent);
rightStyleReferences = astHelpers.getStyleReferenceFromNode(node.alternate);
return [].concat(leftStyleReferences).concat(rightStyleReferences);
default:
return [];
}
},
getStyleObjectExpressionFromNode: function (node) {
let leftStyleObjectExpression;
let rightStyleObjectExpression;
if (!node) {
return [];
}
if (node.type === 'ObjectExpression') {
return [astHelpers.getStyleObjectFromExpression(node)];
}
switch (node.type) {
case 'LogicalExpression':
leftStyleObjectExpression = astHelpers.getStyleObjectExpressionFromNode(node.left);
rightStyleObjectExpression = astHelpers.getStyleObjectExpressionFromNode(node.right);
return [].concat(leftStyleObjectExpression).concat(rightStyleObjectExpression);
case 'ConditionalExpression':
leftStyleObjectExpression = astHelpers.getStyleObjectExpressionFromNode(node.consequent);
rightStyleObjectExpression = astHelpers.getStyleObjectExpressionFromNode(node.alternate);
return [].concat(leftStyleObjectExpression).concat(rightStyleObjectExpression);
default:
return [];
}
},
getColorLiteralsFromNode: function (node) {
let leftColorLiterals;
let rightColorLiterals;
if (!node) {
return [];
}
if (node.type === 'ObjectExpression') {
return [astHelpers.getColorLiteralsFromExpression(node)];
}
switch (node.type) {
case 'LogicalExpression':
leftColorLiterals = astHelpers.getColorLiteralsFromNode(node.left);
rightColorLiterals = astHelpers.getColorLiteralsFromNode(node.right);
return [].concat(leftColorLiterals).concat(rightColorLiterals);
case 'ConditionalExpression':
leftColorLiterals = astHelpers.getColorLiteralsFromNode(node.consequent);
rightColorLiterals = astHelpers.getColorLiteralsFromNode(node.alternate);
return [].concat(leftColorLiterals).concat(rightColorLiterals);
default:
return [];
}
},
hasArrayOfStyleReferences: function (node) {
return node && Boolean(
node.type === 'JSXExpressionContainer'
&& node.expression
&& node.expression.type === 'ArrayExpression'
);
},
getStyleReferenceFromExpression: function (node) {
const result = [];
const name = astHelpers.getObjectName(node);
if (name) {
result.push(name);
}
const property = astHelpers.getPropertyName(node);
if (property) {
result.push(property);
}
return result.join('.');
},
getStyleObjectFromExpression: function (node) {
const obj = {};
let invalid = false;
if (node.properties && node.properties.length) {
node.properties.forEach((p) => {
if (!p.value || !p.key) {
return;
}
if (p.value.type === 'Literal') {
invalid = true;
obj[p.key.name] = p.value.value;
} else if (p.value.type === 'ConditionalExpression') {
const innerNode = p.value;
if (innerNode.consequent.type === 'Literal' || innerNode.alternate.type === 'Literal') {
invalid = true;
obj[p.key.name] = getSourceCode(innerNode);
}
} else if (p.value.type === 'UnaryExpression' && p.value.operator === '-' && p.value.argument.type === 'Literal') {
invalid = true;
obj[p.key.name] = -1 * p.value.argument.value;
} else if (p.value.type === 'UnaryExpression' && p.value.operator === '+' && p.value.argument.type === 'Literal') {
invalid = true;
obj[p.key.name] = p.value.argument.value;
}
});
}
return invalid ? { expression: obj, node: node } : undefined;
},
getColorLiteralsFromExpression: function (node) {
const obj = {};
let invalid = false;
if (node.properties && node.properties.length) {
node.properties.forEach((p) => {
if (p.key && p.key.name && p.key.name.toLowerCase().indexOf('color') !== -1) {
if (p.value.type === 'Literal') {
invalid = true;
obj[p.key.name] = p.value.value;
} else if (p.value.type === 'ConditionalExpression') {
const innerNode = p.value;
if (innerNode.consequent.type === 'Literal' || innerNode.alternate.type === 'Literal') {
invalid = true;
obj[p.key.name] = getSourceCode(innerNode);
}
}
}
});
}
return invalid ? { expression: obj, node: node } : undefined;
},
getObjectName: function (node) {
if (
node
&& node.object
&& node.object.name
) {
return node.object.name;
}
},
getPropertyName: function (node) {
if (
node
&& node.property
&& node.property.name
) {
return node.property.name;
}
},
getPotentialStyleReferenceFromMemberExpression: function (node) {
if (
node
&& node.object
&& node.object.type === 'Identifier'
&& node.object.name
&& node.property
&& node.property.type === 'Identifier'
&& node.property.name
&& node.parent.type !== 'MemberExpression'
) {
return [node.object.name, node.property.name].join('.');
}
},
isEitherShortHand: function (property1, property2) {
const shorthands = ['margin', 'padding', 'border', 'flex'];
if (shorthands.includes(property1)) {
return property2.startsWith(property1);
} if (shorthands.includes(property2)) {
return property1.startsWith(property2);
}
return false;
},
};
module.exports.astHelpers = astHelpers;
module.exports.StyleSheets = StyleSheets;
+99
View File
@@ -0,0 +1,99 @@
/**
* @fileoverview Utility functions for React components detection
* @author Yannick Croissant
*/
'use strict';
/**
* Record that a particular variable has been used in code
*
* @param {String} name The name of the variable to mark as used.
* @returns {Boolean} True if the variable was found and marked as used, false if not.
*/
function markVariableAsUsed(context, name, _node) {
let scope = (context.sourceCode || context).getScope(_node);
let variables;
let i;
let len;
let found = false;
// Special Node.js scope means we need to start one level deeper
if (scope.type === 'global') {
while (scope.childScopes.length) {
([scope] = scope.childScopes);
}
}
do {
variables = scope.variables;
for (i = 0, len = variables.length; i < len; i++) { // eslint-disable-line no-plusplus
if (variables[i].name === name) {
variables[i].eslintUsed = true;
found = true;
}
}
scope = scope.upper;
} while (scope);
return found;
}
/**
* Search a particular variable in a list
* @param {Array} variables The variables list.
* @param {Array} name The name of the variable to search.
* @returns {Boolean} True if the variable was found, false if not.
*/
function findVariable(variables, name) {
let i;
let len;
for (i = 0, len = variables.length; i < len; i++) { // eslint-disable-line no-plusplus
if (variables[i].name === name) {
return true;
}
}
return false;
}
function getScope(context, node) {
if (context.sourceCode && context.sourceCode.getScope) {
return context.sourceCode.getScope(node);
}
return context.getScope();
}
/**
* List all variable in a given scope
*
* Contain a patch for babel-eslint to avoid https://github.com/babel/babel-eslint/issues/21
*
* @param {Object} context The current rule context.
* @param {Array} name The name of the variable to search.
* @returns {Boolean} True if the variable was found, false if not.
*/
function variablesInScope(context, _n) {
let scope = getScope(context, _n);
let { variables } = scope;
while (scope.type !== 'global') {
scope = scope.upper;
variables = scope.variables.concat(variables);
}
if (scope.childScopes.length) {
variables = scope.childScopes[0].variables.concat(variables);
if (scope.childScopes[0].childScopes.length) {
variables = scope.childScopes[0].childScopes[0].variables.concat(variables);
}
}
return variables;
}
module.exports = {
findVariable: findVariable,
variablesInScope: variablesInScope,
markVariableAsUsed: markVariableAsUsed,
};