126 lines
4.7 KiB
JavaScript
126 lines
4.7 KiB
JavaScript
// @ts-check
|
|
/**
|
|
* An ESLint rule that ensures all multiblock pattern builders use
|
|
* `@` char for the controller, ` ` (space) for air and `#` for the 'any' predicate.
|
|
*/
|
|
|
|
import { ESLintUtils } from "@typescript-eslint/utils"
|
|
|
|
const builderFactoryArgs = "definition"
|
|
const builderName = "FactoryBlockPattern"
|
|
|
|
/** @type {Record<string, string>} */
|
|
const rulePredicateCode = {
|
|
"@": `Predicates.controller(Predicates.blocks(${builderFactoryArgs}.get()))`,
|
|
"#": "Predicates.any()",
|
|
" ": "Predicates.air()",
|
|
}
|
|
|
|
/**
|
|
* Finds the root expression in the call chain
|
|
* @param {import('@typescript-eslint/utils').TSESTree.CallExpression} rootCall
|
|
*/
|
|
export function isInMultiblockBuilderCallChain(rootCall) {
|
|
let rootCallee
|
|
for(;;) {
|
|
if(rootCall.callee.type === "MemberExpression")
|
|
rootCallee = rootCall.callee
|
|
else break
|
|
if(rootCallee.object.type === "CallExpression")
|
|
rootCall = rootCallee.object
|
|
else break
|
|
}
|
|
if(!rootCallee) return false
|
|
const o = rootCallee.object
|
|
return o && o.type === "Identifier" && o.name === builderName
|
|
}
|
|
|
|
export default ESLintUtils.RuleCreator.withoutDocs({
|
|
meta: {
|
|
type: "problem",
|
|
hasSuggestions: true,
|
|
fixable: "code",
|
|
messages: {
|
|
"char-not-char": "The first argument must be a single character, like '@'.",
|
|
"not-2-args": "A 'where' call should have 2 arguments.",
|
|
...Object.fromEntries(
|
|
Object.entries(rulePredicateCode).map(([char, code]) => [
|
|
"predicate-for-" + char, `The predicate for '${char}' should be ${code}`
|
|
])
|
|
),
|
|
|
|
"factory-no-args": "Multiblock pattern factory must have a single argument.",
|
|
"factory-args": `Multiblock pattern factory arguments should be '${builderFactoryArgs} =>'`
|
|
},
|
|
schema: [],
|
|
},
|
|
defaultOptions: [],
|
|
create(context) {
|
|
return {
|
|
CallExpression(node) {
|
|
if(node.callee.type !== "MemberExpression")
|
|
return
|
|
if(node.callee.property.type !== "Identifier" || node.callee.property.name !== "where")
|
|
return
|
|
if(!isInMultiblockBuilderCallChain(node))
|
|
return
|
|
// Now we are sure we are dealing with a multiblock pattern builder
|
|
|
|
if(node.arguments.length !== 2)
|
|
return context.report({
|
|
messageId: "not-2-args",
|
|
node: node
|
|
})
|
|
|
|
const [char, predicate] = node.arguments
|
|
|
|
// First argument check
|
|
if(char.type !== "Literal" || typeof char.value !== "string" || char.value.length !== 1)
|
|
return context.report({
|
|
messageId: "char-not-char",
|
|
node: char
|
|
})
|
|
|
|
// Second argument check
|
|
const predicateCodeActual = context.sourceCode.text.slice(...predicate.range)
|
|
const predicateCodeShouldBe = rulePredicateCode[char.value]
|
|
if(predicateCodeShouldBe && predicateCodeActual !== predicateCodeShouldBe)
|
|
context.report({
|
|
// @ts-ignore It's not smart enough
|
|
messageId: "predicate-for-" + char.value,
|
|
node: predicate,
|
|
fix: fixer => [
|
|
fixer.replaceText(predicate, predicateCodeShouldBe)
|
|
]
|
|
})
|
|
},
|
|
ArrowFunctionExpression(node) {
|
|
if(node.body.type !== "CallExpression")
|
|
return
|
|
if(!isInMultiblockBuilderCallChain(node.body))
|
|
return
|
|
// Now we are sure we are dealing with a multiblock factory function
|
|
|
|
if(node.params.length < 1)
|
|
return context.report({
|
|
messageId: "factory-no-args",
|
|
node: node
|
|
})
|
|
|
|
/** @type {[number, number]} */
|
|
const paramsRange = [
|
|
node.params[0].range[0],
|
|
node.params.at(-1).range[1]
|
|
]
|
|
if(context.sourceCode.text.slice(...paramsRange) !== builderFactoryArgs)
|
|
return context.report({
|
|
messageId: "factory-args",
|
|
node: node.params[0],
|
|
fix: fixer => [
|
|
fixer.replaceTextRange(paramsRange, builderFactoryArgs)
|
|
]
|
|
})
|
|
}
|
|
}
|
|
},
|
|
})
|