/** * Converts store name to variable name. * Removes dashes and uppercases the characters after dashes and appends `Store` at the end. * * @param {string} storeName * @return {string} store name as variable name */ function storeNameToVariableNames( storeName ) { return ( storeName .split( '-' ) .map( ( value, index ) => index === 0 ? value.toLowerCase() : value[ 0 ].toUpperCase() + value.slice( 1 ).toLowerCase() ) .join( '' ) + 'Store' ); } /** * Returns last element of an array. * * @param {Array} array * @return {*} last element of the array */ function arrayLast( array ) { return array[ array.length - 1 ]; } 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', 'useSelect', '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; } function getSuggest( context, callNode ) { return [ { desc: 'Replace literal with store definition. Import store if necessary.', fix: ( fixer ) => getFixes( fixer, context, callNode ), }, ]; } function getFixes( fixer, context, callNode ) { const storeName = callNode.arguments[ 0 ].value; const storeDefinitions = { core: { import: '@wordpress/core-data', variable: 'coreStore', }, }; let storeDefinition = storeDefinitions[ storeName ]; if ( ! storeDefinition && storeName.startsWith( 'core/' ) ) { const storeNameWithoutCore = storeName.substring( 5 ); storeDefinition = { import: `@wordpress/${ storeNameWithoutCore }`, variable: storeNameToVariableNames( storeNameWithoutCore ), }; } if ( ! storeDefinition ) { return null; } const { variable: variableName, import: importName } = storeDefinition; const fixes = [ fixer.replaceText( callNode.arguments[ 0 ], variableName ), ]; const imports = context .getAncestors()[ 0 ] .body.filter( ( node ) => node.type === 'ImportDeclaration' ); const packageImports = imports.filter( ( node ) => node.source.value === importName ); const packageImport = packageImports.length > 0 ? packageImports[ 0 ] : null; if ( packageImport ) { const alreadyHasStore = packageImport.specifiers.some( ( specifier ) => specifier.imported.name === 'store' ); if ( ! alreadyHasStore ) { const lastSpecifier = arrayLast( packageImport.specifiers ); fixes.push( fixer.insertTextAfter( lastSpecifier, `,store as ${ variableName }` ) ); } } else { const wpImports = imports.filter( ( node ) => node.source.value.startsWith( '@wordpress/' ) ); const lastImport = wpImports.length > 0 ? arrayLast( wpImports ) : arrayLast( imports ); fixes.push( fixer.insertTextAfter( lastImport, `\nimport { store as ${ variableName } } from '${ importName }';` ) ); } return fixes; } module.exports = { meta: { type: 'problem', hasSuggestions: true, schema: [], messages: { doNotUseStringLiteral: `Do not use string literals ( '{{ argument }}' ) for accessing @wordpress/data stores. Pass the store definition instead`, }, }, 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 }, suggest: getSuggest( context, callNode ), } ); } ); }, }; }, };