| /** |
| * @fileoverview Rule to flag use of .only in tests, preventing focused tests being committed accidentally |
| * @author Levi Buzolic |
| */ |
| |
| /** @typedef {{block?: string[], focus?: string[], functions?: string[], fix?: boolean}} Options */ |
| |
| /** @type {Options} */ |
| const defaultOptions = { |
| block: [ |
| "describe", |
| "it", |
| "context", |
| "test", |
| "tape", |
| "fixture", |
| "serial", |
| "Feature", |
| "Scenario", |
| "Given", |
| "And", |
| "When", |
| "Then", |
| ], |
| focus: ["only"], |
| functions: [], |
| fix: false, |
| }; |
| |
| /** @type {import('eslint').Rule.RuleModule} */ |
| module.exports = { |
| meta: { |
| docs: { |
| description: "disallow .only blocks in tests", |
| category: "Possible Errors", |
| recommended: true, |
| url: "https://github.com/levibuzolic/eslint-plugin-no-only-tests", |
| }, |
| fixable: "code", |
| schema: [ |
| { |
| type: "object", |
| properties: { |
| block: { |
| type: "array", |
| items: { |
| type: "string", |
| }, |
| uniqueItems: true, |
| default: defaultOptions.block, |
| }, |
| focus: { |
| type: "array", |
| items: { |
| type: "string", |
| }, |
| uniqueItems: true, |
| default: defaultOptions.focus, |
| }, |
| functions: { |
| type: "array", |
| items: { |
| type: "string", |
| }, |
| uniqueItems: true, |
| default: defaultOptions.functions, |
| }, |
| fix: { |
| type: "boolean", |
| default: defaultOptions.fix, |
| }, |
| }, |
| additionalProperties: false, |
| }, |
| ], |
| }, |
| create(context) { |
| /** @type {Options} */ |
| const options = Object.assign({}, defaultOptions, context.options[0]); |
| const blocks = options.block || []; |
| const focus = options.focus || []; |
| const functions = options.functions || []; |
| const fix = !!options.fix; |
| |
| return { |
| Identifier(node) { |
| if (functions.length && functions.indexOf(node.name) > -1) { |
| context.report({ |
| node, |
| message: `${node.name} not permitted`, |
| }); |
| } |
| |
| const parentObject = |
| "object" in node.parent ? node.parent.object : undefined; |
| if (parentObject == null) return; |
| if (focus.indexOf(node.name) === -1) return; |
| |
| const callPath = getCallPath(node.parent).join("."); |
| |
| // comparison guarantees that matching is done with the beginning of call path |
| if ( |
| blocks.find((block) => { |
| // Allow wildcard tail matching of blocks when ending in a `*` |
| if (block.endsWith("*")) |
| return callPath.startsWith(block.replace(/\*$/, "")); |
| return callPath.startsWith(`${block}.`); |
| }) |
| ) { |
| const rangeStart = node.range?.[0]; |
| const rangeEnd = node.range?.[1]; |
| |
| context.report({ |
| node, |
| message: `${callPath} not permitted`, |
| fix: |
| fix && rangeStart != null && rangeEnd != null |
| ? (fixer) => fixer.removeRange([rangeStart - 1, rangeEnd]) |
| : undefined, |
| }); |
| } |
| }, |
| }; |
| }, |
| }; |
| |
| /** |
| * |
| * @param {import('estree').Node} node |
| * @param {string[]} path |
| * @returns |
| */ |
| function getCallPath(node, path = []) { |
| if (node) { |
| const nodeName = |
| "name" in node && node.name |
| ? node.name |
| : "property" in node && node.property && "name" in node.property |
| ? node.property?.name |
| : undefined; |
| if ("object" in node && node.object && nodeName) |
| return getCallPath(node.object, [nodeName, ...path]); |
| if ("callee" in node && node.callee) return getCallPath(node.callee, path); |
| if (nodeName) return [nodeName, ...path]; |
| return path; |
| } |
| return path; |
| } |