eslint-plugin-jest
Version:
ESLint rules for Jest
308 lines (292 loc) • 11.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.createPaddingRule = exports.StatementType = exports.PaddingType = void 0;
var _utils = require("@typescript-eslint/utils");
var astUtils = _interopRequireWildcard(require("./ast-utils"));
var _misc = require("./misc");
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
/**
* Require/fix newlines around jest functions
*
* Based on eslint/padding-line-between-statements by Toru Nagashima
* See: https://github.com/eslint/eslint/blob/master/lib/rules/padding-line-between-statements.js
*
* Some helpers borrowed from eslint ast-utils by Gyandeep Singh
* See: https://github.com/eslint/eslint/blob/master/lib/rules/utils/ast-utils.js
*/
// Statement types we'll respond to
let StatementType = exports.StatementType = /*#__PURE__*/function (StatementType) {
StatementType[StatementType["Any"] = 0] = "Any";
StatementType[StatementType["AfterAllToken"] = 1] = "AfterAllToken";
StatementType[StatementType["AfterEachToken"] = 2] = "AfterEachToken";
StatementType[StatementType["BeforeAllToken"] = 3] = "BeforeAllToken";
StatementType[StatementType["BeforeEachToken"] = 4] = "BeforeEachToken";
StatementType[StatementType["DescribeToken"] = 5] = "DescribeToken";
StatementType[StatementType["ExpectToken"] = 6] = "ExpectToken";
StatementType[StatementType["FdescribeToken"] = 7] = "FdescribeToken";
StatementType[StatementType["FitToken"] = 8] = "FitToken";
StatementType[StatementType["ItToken"] = 9] = "ItToken";
StatementType[StatementType["TestToken"] = 10] = "TestToken";
StatementType[StatementType["XdescribeToken"] = 11] = "XdescribeToken";
StatementType[StatementType["XitToken"] = 12] = "XitToken";
StatementType[StatementType["XtestToken"] = 13] = "XtestToken";
return StatementType;
}({});
// Padding type to apply between statements
let PaddingType = exports.PaddingType = /*#__PURE__*/function (PaddingType) {
PaddingType[PaddingType["Any"] = 0] = "Any";
PaddingType[PaddingType["Always"] = 1] = "Always";
return PaddingType;
}({}); // A configuration object for padding type and the two statement types
// Tracks position in scope and prevNode. Used to compare current and prev node
// and then to walk back up to the parent scope or down into the next one.
// And so on...
// Creates a StatementTester to test an ExpressionStatement's first token name
const createTokenTester = tokenName => {
return (node, sourceCode) => {
let activeNode = node;
if (activeNode.type === _utils.AST_NODE_TYPES.ExpressionStatement) {
// In the case of `await`, we actually care about its argument
if (activeNode.expression.type === _utils.AST_NODE_TYPES.AwaitExpression) {
activeNode = activeNode.expression.argument;
}
const token = sourceCode.getFirstToken(activeNode);
return token?.type === _utils.AST_TOKEN_TYPES.Identifier && token.value === tokenName;
}
return false;
};
};
// A mapping of StatementType to StatementTester for... testing statements
const statementTesters = {
[StatementType.Any]: () => true,
[StatementType.AfterAllToken]: createTokenTester('afterAll'),
[StatementType.AfterEachToken]: createTokenTester('afterEach'),
[StatementType.BeforeAllToken]: createTokenTester('beforeAll'),
[StatementType.BeforeEachToken]: createTokenTester('beforeEach'),
[StatementType.DescribeToken]: createTokenTester('describe'),
[StatementType.ExpectToken]: createTokenTester('expect'),
[StatementType.FdescribeToken]: createTokenTester('fdescribe'),
[StatementType.FitToken]: createTokenTester('fit'),
[StatementType.ItToken]: createTokenTester('it'),
[StatementType.TestToken]: createTokenTester('test'),
[StatementType.XdescribeToken]: createTokenTester('xdescribe'),
[StatementType.XitToken]: createTokenTester('xit'),
[StatementType.XtestToken]: createTokenTester('xtest')
};
/**
* Check and report statements for `PaddingType.Always` configuration.
* This autofix inserts a blank line between the given 2 statements.
* If the `prevNode` has trailing comments, it inserts a blank line after the
* trailing comments.
*/
const paddingAlwaysTester = (prevNode, nextNode, paddingContext) => {
const {
sourceCode,
ruleContext
} = paddingContext;
const paddingLines = astUtils.getPaddingLineSequences(prevNode, nextNode, sourceCode);
// We've got some padding lines. Great.
if (paddingLines.length > 0) {
return;
}
// Missing padding line
ruleContext.report({
node: nextNode,
messageId: 'missingPadding',
fix(fixer) {
let prevToken = astUtils.getActualLastToken(sourceCode, prevNode);
const nextToken = sourceCode.getFirstTokenBetween(prevToken, nextNode, {
includeComments: true,
/**
* Skip the trailing comments of the previous node.
* This inserts a blank line after the last trailing comment.
*
* For example:
*
* foo(); // trailing comment.
* // comment.
* bar();
*
* Get fixed to:
*
* foo(); // trailing comment.
*
* // comment.
* bar();
*/
filter(token) {
if (astUtils.areTokensOnSameLine(prevToken, token)) {
prevToken = token;
return false;
}
return true;
}
}) || nextNode;
const insertText = astUtils.areTokensOnSameLine(prevToken, nextToken) ? '\n\n' : '\n';
return fixer.insertTextAfter(prevToken, insertText);
}
});
};
// A mapping of PaddingType to PaddingTester
const paddingTesters = {
[PaddingType.Any]: () => true,
[PaddingType.Always]: paddingAlwaysTester
};
const createScopeInfo = () => {
let scope = null;
// todo: explore seeing if we can refactor to a more TypeScript friendly structure
return {
get prevNode() {
return scope.prevNode;
},
set prevNode(node) {
scope.prevNode = node;
},
enter() {
scope = {
upper: scope,
prevNode: null
};
},
exit() {
scope = scope.upper;
}
};
};
/**
* Check whether the given node matches the statement type
*/
const nodeMatchesType = (node, statementType, paddingContext) => {
let innerStatementNode = node;
const {
sourceCode
} = paddingContext;
// Dig into LabeledStatement body until it's not that anymore
while (innerStatementNode.type === _utils.AST_NODE_TYPES.LabeledStatement) {
innerStatementNode = innerStatementNode.body;
}
// If it's an array recursively check if any of the statement types match
// the node
if (Array.isArray(statementType)) {
return statementType.some(type => nodeMatchesType(innerStatementNode, type, paddingContext));
}
return statementTesters[statementType](innerStatementNode, sourceCode);
};
/**
* Executes matching padding tester for last matched padding config for given
* nodes
*/
const testPadding = (prevNode, nextNode, paddingContext) => {
const {
configs
} = paddingContext;
const testType = type => paddingTesters[type](prevNode, nextNode, paddingContext);
for (let i = configs.length - 1; i >= 0; --i) {
const {
prevStatementType: prevType,
nextStatementType: nextType,
paddingType
} = configs[i];
if (nodeMatchesType(prevNode, prevType, paddingContext) && nodeMatchesType(nextNode, nextType, paddingContext)) {
testType(paddingType);
return;
}
}
// There were no matching padding rules for the prevNode, nextNode,
// paddingType combination... so we'll use PaddingType.Any which is always ok
testType(PaddingType.Any);
};
/**
* Verify padding lines between the given node and the previous node.
*/
const verifyNode = (node, paddingContext) => {
const {
scopeInfo
} = paddingContext;
// NOTE: ESLint types use ESTree which provides a Node type, however
// ESTree.Node doesn't support the parent property which is added by
// ESLint during traversal. Our best bet is to ignore the property access
// here as it's the only place that it's checked.
if (!astUtils.isValidParent(node.parent.type)) {
return;
}
if (scopeInfo.prevNode) {
testPadding(scopeInfo.prevNode, node, paddingContext);
}
scopeInfo.prevNode = node;
};
/**
* Creates an ESLint rule for a given set of padding Config objects.
*
* The algorithm is approximately this:
*
* For each 'scope' in the program
* - Enter the scope (store the parent scope and previous node)
* - For each statement in the scope
* - Check the current node and previous node against the Config objects
* - If the current node and previous node match a Config, check the padding.
* Otherwise, ignore it.
* - If the padding is missing (and required), report and fix
* - Store the current node as the previous
* - Repeat
* - Exit scope (return to parent scope and clear previous node)
*
* The items we're looking for with this rule are ExpressionStatement nodes
* where the first token is an Identifier with a name matching one of the Jest
* functions. It's not foolproof, of course, but it's probably good enough for
* almost all cases.
*
* The Config objects specify a padding type, a previous statement type, and a
* next statement type. Wildcard statement types and padding types are
* supported. The current node and previous node are checked against the
* statement types. If they match then the specified padding type is
* tested/enforced.
*
* See src/index.ts for examples of Config usage.
*/
const createPaddingRule = (name, description, configs, deprecated = false) => {
return (0, _misc.createRule)({
name,
meta: {
docs: {
description
},
fixable: 'whitespace',
deprecated,
messages: {
missingPadding: 'Expected blank line before this statement.'
},
schema: [],
type: 'suggestion'
},
defaultOptions: [],
create(context) {
const paddingContext = {
ruleContext: context,
sourceCode: context.sourceCode,
scopeInfo: createScopeInfo(),
configs
};
const {
scopeInfo
} = paddingContext;
return {
Program: scopeInfo.enter,
'Program:exit': scopeInfo.enter,
BlockStatement: scopeInfo.enter,
'BlockStatement:exit': scopeInfo.exit,
SwitchStatement: scopeInfo.enter,
'SwitchStatement:exit': scopeInfo.exit,
':statement': node => verifyNode(node, paddingContext),
SwitchCase(node) {
verifyNode(node, paddingContext);
scopeInfo.enter();
},
'SwitchCase:exit': scopeInfo.exit
};
}
});
};
exports.createPaddingRule = createPaddingRule;