208 lines
5.3 KiB
JavaScript
208 lines
5.3 KiB
JavaScript
module.exports = {
|
|
meta: {
|
|
type: 'layout',
|
|
schema: [],
|
|
},
|
|
create( context ) {
|
|
const comments = context.getSourceCode().getAllComments();
|
|
|
|
/**
|
|
* Locality classification of an import, "External" or "Internal".
|
|
*
|
|
* @typedef {string} WCPackageLocality
|
|
*/
|
|
|
|
/**
|
|
* Given a desired locality, generates the expected comment node value
|
|
* property.
|
|
*
|
|
* @param {WCPackageLocality} locality Desired package locality.
|
|
*
|
|
* @return {string} Expected comment node value.
|
|
*/
|
|
function getCommentValue( locality ) {
|
|
return `*\n * ${ locality } dependencies\n `;
|
|
}
|
|
|
|
/**
|
|
* Given an import source string, returns the locality classification
|
|
* of the import sort.
|
|
*
|
|
* @param {string} source Import source string.
|
|
*
|
|
* @return {WCPackageLocality} Package locality.
|
|
*/
|
|
function getPackageLocality( source ) {
|
|
if ( source.startsWith( '.' ) || source.startsWith( '~/' ) ) {
|
|
return 'Internal';
|
|
}
|
|
return 'External';
|
|
}
|
|
|
|
/**
|
|
* Returns true if the given comment node satisfies a desired locality,
|
|
* or false otherwise.
|
|
*
|
|
* @param {espree.Node} node Comment node to check.
|
|
* @param {WCPackageLocality} locality Desired package locality.
|
|
*
|
|
* @return {boolean} Whether comment node satisfies locality.
|
|
*/
|
|
function isLocalityDependencyBlock( node, locality ) {
|
|
const { type, value } = node;
|
|
if ( type !== 'Block' ) {
|
|
return false;
|
|
}
|
|
|
|
// Tolerances:
|
|
// - Normalize `/**` and `/*`
|
|
// - Case insensitive "Dependencies" vs. "dependencies"
|
|
// - Ending period
|
|
// - "Node" dependencies as an alias for External
|
|
|
|
if ( locality === 'External' ) {
|
|
locality = '(External|Node)';
|
|
}
|
|
|
|
const pattern = new RegExp(
|
|
`^\\*?\\n \\* ${ locality } dependencies\\.?\\n $`,
|
|
'i'
|
|
);
|
|
return pattern.test( value );
|
|
}
|
|
|
|
/**
|
|
* Returns true if the given node occurs prior in code to a reference,
|
|
* or false otherwise.
|
|
*
|
|
* @param {espree.Node} node Node to test being before reference.
|
|
* @param {espree.Node} reference Node against which to compare.
|
|
*
|
|
* @return {boolean} Whether node occurs before reference.
|
|
*/
|
|
function isBefore( node, reference ) {
|
|
if ( ! node.range || ! reference.range ) {
|
|
return false;
|
|
}
|
|
return node.range[ 0 ] < reference.range[ 0 ];
|
|
}
|
|
|
|
/**
|
|
* Tests source comments to determine whether a comment exists which
|
|
* satisfies the desired locality. If a match is found and requires no
|
|
* updates, the function returns false. Otherwise, it will return true.
|
|
*
|
|
* @param {espree.Node} node Node to test.
|
|
* @param {WCPackageLocality} locality Desired package locality.
|
|
*
|
|
* @return {boolean} Whether the node is in the correct locality.
|
|
*/
|
|
function isNodeInLocality( node, locality ) {
|
|
const value = getCommentValue( locality );
|
|
|
|
let comment;
|
|
let nextComment;
|
|
for ( let i = 0; i < comments.length; i++ ) {
|
|
comment = comments[ i ];
|
|
nextComment =
|
|
i < comments.length - 1 ? comments[ i + 1 ] : null;
|
|
|
|
if ( nextComment && isBefore( nextComment, node ) ) {
|
|
// If it's not the immediately previous comment, continue.
|
|
continue;
|
|
}
|
|
|
|
if ( ! isBefore( comment, node ) ) {
|
|
// Exhausted options.
|
|
break;
|
|
}
|
|
|
|
if ( ! isLocalityDependencyBlock( comment, locality ) ) {
|
|
// Not usable (either not an block comment, or not one
|
|
// matching a tolerable pattern).
|
|
continue;
|
|
}
|
|
|
|
if ( comment.value === value ) {
|
|
// No change needed. (OK)
|
|
return true;
|
|
}
|
|
|
|
// Found a comment needing correction.
|
|
return false;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Tests if the locality is defined in the correct order (External, WooCommerce, Internal).
|
|
*
|
|
* @param {espree.Node} child Node to test.
|
|
* @param {WCPackageLocality} locality Package locality.
|
|
* @param {WCPackageLocality} previousLocality Previous package locality.
|
|
*/
|
|
function checkLocalityOrder( child, locality, previousLocality ) {
|
|
switch ( locality ) {
|
|
case 'External':
|
|
if ( previousLocality === 'Internal' ) {
|
|
context.report( {
|
|
node: child,
|
|
message: `Expected "External dependencies" to be defined before ${ previousLocality }`,
|
|
} );
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
return {
|
|
Program( node ) {
|
|
let previousLocality = null;
|
|
|
|
// Since we only care to enforce imports which occur at the
|
|
// top-level scope, match on Program and test its children,
|
|
// rather than matching the import nodes directly.
|
|
node.body.forEach( ( child ) => {
|
|
let source;
|
|
switch ( child.type ) {
|
|
case 'ImportDeclaration':
|
|
source = child.source.value;
|
|
break;
|
|
|
|
case 'CallExpression':
|
|
const { callee, arguments: args } = child;
|
|
if (
|
|
callee.name === 'require' &&
|
|
args.length === 1 &&
|
|
args[ 0 ].type === 'Literal' &&
|
|
typeof args[ 0 ].value === 'string'
|
|
) {
|
|
source = args[ 0 ].value;
|
|
}
|
|
break;
|
|
}
|
|
|
|
if ( ! source ) {
|
|
return;
|
|
}
|
|
|
|
const locality = getPackageLocality( source );
|
|
|
|
checkLocalityOrder( child, locality, previousLocality );
|
|
|
|
previousLocality = locality;
|
|
|
|
if ( isNodeInLocality( child, locality ) ) {
|
|
return;
|
|
}
|
|
|
|
context.report( {
|
|
node: child,
|
|
message: `Expected preceding "${ locality } dependencies" comment block`,
|
|
} );
|
|
} );
|
|
},
|
|
};
|
|
},
|
|
};
|