Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add eslint rule for preventing string literals in select/dispatch/useDispatch #28726

Merged
merged 10 commits into from
Feb 9, 2021
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ module.exports = {
},
],
'@wordpress/no-unsafe-wp-apis': 'off',
'@wordpress/no-store-string-literals': 'warn',
gziolo marked this conversation as resolved.
Show resolved Hide resolved
'import/default': 'error',
'import/named': 'error',
'no-restricted-imports': [
Expand Down
70 changes: 70 additions & 0 deletions packages/eslint-plugin/rules/__tests__/no-store-string-literals.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* External dependencies
*/
import { RuleTester } from 'eslint';

/**
* Internal dependencies
*/
import rule from '../no-store-string-literals';

const ruleTester = new RuleTester( {
gziolo marked this conversation as resolved.
Show resolved Hide resolved
parserOptions: {
sourceType: 'module',
ecmaVersion: 6,
},
} );

const valid = [
// Callback functions
`import { createRegistrySelector } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; createRegistrySelector(( select ) => { select(store); });`,
`import { useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; useSelect(( select ) => { select(store); });`,
`import { withSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; withSelect(( select ) => { select(store); });`,
`import { withDispatch } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; withDispatch(( select ) => { select(store); });`,
`import { withDispatch as withDispatchAlias } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; withDispatchAlias(( select ) => { select(store); });`,

// Direct function calls
`import { useDispatch } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; useDispatch( store );`,
`import { dispatch } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; dispatch( store );`,
`import { select } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; select( store );`,
`import { resolveSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; resolveSelect( store );`,
`import { resolveSelect as resolveSelectAlias } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; resolveSelectAlias( store );`,

// Object property function calls
`import { controls } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; controls.select( store );`,
`import { controls } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; controls.dispatch( store );`,
`import { controls } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; controls.resolveSelect( store );`,
`import { controls as controlsAlias } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; controlsAlias.resolveSelect( store );`,
];

const invalid = [
// Callback functions
`import { createRegistrySelector } from '@wordpress/data'; createRegistrySelector(( select ) => { select( 'core' ); });`,
`import { useSelect } from '@wordpress/data'; useSelect(( select ) => { select( 'core' ); });`,
`import { withSelect } from '@wordpress/data'; withSelect(( select ) => { select( 'core' ); });`,
`import { withDispatch } from '@wordpress/data'; withDispatch(( select ) => { select( 'core' ); });`,
`import { withDispatch as withDispatchAlias } from '@wordpress/data'; withDispatchAlias(( select ) => { select( 'core' ); });`,

// Direct function calls
`import { useDispatch } from '@wordpress/data'; useDispatch( 'core' );`,
`import { dispatch } from '@wordpress/data'; dispatch( 'core' );`,
`import { select } from '@wordpress/data'; select( 'core' );`,
`import { resolveSelect } from '@wordpress/data'; resolveSelect( 'core' );`,
`import { resolveSelect as resolveSelectAlias } from '@wordpress/data'; resolveSelectAlias( 'core' );`,

// Object property function calls
`import { controls } from '@wordpress/data'; controls.select( 'core' );`,
`import { controls } from '@wordpress/data'; controls.dispatch( 'core' );`,
`import { controls } from '@wordpress/data'; controls.resolveSelect( 'core' );`,
`import { controls as controlsAlias } from '@wordpress/data'; controlsAlias.resolveSelect( 'core' );`,
];
const errors = [
{
message: `Do not use string literals ( 'core' ) for accessing stores ; import the store and use the store object or store name constant instead`,
},
];

ruleTester.run( 'no-store-string-literals', rule, {
valid: valid.map( ( code ) => ( { code } ) ),
invalid: invalid.map( ( code ) => ( { code, errors } ) ),
} );
139 changes: 139 additions & 0 deletions packages/eslint-plugin/rules/no-store-string-literals.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
function getReferences( context, specifiers ) {
const variables = specifiers.reduce(
( acc, specifier ) =>
acc.concat( context.getDeclaredVariables( specifier ) ),
[]
);
const references = variables.reduce(
( acc, variable ) => acc.concat( variable.references ),
[]
);
return references;
}

function collectAllNodesFromCallbackFunctions( context, node ) {
const functionSpecifiers = node.specifiers.filter(
( specifier ) =>
specifier.imported &&
[
'createRegistrySelector',
'useSelect',
'withSelect',
'withDispatch',
].includes( specifier.imported.name )
);
const functionReferences = getReferences( context, functionSpecifiers );

const functionArgumentVariables = functionReferences.reduce(
( acc, { identifier: { parent } } ) =>
parent && parent.arguments && parent.arguments.length > 0
? acc.concat(
context.getDeclaredVariables( parent.arguments[ 0 ] )
)
: acc,
[]
);
const functionArgumentReferences = functionArgumentVariables.reduce(
( acc, variable ) => acc.concat( variable.references ),
[]
);
const possibleCallExpressionNodes = functionArgumentReferences
.filter( ( reference ) => reference.identifier.parent )
.map( ( reference ) => reference.identifier.parent );

return possibleCallExpressionNodes;
}

function collectAllNodesFromDirectFunctionCalls( context, node ) {
const specifiers = node.specifiers.filter(
( specifier ) =>
specifier.imported &&
[ 'useDispatch', 'dispatch', 'select', 'resolveSelect' ].includes(
specifier.imported.name
)
);
const references = getReferences( context, specifiers );
const possibleCallExpressionNodes = references
.filter( ( reference ) => reference.identifier.parent )
.map( ( reference ) => reference.identifier.parent );

return possibleCallExpressionNodes;
}

function collectAllNodesFromObjectPropertyFunctionCalls( context, node ) {
const specifiers = node.specifiers.filter(
( specifier ) =>
specifier.imported &&
[ 'controls' ].includes( specifier.imported.name )
);
const references = getReferences( context, specifiers );
const referencesWithPropertyCalls = references.filter(
( reference ) =>
reference.identifier.parent.property &&
[ 'select', 'resolveSelect', 'dispatch' ].includes(
reference.identifier.parent.property.name
)
);
const possibleCallExpressionNodes = referencesWithPropertyCalls
.filter(
( reference ) =>
reference.identifier.parent &&
reference.identifier.parent.parent
)
.map( ( reference ) => reference.identifier.parent.parent );

return possibleCallExpressionNodes;
}

module.exports = {
meta: {
type: 'problem',
schema: [],
messages: {
doNotUseStringLiteral: `Do not use string literals ( '{{ argument }}' ) for accessing stores ; import the store and use the store object or store name constant instead`,
gziolo marked this conversation as resolved.
Show resolved Hide resolved
},
},
create( context ) {
return {
ImportDeclaration( node ) {
if ( node.source.value !== '@wordpress/data' ) {
return;
}

const callbackFunctionNodes = collectAllNodesFromCallbackFunctions(
context,
node
);
const directNodes = collectAllNodesFromDirectFunctionCalls(
context,
node
);
const objectPropertyCallNodes = collectAllNodesFromObjectPropertyFunctionCalls(
context,
node
);

const allNodes = [
...callbackFunctionNodes,
...directNodes,
...objectPropertyCallNodes,
];
allNodes
.filter(
( callNode ) =>
callNode &&
callNode.type === 'CallExpression' &&
callNode.arguments.length > 0 &&
callNode.arguments[ 0 ].type === 'Literal'
)
.forEach( ( callNode ) => {
context.report( {
node: callNode.parent,
messageId: 'doNotUseStringLiteral',
data: { argument: callNode.arguments[ 0 ].value },
} );
} );
},
};
},
};