mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 00:20:04 +01:00
[eslint-plugin-react-hooks] updates for component syntax (#33089)
Adds support for Flow's component and hook syntax. [docs](https://flow.org/en/docs/react/component-syntax/)
This commit is contained in:
parent
dc2b11817b
commit
4c4a57c4f9
|
|
@ -34,7 +34,7 @@ function normalizeIndent(strings) {
|
|||
// }
|
||||
// ***************************************************
|
||||
|
||||
const tests = {
|
||||
const allTests = {
|
||||
valid: [
|
||||
{
|
||||
code: normalizeIndent`
|
||||
|
|
@ -44,6 +44,25 @@ const tests = {
|
|||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
syntax: 'flow',
|
||||
code: normalizeIndent`
|
||||
// Component syntax
|
||||
component Button() {
|
||||
useHook();
|
||||
return <div>Button!</div>;
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
syntax: 'flow',
|
||||
code: normalizeIndent`
|
||||
// Hook syntax
|
||||
hook useSampleHook() {
|
||||
useHook();
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
code: normalizeIndent`
|
||||
// Valid because components can use hooks.
|
||||
|
|
@ -563,6 +582,28 @@ const tests = {
|
|||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
syntax: 'flow',
|
||||
code: normalizeIndent`
|
||||
component Button(cond: boolean) {
|
||||
if (cond) {
|
||||
useConditionalHook();
|
||||
}
|
||||
}
|
||||
`,
|
||||
errors: [conditionalError('useConditionalHook')],
|
||||
},
|
||||
{
|
||||
syntax: 'flow',
|
||||
code: normalizeIndent`
|
||||
hook useTest(cond: boolean) {
|
||||
if (cond) {
|
||||
useConditionalHook();
|
||||
}
|
||||
}
|
||||
`,
|
||||
errors: [conditionalError('useConditionalHook')],
|
||||
},
|
||||
{
|
||||
code: normalizeIndent`
|
||||
// Invalid because it's dangerous and might not warn otherwise.
|
||||
|
|
@ -1287,8 +1328,8 @@ const tests = {
|
|||
};
|
||||
|
||||
if (__EXPERIMENTAL__) {
|
||||
tests.valid = [
|
||||
...tests.valid,
|
||||
allTests.valid = [
|
||||
...allTests.valid,
|
||||
{
|
||||
code: normalizeIndent`
|
||||
// Valid because functions created with useEffectEvent can be called in a useEffect.
|
||||
|
|
@ -1385,8 +1426,8 @@ if (__EXPERIMENTAL__) {
|
|||
`,
|
||||
},
|
||||
];
|
||||
tests.invalid = [
|
||||
...tests.invalid,
|
||||
allTests.invalid = [
|
||||
...allTests.invalid,
|
||||
{
|
||||
code: normalizeIndent`
|
||||
function MyComponent({ theme }) {
|
||||
|
|
@ -1536,7 +1577,7 @@ function asyncComponentHookError(fn) {
|
|||
if (!process.env.CI) {
|
||||
let only = [];
|
||||
let skipped = [];
|
||||
[...tests.valid, ...tests.invalid].forEach(t => {
|
||||
[...allTests.valid, ...allTests.invalid].forEach(t => {
|
||||
if (t.skip) {
|
||||
delete t.skip;
|
||||
skipped.push(t);
|
||||
|
|
@ -1555,10 +1596,23 @@ if (!process.env.CI) {
|
|||
}
|
||||
return true;
|
||||
};
|
||||
tests.valid = tests.valid.filter(predicate);
|
||||
tests.invalid = tests.invalid.filter(predicate);
|
||||
allTests.valid = allTests.valid.filter(predicate);
|
||||
allTests.invalid = allTests.invalid.filter(predicate);
|
||||
}
|
||||
|
||||
function filteredTests(predicate) {
|
||||
return {
|
||||
valid: allTests.valid.filter(predicate),
|
||||
invalid: allTests.invalid.filter(predicate),
|
||||
};
|
||||
}
|
||||
|
||||
const flowTests = filteredTests(t => t.syntax == null || t.syntax === 'flow');
|
||||
const tests = filteredTests(t => t.syntax !== 'flow');
|
||||
|
||||
allTests.valid.forEach(t => delete t.syntax);
|
||||
allTests.invalid.forEach(t => delete t.syntax);
|
||||
|
||||
describe('rules-of-hooks/rules-of-hooks', () => {
|
||||
const parserOptionsV7 = {
|
||||
ecmaFeatures: {
|
||||
|
|
@ -1594,6 +1648,25 @@ describe('rules-of-hooks/rules-of-hooks', () => {
|
|||
tests
|
||||
);
|
||||
|
||||
new ESLintTesterV7({
|
||||
parser: require.resolve('hermes-eslint'),
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
enableExperimentalComponentSyntax: true,
|
||||
},
|
||||
}).run('eslint: v7, parser: hermes-eslint', ReactHooksESLintRule, flowTests);
|
||||
|
||||
new ESLintTesterV9({
|
||||
languageOptions: {
|
||||
...languageOptionsV9,
|
||||
parser: require('hermes-eslint'),
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
enableExperimentalComponentSyntax: true,
|
||||
},
|
||||
},
|
||||
}).run('eslint: v9, parser: hermes-eslint', ReactHooksESLintRule, flowTests);
|
||||
|
||||
new ESLintTesterV7({
|
||||
parser: require.resolve('@typescript-eslint/parser-v2'),
|
||||
parserOptions: parserOptionsV7,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
Copyright OpenJS Foundation and other contributors, <www.openjsf.org>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# Code Path Analyzer
|
||||
|
||||
This code is a forked version of ESLints Code Path Analyzer which includes
|
||||
support for Component Syntax.
|
||||
|
||||
Forked from: https://github.com/eslint/eslint/tree/main/lib/linter/code-path-analysis
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
'use strict';
|
||||
|
||||
function assert(cond) {
|
||||
if (!cond) {
|
||||
throw new Error('Assertion violated.');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = assert;
|
||||
|
|
@ -0,0 +1,802 @@
|
|||
'use strict';
|
||||
|
||||
/* eslint-disable react-internal/no-primitive-constructors */
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// eslint-disable-next-line
|
||||
const assert = require('./assert');
|
||||
// eslint-disable-next-line
|
||||
const CodePath = require('./code-path');
|
||||
// eslint-disable-next-line
|
||||
const CodePathSegment = require('./code-path-segment');
|
||||
// eslint-disable-next-line
|
||||
const IdGenerator = require('./id-generator');
|
||||
|
||||
const breakableTypePattern =
|
||||
/^(?:(?:Do)?While|For(?:In|Of)?|Switch)Statement$/u;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Checks whether or not a given node is a `case` node (not `default` node).
|
||||
* @param {ASTNode} node A `SwitchCase` node to check.
|
||||
* @returns {boolean} `true` if the node is a `case` node (not `default` node).
|
||||
*/
|
||||
function isCaseNode(node) {
|
||||
return Boolean(node.test);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given node appears as the value of a PropertyDefinition node.
|
||||
* @param {ASTNode} node THe node to check.
|
||||
* @returns {boolean} `true` if the node is a PropertyDefinition value,
|
||||
* false if not.
|
||||
*/
|
||||
function isPropertyDefinitionValue(node) {
|
||||
const parent = node.parent;
|
||||
|
||||
return (
|
||||
parent && parent.type === 'PropertyDefinition' && parent.value === node
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given logical operator is taken into account for the code
|
||||
* path analysis.
|
||||
* @param {string} operator The operator found in the LogicalExpression node
|
||||
* @returns {boolean} `true` if the operator is "&&" or "||" or "??"
|
||||
*/
|
||||
function isHandledLogicalOperator(operator) {
|
||||
return operator === '&&' || operator === '||' || operator === '??';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given assignment operator is a logical assignment operator.
|
||||
* Logical assignments are taken into account for the code path analysis
|
||||
* because of their short-circuiting semantics.
|
||||
* @param {string} operator The operator found in the AssignmentExpression node
|
||||
* @returns {boolean} `true` if the operator is "&&=" or "||=" or "??="
|
||||
*/
|
||||
function isLogicalAssignmentOperator(operator) {
|
||||
return operator === '&&=' || operator === '||=' || operator === '??=';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the label if the parent node of a given node is a LabeledStatement.
|
||||
* @param {ASTNode} node A node to get.
|
||||
* @returns {string|null} The label or `null`.
|
||||
*/
|
||||
function getLabel(node) {
|
||||
if (node.parent.type === 'LabeledStatement') {
|
||||
return node.parent.label.name;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not a given logical expression node goes different path
|
||||
* between the `true` case and the `false` case.
|
||||
* @param {ASTNode} node A node to check.
|
||||
* @returns {boolean} `true` if the node is a test of a choice statement.
|
||||
*/
|
||||
function isForkingByTrueOrFalse(node) {
|
||||
const parent = node.parent;
|
||||
|
||||
switch (parent.type) {
|
||||
case 'ConditionalExpression':
|
||||
case 'IfStatement':
|
||||
case 'WhileStatement':
|
||||
case 'DoWhileStatement':
|
||||
case 'ForStatement':
|
||||
return parent.test === node;
|
||||
|
||||
case 'LogicalExpression':
|
||||
return isHandledLogicalOperator(parent.operator);
|
||||
|
||||
case 'AssignmentExpression':
|
||||
return isLogicalAssignmentOperator(parent.operator);
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the boolean value of a given literal node.
|
||||
*
|
||||
* This is used to detect infinity loops (e.g. `while (true) {}`).
|
||||
* Statements preceded by an infinity loop are unreachable if the loop didn't
|
||||
* have any `break` statement.
|
||||
* @param {ASTNode} node A node to get.
|
||||
* @returns {boolean|undefined} a boolean value if the node is a Literal node,
|
||||
* otherwise `undefined`.
|
||||
*/
|
||||
function getBooleanValueIfSimpleConstant(node) {
|
||||
if (node.type === 'Literal') {
|
||||
return Boolean(node.value);
|
||||
}
|
||||
return void 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that a given identifier node is a reference or not.
|
||||
*
|
||||
* This is used to detect the first throwable node in a `try` block.
|
||||
* @param {ASTNode} node An Identifier node to check.
|
||||
* @returns {boolean} `true` if the node is a reference.
|
||||
*/
|
||||
function isIdentifierReference(node) {
|
||||
const parent = node.parent;
|
||||
|
||||
switch (parent.type) {
|
||||
case 'LabeledStatement':
|
||||
case 'BreakStatement':
|
||||
case 'ContinueStatement':
|
||||
case 'ArrayPattern':
|
||||
case 'RestElement':
|
||||
case 'ImportSpecifier':
|
||||
case 'ImportDefaultSpecifier':
|
||||
case 'ImportNamespaceSpecifier':
|
||||
case 'CatchClause':
|
||||
return false;
|
||||
|
||||
case 'FunctionDeclaration':
|
||||
case 'ComponentDeclaration':
|
||||
case 'HookDeclaration':
|
||||
case 'FunctionExpression':
|
||||
case 'ArrowFunctionExpression':
|
||||
case 'ClassDeclaration':
|
||||
case 'ClassExpression':
|
||||
case 'VariableDeclarator':
|
||||
return parent.id !== node;
|
||||
|
||||
case 'Property':
|
||||
case 'PropertyDefinition':
|
||||
case 'MethodDefinition':
|
||||
return parent.key !== node || parent.computed || parent.shorthand;
|
||||
|
||||
case 'AssignmentPattern':
|
||||
return parent.key !== node;
|
||||
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current segment with the head segment.
|
||||
* This is similar to local branches and tracking branches of git.
|
||||
*
|
||||
* To separate the current and the head is in order to not make useless segments.
|
||||
*
|
||||
* In this process, both "onCodePathSegmentStart" and "onCodePathSegmentEnd"
|
||||
* events are fired.
|
||||
* @param {CodePathAnalyzer} analyzer The instance.
|
||||
* @param {ASTNode} node The current AST node.
|
||||
* @returns {void}
|
||||
*/
|
||||
function forwardCurrentToHead(analyzer, node) {
|
||||
const codePath = analyzer.codePath;
|
||||
const state = CodePath.getState(codePath);
|
||||
const currentSegments = state.currentSegments;
|
||||
const headSegments = state.headSegments;
|
||||
const end = Math.max(currentSegments.length, headSegments.length);
|
||||
let i, currentSegment, headSegment;
|
||||
|
||||
// Fires leaving events.
|
||||
for (i = 0; i < end; ++i) {
|
||||
currentSegment = currentSegments[i];
|
||||
headSegment = headSegments[i];
|
||||
|
||||
if (currentSegment !== headSegment && currentSegment) {
|
||||
if (currentSegment.reachable) {
|
||||
analyzer.emitter.emit('onCodePathSegmentEnd', currentSegment, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update state.
|
||||
state.currentSegments = headSegments;
|
||||
|
||||
// Fires entering events.
|
||||
for (i = 0; i < end; ++i) {
|
||||
currentSegment = currentSegments[i];
|
||||
headSegment = headSegments[i];
|
||||
|
||||
if (currentSegment !== headSegment && headSegment) {
|
||||
CodePathSegment.markUsed(headSegment);
|
||||
if (headSegment.reachable) {
|
||||
analyzer.emitter.emit('onCodePathSegmentStart', headSegment, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current segment with empty.
|
||||
* This is called at the last of functions or the program.
|
||||
* @param {CodePathAnalyzer} analyzer The instance.
|
||||
* @param {ASTNode} node The current AST node.
|
||||
* @returns {void}
|
||||
*/
|
||||
function leaveFromCurrentSegment(analyzer, node) {
|
||||
const state = CodePath.getState(analyzer.codePath);
|
||||
const currentSegments = state.currentSegments;
|
||||
|
||||
for (let i = 0; i < currentSegments.length; ++i) {
|
||||
const currentSegment = currentSegments[i];
|
||||
if (currentSegment.reachable) {
|
||||
analyzer.emitter.emit('onCodePathSegmentEnd', currentSegment, node);
|
||||
}
|
||||
}
|
||||
|
||||
state.currentSegments = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the code path due to the position of a given node in the parent node
|
||||
* thereof.
|
||||
*
|
||||
* For example, if the node is `parent.consequent`, this creates a fork from the
|
||||
* current path.
|
||||
* @param {CodePathAnalyzer} analyzer The instance.
|
||||
* @param {ASTNode} node The current AST node.
|
||||
* @returns {void}
|
||||
*/
|
||||
function preprocess(analyzer, node) {
|
||||
const codePath = analyzer.codePath;
|
||||
const state = CodePath.getState(codePath);
|
||||
const parent = node.parent;
|
||||
|
||||
switch (parent.type) {
|
||||
// The `arguments.length == 0` case is in `postprocess` function.
|
||||
case 'CallExpression':
|
||||
if (
|
||||
parent.optional === true &&
|
||||
parent.arguments.length >= 1 &&
|
||||
parent.arguments[0] === node
|
||||
) {
|
||||
state.makeOptionalRight();
|
||||
}
|
||||
break;
|
||||
case 'MemberExpression':
|
||||
if (parent.optional === true && parent.property === node) {
|
||||
state.makeOptionalRight();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'LogicalExpression':
|
||||
if (parent.right === node && isHandledLogicalOperator(parent.operator)) {
|
||||
state.makeLogicalRight();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'AssignmentExpression':
|
||||
if (
|
||||
parent.right === node &&
|
||||
isLogicalAssignmentOperator(parent.operator)
|
||||
) {
|
||||
state.makeLogicalRight();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ConditionalExpression':
|
||||
case 'IfStatement':
|
||||
/*
|
||||
* Fork if this node is at `consequent`/`alternate`.
|
||||
* `popForkContext()` exists at `IfStatement:exit` and
|
||||
* `ConditionalExpression:exit`.
|
||||
*/
|
||||
if (parent.consequent === node) {
|
||||
state.makeIfConsequent();
|
||||
} else if (parent.alternate === node) {
|
||||
state.makeIfAlternate();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'SwitchCase':
|
||||
if (parent.consequent[0] === node) {
|
||||
state.makeSwitchCaseBody(false, !parent.test);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'TryStatement':
|
||||
if (parent.handler === node) {
|
||||
state.makeCatchBlock();
|
||||
} else if (parent.finalizer === node) {
|
||||
state.makeFinallyBlock();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'WhileStatement':
|
||||
if (parent.test === node) {
|
||||
state.makeWhileTest(getBooleanValueIfSimpleConstant(node));
|
||||
} else {
|
||||
assert(parent.body === node);
|
||||
state.makeWhileBody();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'DoWhileStatement':
|
||||
if (parent.body === node) {
|
||||
state.makeDoWhileBody();
|
||||
} else {
|
||||
assert(parent.test === node);
|
||||
state.makeDoWhileTest(getBooleanValueIfSimpleConstant(node));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ForStatement':
|
||||
if (parent.test === node) {
|
||||
state.makeForTest(getBooleanValueIfSimpleConstant(node));
|
||||
} else if (parent.update === node) {
|
||||
state.makeForUpdate();
|
||||
} else if (parent.body === node) {
|
||||
state.makeForBody();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ForInStatement':
|
||||
case 'ForOfStatement':
|
||||
if (parent.left === node) {
|
||||
state.makeForInOfLeft();
|
||||
} else if (parent.right === node) {
|
||||
state.makeForInOfRight();
|
||||
} else {
|
||||
assert(parent.body === node);
|
||||
state.makeForInOfBody();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'AssignmentPattern':
|
||||
/*
|
||||
* Fork if this node is at `right`.
|
||||
* `left` is executed always, so it uses the current path.
|
||||
* `popForkContext()` exists at `AssignmentPattern:exit`.
|
||||
*/
|
||||
if (parent.right === node) {
|
||||
state.pushForkContext();
|
||||
state.forkBypassPath();
|
||||
state.forkPath();
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the code path due to the type of a given node in entering.
|
||||
* @param {CodePathAnalyzer} analyzer The instance.
|
||||
* @param {ASTNode} node The current AST node.
|
||||
* @returns {void}
|
||||
*/
|
||||
function processCodePathToEnter(analyzer, node) {
|
||||
let codePath = analyzer.codePath;
|
||||
let state = codePath && CodePath.getState(codePath);
|
||||
const parent = node.parent;
|
||||
|
||||
/**
|
||||
* Creates a new code path and trigger the onCodePathStart event
|
||||
* based on the currently selected node.
|
||||
* @param {string} origin The reason the code path was started.
|
||||
* @returns {void}
|
||||
*/
|
||||
function startCodePath(origin) {
|
||||
if (codePath) {
|
||||
// Emits onCodePathSegmentStart events if updated.
|
||||
forwardCurrentToHead(analyzer, node);
|
||||
}
|
||||
|
||||
// Create the code path of this scope.
|
||||
codePath = analyzer.codePath = new CodePath({
|
||||
id: analyzer.idGenerator.next(),
|
||||
origin,
|
||||
upper: codePath,
|
||||
onLooped: analyzer.onLooped,
|
||||
});
|
||||
state = CodePath.getState(codePath);
|
||||
|
||||
// Emits onCodePathStart events.
|
||||
analyzer.emitter.emit('onCodePathStart', codePath, node);
|
||||
}
|
||||
|
||||
/*
|
||||
* Special case: The right side of class field initializer is considered
|
||||
* to be its own function, so we need to start a new code path in this
|
||||
* case.
|
||||
*/
|
||||
if (isPropertyDefinitionValue(node)) {
|
||||
startCodePath('class-field-initializer');
|
||||
|
||||
/*
|
||||
* Intentional fall through because `node` needs to also be
|
||||
* processed by the code below. For example, if we have:
|
||||
*
|
||||
* class Foo {
|
||||
* a = () => {}
|
||||
* }
|
||||
*
|
||||
* In this case, we also need start a second code path.
|
||||
*/
|
||||
}
|
||||
|
||||
switch (node.type) {
|
||||
case 'Program':
|
||||
startCodePath('program');
|
||||
break;
|
||||
|
||||
case 'FunctionDeclaration':
|
||||
case 'ComponentDeclaration':
|
||||
case 'HookDeclaration':
|
||||
case 'FunctionExpression':
|
||||
case 'ArrowFunctionExpression':
|
||||
startCodePath('function');
|
||||
break;
|
||||
|
||||
case 'StaticBlock':
|
||||
startCodePath('class-static-block');
|
||||
break;
|
||||
|
||||
case 'ChainExpression':
|
||||
state.pushChainContext();
|
||||
break;
|
||||
case 'CallExpression':
|
||||
if (node.optional === true) {
|
||||
state.makeOptionalNode();
|
||||
}
|
||||
break;
|
||||
case 'MemberExpression':
|
||||
if (node.optional === true) {
|
||||
state.makeOptionalNode();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'LogicalExpression':
|
||||
if (isHandledLogicalOperator(node.operator)) {
|
||||
state.pushChoiceContext(node.operator, isForkingByTrueOrFalse(node));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'AssignmentExpression':
|
||||
if (isLogicalAssignmentOperator(node.operator)) {
|
||||
state.pushChoiceContext(
|
||||
node.operator.slice(0, -1), // removes `=` from the end
|
||||
isForkingByTrueOrFalse(node),
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ConditionalExpression':
|
||||
case 'IfStatement':
|
||||
state.pushChoiceContext('test', false);
|
||||
break;
|
||||
|
||||
case 'SwitchStatement':
|
||||
state.pushSwitchContext(node.cases.some(isCaseNode), getLabel(node));
|
||||
break;
|
||||
|
||||
case 'TryStatement':
|
||||
state.pushTryContext(Boolean(node.finalizer));
|
||||
break;
|
||||
|
||||
case 'SwitchCase':
|
||||
/*
|
||||
* Fork if this node is after the 2st node in `cases`.
|
||||
* It's similar to `else` blocks.
|
||||
* The next `test` node is processed in this path.
|
||||
*/
|
||||
if (parent.discriminant !== node && parent.cases[0] !== node) {
|
||||
state.forkPath();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'WhileStatement':
|
||||
case 'DoWhileStatement':
|
||||
case 'ForStatement':
|
||||
case 'ForInStatement':
|
||||
case 'ForOfStatement':
|
||||
state.pushLoopContext(node.type, getLabel(node));
|
||||
break;
|
||||
|
||||
case 'LabeledStatement':
|
||||
if (!breakableTypePattern.test(node.body.type)) {
|
||||
state.pushBreakContext(false, node.label.name);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Emits onCodePathSegmentStart events if updated.
|
||||
forwardCurrentToHead(analyzer, node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the code path due to the type of a given node in leaving.
|
||||
* @param {CodePathAnalyzer} analyzer The instance.
|
||||
* @param {ASTNode} node The current AST node.
|
||||
* @returns {void}
|
||||
*/
|
||||
function processCodePathToExit(analyzer, node) {
|
||||
const codePath = analyzer.codePath;
|
||||
const state = CodePath.getState(codePath);
|
||||
let dontForward = false;
|
||||
|
||||
switch (node.type) {
|
||||
case 'ChainExpression':
|
||||
state.popChainContext();
|
||||
break;
|
||||
|
||||
case 'IfStatement':
|
||||
case 'ConditionalExpression':
|
||||
state.popChoiceContext();
|
||||
break;
|
||||
|
||||
case 'LogicalExpression':
|
||||
if (isHandledLogicalOperator(node.operator)) {
|
||||
state.popChoiceContext();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'AssignmentExpression':
|
||||
if (isLogicalAssignmentOperator(node.operator)) {
|
||||
state.popChoiceContext();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'SwitchStatement':
|
||||
state.popSwitchContext();
|
||||
break;
|
||||
|
||||
case 'SwitchCase':
|
||||
/*
|
||||
* This is the same as the process at the 1st `consequent` node in
|
||||
* `preprocess` function.
|
||||
* Must do if this `consequent` is empty.
|
||||
*/
|
||||
if (node.consequent.length === 0) {
|
||||
state.makeSwitchCaseBody(true, !node.test);
|
||||
}
|
||||
if (state.forkContext.reachable) {
|
||||
dontForward = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'TryStatement':
|
||||
state.popTryContext();
|
||||
break;
|
||||
|
||||
case 'BreakStatement':
|
||||
forwardCurrentToHead(analyzer, node);
|
||||
state.makeBreak(node.label && node.label.name);
|
||||
dontForward = true;
|
||||
break;
|
||||
|
||||
case 'ContinueStatement':
|
||||
forwardCurrentToHead(analyzer, node);
|
||||
state.makeContinue(node.label && node.label.name);
|
||||
dontForward = true;
|
||||
break;
|
||||
|
||||
case 'ReturnStatement':
|
||||
forwardCurrentToHead(analyzer, node);
|
||||
state.makeReturn();
|
||||
dontForward = true;
|
||||
break;
|
||||
|
||||
case 'ThrowStatement':
|
||||
forwardCurrentToHead(analyzer, node);
|
||||
state.makeThrow();
|
||||
dontForward = true;
|
||||
break;
|
||||
|
||||
case 'Identifier':
|
||||
if (isIdentifierReference(node)) {
|
||||
state.makeFirstThrowablePathInTryBlock();
|
||||
dontForward = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'CallExpression':
|
||||
case 'ImportExpression':
|
||||
case 'MemberExpression':
|
||||
case 'NewExpression':
|
||||
case 'YieldExpression':
|
||||
state.makeFirstThrowablePathInTryBlock();
|
||||
break;
|
||||
|
||||
case 'WhileStatement':
|
||||
case 'DoWhileStatement':
|
||||
case 'ForStatement':
|
||||
case 'ForInStatement':
|
||||
case 'ForOfStatement':
|
||||
state.popLoopContext();
|
||||
break;
|
||||
|
||||
case 'AssignmentPattern':
|
||||
state.popForkContext();
|
||||
break;
|
||||
|
||||
case 'LabeledStatement':
|
||||
if (!breakableTypePattern.test(node.body.type)) {
|
||||
state.popBreakContext();
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Emits onCodePathSegmentStart events if updated.
|
||||
if (!dontForward) {
|
||||
forwardCurrentToHead(analyzer, node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the code path to finalize the current code path.
|
||||
* @param {CodePathAnalyzer} analyzer The instance.
|
||||
* @param {ASTNode} node The current AST node.
|
||||
* @returns {void}
|
||||
*/
|
||||
function postprocess(analyzer, node) {
|
||||
/**
|
||||
* Ends the code path for the current node.
|
||||
* @returns {void}
|
||||
*/
|
||||
function endCodePath() {
|
||||
let codePath = analyzer.codePath;
|
||||
|
||||
// Mark the current path as the final node.
|
||||
CodePath.getState(codePath).makeFinal();
|
||||
|
||||
// Emits onCodePathSegmentEnd event of the current segments.
|
||||
leaveFromCurrentSegment(analyzer, node);
|
||||
|
||||
// Emits onCodePathEnd event of this code path.
|
||||
analyzer.emitter.emit('onCodePathEnd', codePath, node);
|
||||
|
||||
codePath = analyzer.codePath = analyzer.codePath.upper;
|
||||
}
|
||||
|
||||
switch (node.type) {
|
||||
case 'Program':
|
||||
case 'FunctionDeclaration':
|
||||
case 'ComponentDeclaration':
|
||||
case 'HookDeclaration':
|
||||
case 'FunctionExpression':
|
||||
case 'ArrowFunctionExpression':
|
||||
case 'StaticBlock': {
|
||||
endCodePath();
|
||||
break;
|
||||
}
|
||||
|
||||
// The `arguments.length >= 1` case is in `preprocess` function.
|
||||
case 'CallExpression':
|
||||
if (node.optional === true && node.arguments.length === 0) {
|
||||
CodePath.getState(analyzer.codePath).makeOptionalRight();
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
/*
|
||||
* Special case: The right side of class field initializer is considered
|
||||
* to be its own function, so we need to end a code path in this
|
||||
* case.
|
||||
*
|
||||
* We need to check after the other checks in order to close the
|
||||
* code paths in the correct order for code like this:
|
||||
*
|
||||
*
|
||||
* class Foo {
|
||||
* a = () => {}
|
||||
* }
|
||||
*
|
||||
* In this case, The ArrowFunctionExpression code path is closed first
|
||||
* and then we need to close the code path for the PropertyDefinition
|
||||
* value.
|
||||
*/
|
||||
if (isPropertyDefinitionValue(node)) {
|
||||
endCodePath();
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* The class to analyze code paths.
|
||||
* This class implements the EventGenerator interface.
|
||||
*/
|
||||
class CodePathAnalyzer {
|
||||
/**
|
||||
* @param {EventGenerator} eventGenerator An event generator to wrap.
|
||||
*/
|
||||
constructor(emitters) {
|
||||
this.emitter = {
|
||||
emit(event, ...args) {
|
||||
emitters[event]?.(...args);
|
||||
},
|
||||
};
|
||||
this.codePath = null;
|
||||
this.idGenerator = new IdGenerator('s');
|
||||
this.currentNode = null;
|
||||
this.onLooped = this.onLooped.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the process to enter a given AST node.
|
||||
* This updates state of analysis and calls `enterNode` of the wrapped.
|
||||
* @param {ASTNode} node A node which is entering.
|
||||
* @returns {void}
|
||||
*/
|
||||
enterNode(node) {
|
||||
this.currentNode = node;
|
||||
|
||||
// Updates the code path due to node's position in its parent node.
|
||||
if (node.parent) {
|
||||
preprocess(this, node);
|
||||
}
|
||||
|
||||
/*
|
||||
* Updates the code path.
|
||||
* And emits onCodePathStart/onCodePathSegmentStart events.
|
||||
*/
|
||||
processCodePathToEnter(this, node);
|
||||
|
||||
this.currentNode = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the process to leave a given AST node.
|
||||
* This updates state of analysis and calls `leaveNode` of the wrapped.
|
||||
* @param {ASTNode} node A node which is leaving.
|
||||
* @returns {void}
|
||||
*/
|
||||
leaveNode(node) {
|
||||
this.currentNode = node;
|
||||
|
||||
/*
|
||||
* Updates the code path.
|
||||
* And emits onCodePathStart/onCodePathSegmentStart events.
|
||||
*/
|
||||
processCodePathToExit(this, node);
|
||||
|
||||
// Emits the last onCodePathStart/onCodePathSegmentStart events.
|
||||
postprocess(this, node);
|
||||
|
||||
this.currentNode = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called on a code path looped.
|
||||
* Then this raises a looped event.
|
||||
* @param {CodePathSegment} fromSegment A segment of prev.
|
||||
* @param {CodePathSegment} toSegment A segment of next.
|
||||
* @returns {void}
|
||||
*/
|
||||
onLooped(fromSegment, toSegment) {
|
||||
if (fromSegment.reachable && toSegment.reachable) {
|
||||
this.emitter.emit(
|
||||
'onCodePathSegmentLoop',
|
||||
fromSegment,
|
||||
toSegment,
|
||||
this.currentNode,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CodePathAnalyzer;
|
||||
|
|
@ -0,0 +1,225 @@
|
|||
'use strict';
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Checks whether or not a given segment is reachable.
|
||||
* @param {CodePathSegment} segment A segment to check.
|
||||
* @returns {boolean} `true` if the segment is reachable.
|
||||
*/
|
||||
function isReachable(segment) {
|
||||
return segment.reachable;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* A code path segment.
|
||||
*/
|
||||
class CodePathSegment {
|
||||
/**
|
||||
* @param {string} id An identifier.
|
||||
* @param {CodePathSegment[]} allPrevSegments An array of the previous segments.
|
||||
* This array includes unreachable segments.
|
||||
* @param {boolean} reachable A flag which shows this is reachable.
|
||||
*/
|
||||
constructor(id, allPrevSegments, reachable) {
|
||||
/**
|
||||
* The identifier of this code path.
|
||||
* Rules use it to store additional information of each rule.
|
||||
* @type {string}
|
||||
*/
|
||||
this.id = id;
|
||||
|
||||
/**
|
||||
* An array of the next segments.
|
||||
* @type {CodePathSegment[]}
|
||||
*/
|
||||
this.nextSegments = [];
|
||||
|
||||
/**
|
||||
* An array of the previous segments.
|
||||
* @type {CodePathSegment[]}
|
||||
*/
|
||||
this.prevSegments = allPrevSegments.filter(isReachable);
|
||||
|
||||
/**
|
||||
* An array of the next segments.
|
||||
* This array includes unreachable segments.
|
||||
* @type {CodePathSegment[]}
|
||||
*/
|
||||
this.allNextSegments = [];
|
||||
|
||||
/**
|
||||
* An array of the previous segments.
|
||||
* This array includes unreachable segments.
|
||||
* @type {CodePathSegment[]}
|
||||
*/
|
||||
this.allPrevSegments = allPrevSegments;
|
||||
|
||||
/**
|
||||
* A flag which shows this is reachable.
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.reachable = reachable;
|
||||
|
||||
// Internal data.
|
||||
Object.defineProperty(this, 'internal', {
|
||||
value: {
|
||||
used: false,
|
||||
loopedPrevSegments: [],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks a given previous segment is coming from the end of a loop.
|
||||
* @param {CodePathSegment} segment A previous segment to check.
|
||||
* @returns {boolean} `true` if the segment is coming from the end of a loop.
|
||||
*/
|
||||
isLoopedPrevSegment(segment) {
|
||||
return this.internal.loopedPrevSegments.includes(segment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the root segment.
|
||||
* @param {string} id An identifier.
|
||||
* @returns {CodePathSegment} The created segment.
|
||||
*/
|
||||
static newRoot(id) {
|
||||
return new CodePathSegment(id, [], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a segment that follows given segments.
|
||||
* @param {string} id An identifier.
|
||||
* @param {CodePathSegment[]} allPrevSegments An array of the previous segments.
|
||||
* @returns {CodePathSegment} The created segment.
|
||||
*/
|
||||
static newNext(id, allPrevSegments) {
|
||||
return new CodePathSegment(
|
||||
id,
|
||||
CodePathSegment.flattenUnusedSegments(allPrevSegments),
|
||||
allPrevSegments.some(isReachable),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an unreachable segment that follows given segments.
|
||||
* @param {string} id An identifier.
|
||||
* @param {CodePathSegment[]} allPrevSegments An array of the previous segments.
|
||||
* @returns {CodePathSegment} The created segment.
|
||||
*/
|
||||
static newUnreachable(id, allPrevSegments) {
|
||||
const segment = new CodePathSegment(
|
||||
id,
|
||||
CodePathSegment.flattenUnusedSegments(allPrevSegments),
|
||||
false,
|
||||
);
|
||||
|
||||
/*
|
||||
* In `if (a) return a; foo();` case, the unreachable segment preceded by
|
||||
* the return statement is not used but must not be remove.
|
||||
*/
|
||||
CodePathSegment.markUsed(segment);
|
||||
|
||||
return segment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a segment that follows given segments.
|
||||
* This factory method does not connect with `allPrevSegments`.
|
||||
* But this inherits `reachable` flag.
|
||||
* @param {string} id An identifier.
|
||||
* @param {CodePathSegment[]} allPrevSegments An array of the previous segments.
|
||||
* @returns {CodePathSegment} The created segment.
|
||||
*/
|
||||
static newDisconnected(id, allPrevSegments) {
|
||||
return new CodePathSegment(id, [], allPrevSegments.some(isReachable));
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a given segment being used.
|
||||
*
|
||||
* And this function registers the segment into the previous segments as a next.
|
||||
* @param {CodePathSegment} segment A segment to mark.
|
||||
* @returns {void}
|
||||
*/
|
||||
static markUsed(segment) {
|
||||
if (segment.internal.used) {
|
||||
return;
|
||||
}
|
||||
segment.internal.used = true;
|
||||
|
||||
let i;
|
||||
|
||||
if (segment.reachable) {
|
||||
for (i = 0; i < segment.allPrevSegments.length; ++i) {
|
||||
const prevSegment = segment.allPrevSegments[i];
|
||||
|
||||
prevSegment.allNextSegments.push(segment);
|
||||
prevSegment.nextSegments.push(segment);
|
||||
}
|
||||
} else {
|
||||
for (i = 0; i < segment.allPrevSegments.length; ++i) {
|
||||
segment.allPrevSegments[i].allNextSegments.push(segment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a previous segment as looped.
|
||||
* @param {CodePathSegment} segment A segment.
|
||||
* @param {CodePathSegment} prevSegment A previous segment to mark.
|
||||
* @returns {void}
|
||||
*/
|
||||
static markPrevSegmentAsLooped(segment, prevSegment) {
|
||||
segment.internal.loopedPrevSegments.push(prevSegment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces unused segments with the previous segments of each unused segment.
|
||||
* @param {CodePathSegment[]} segments An array of segments to replace.
|
||||
* @returns {CodePathSegment[]} The replaced array.
|
||||
*/
|
||||
static flattenUnusedSegments(segments) {
|
||||
const done = Object.create(null);
|
||||
const retv = [];
|
||||
|
||||
for (let i = 0; i < segments.length; ++i) {
|
||||
const segment = segments[i];
|
||||
|
||||
// Ignores duplicated.
|
||||
if (done[segment.id]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use previous segments if unused.
|
||||
if (!segment.internal.used) {
|
||||
for (let j = 0; j < segment.allPrevSegments.length; ++j) {
|
||||
const prevSegment = segment.allPrevSegments[j];
|
||||
|
||||
if (!done[prevSegment.id]) {
|
||||
done[prevSegment.id] = true;
|
||||
retv.push(prevSegment);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
done[segment.id] = true;
|
||||
retv.push(segment);
|
||||
}
|
||||
}
|
||||
|
||||
return retv;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CodePathSegment;
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,239 @@
|
|||
'use strict';
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// eslint-disable-next-line
|
||||
const CodePathState = require('./code-path-state');
|
||||
// eslint-disable-next-line
|
||||
const IdGenerator = require('./id-generator');
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* A code path.
|
||||
*/
|
||||
class CodePath {
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {Object} options Options for the function (see below).
|
||||
* @param {string} options.id An identifier.
|
||||
* @param {string} options.origin The type of code path origin.
|
||||
* @param {CodePath|null} options.upper The code path of the upper function scope.
|
||||
* @param {Function} options.onLooped A callback function to notify looping.
|
||||
*/
|
||||
constructor({id, origin, upper, onLooped}) {
|
||||
/**
|
||||
* The identifier of this code path.
|
||||
* Rules use it to store additional information of each rule.
|
||||
* @type {string}
|
||||
*/
|
||||
this.id = id;
|
||||
|
||||
/**
|
||||
* The reason that this code path was started. May be "program",
|
||||
* "function", "class-field-initializer", or "class-static-block".
|
||||
* @type {string}
|
||||
*/
|
||||
this.origin = origin;
|
||||
|
||||
/**
|
||||
* The code path of the upper function scope.
|
||||
* @type {CodePath|null}
|
||||
*/
|
||||
this.upper = upper;
|
||||
|
||||
/**
|
||||
* The code paths of nested function scopes.
|
||||
* @type {CodePath[]}
|
||||
*/
|
||||
this.childCodePaths = [];
|
||||
|
||||
// Initializes internal state.
|
||||
Object.defineProperty(this, 'internal', {
|
||||
value: new CodePathState(new IdGenerator(`${id}_`), onLooped),
|
||||
});
|
||||
|
||||
// Adds this into `childCodePaths` of `upper`.
|
||||
if (upper) {
|
||||
upper.childCodePaths.push(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the state of a given code path.
|
||||
* @param {CodePath} codePath A code path to get.
|
||||
* @returns {CodePathState} The state of the code path.
|
||||
*/
|
||||
static getState(codePath) {
|
||||
return codePath.internal;
|
||||
}
|
||||
|
||||
/**
|
||||
* The initial code path segment.
|
||||
* @type {CodePathSegment}
|
||||
*/
|
||||
get initialSegment() {
|
||||
return this.internal.initialSegment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Final code path segments.
|
||||
* This array is a mix of `returnedSegments` and `thrownSegments`.
|
||||
* @type {CodePathSegment[]}
|
||||
*/
|
||||
get finalSegments() {
|
||||
return this.internal.finalSegments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Final code path segments which is with `return` statements.
|
||||
* This array contains the last path segment if it's reachable.
|
||||
* Since the reachable last path returns `undefined`.
|
||||
* @type {CodePathSegment[]}
|
||||
*/
|
||||
get returnedSegments() {
|
||||
return this.internal.returnedForkContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Final code path segments which is with `throw` statements.
|
||||
* @type {CodePathSegment[]}
|
||||
*/
|
||||
get thrownSegments() {
|
||||
return this.internal.thrownForkContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current code path segments.
|
||||
* @type {CodePathSegment[]}
|
||||
*/
|
||||
get currentSegments() {
|
||||
return this.internal.currentSegments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverses all segments in this code path.
|
||||
*
|
||||
* codePath.traverseSegments(function(segment, controller) {
|
||||
* // do something.
|
||||
* });
|
||||
*
|
||||
* This method enumerates segments in order from the head.
|
||||
*
|
||||
* The `controller` object has two methods.
|
||||
*
|
||||
* - `controller.skip()` - Skip the following segments in this branch.
|
||||
* - `controller.break()` - Skip all following segments.
|
||||
* @param {Object} [options] Omittable.
|
||||
* @param {CodePathSegment} [options.first] The first segment to traverse.
|
||||
* @param {CodePathSegment} [options.last] The last segment to traverse.
|
||||
* @param {Function} callback A callback function.
|
||||
* @returns {void}
|
||||
*/
|
||||
traverseSegments(options, callback) {
|
||||
let resolvedOptions;
|
||||
let resolvedCallback;
|
||||
|
||||
if (typeof options === 'function') {
|
||||
resolvedCallback = options;
|
||||
resolvedOptions = {};
|
||||
} else {
|
||||
resolvedOptions = options || {};
|
||||
resolvedCallback = callback;
|
||||
}
|
||||
|
||||
const startSegment = resolvedOptions.first || this.internal.initialSegment;
|
||||
const lastSegment = resolvedOptions.last;
|
||||
|
||||
let item = null;
|
||||
let index = 0;
|
||||
let end = 0;
|
||||
let segment = null;
|
||||
const visited = Object.create(null);
|
||||
const stack = [[startSegment, 0]];
|
||||
let skippedSegment = null;
|
||||
let broken = false;
|
||||
const controller = {
|
||||
skip() {
|
||||
if (stack.length <= 1) {
|
||||
broken = true;
|
||||
} else {
|
||||
skippedSegment = stack[stack.length - 2][0];
|
||||
}
|
||||
},
|
||||
break() {
|
||||
broken = true;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks a given previous segment has been visited.
|
||||
* @param {CodePathSegment} prevSegment A previous segment to check.
|
||||
* @returns {boolean} `true` if the segment has been visited.
|
||||
*/
|
||||
function isVisited(prevSegment) {
|
||||
return (
|
||||
visited[prevSegment.id] || segment.isLoopedPrevSegment(prevSegment)
|
||||
);
|
||||
}
|
||||
|
||||
while (stack.length > 0) {
|
||||
item = stack[stack.length - 1];
|
||||
segment = item[0];
|
||||
index = item[1];
|
||||
|
||||
if (index === 0) {
|
||||
// Skip if this segment has been visited already.
|
||||
if (visited[segment.id]) {
|
||||
stack.pop();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if all previous segments have not been visited.
|
||||
if (
|
||||
segment !== startSegment &&
|
||||
segment.prevSegments.length > 0 &&
|
||||
!segment.prevSegments.every(isVisited)
|
||||
) {
|
||||
stack.pop();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reset the flag of skipping if all branches have been skipped.
|
||||
if (skippedSegment && segment.prevSegments.includes(skippedSegment)) {
|
||||
skippedSegment = null;
|
||||
}
|
||||
visited[segment.id] = true;
|
||||
|
||||
// Call the callback when the first time.
|
||||
if (!skippedSegment) {
|
||||
resolvedCallback.call(this, segment, controller);
|
||||
if (segment === lastSegment) {
|
||||
controller.skip();
|
||||
}
|
||||
if (broken) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the stack.
|
||||
end = segment.nextSegments.length - 1;
|
||||
if (index < end) {
|
||||
item[1] += 1;
|
||||
stack.push([segment.nextSegments[index], 0]);
|
||||
} else if (index === end) {
|
||||
item[0] = segment.nextSegments[index];
|
||||
item[1] = 0;
|
||||
} else {
|
||||
stack.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CodePath;
|
||||
|
|
@ -0,0 +1,252 @@
|
|||
'use strict';
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Requirements
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// eslint-disable-next-line
|
||||
const assert = require('./assert');
|
||||
// eslint-disable-next-line
|
||||
const CodePathSegment = require('./code-path-segment');
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Gets whether or not a given segment is reachable.
|
||||
* @param {CodePathSegment} segment A segment to get.
|
||||
* @returns {boolean} `true` if the segment is reachable.
|
||||
*/
|
||||
function isReachable(segment) {
|
||||
return segment.reachable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new segments from the specific range of `context.segmentsList`.
|
||||
*
|
||||
* When `context.segmentsList` is `[[a, b], [c, d], [e, f]]`, `begin` is `0`, and
|
||||
* `end` is `-1`, this creates `[g, h]`. This `g` is from `a`, `c`, and `e`.
|
||||
* This `h` is from `b`, `d`, and `f`.
|
||||
* @param {ForkContext} context An instance.
|
||||
* @param {number} begin The first index of the previous segments.
|
||||
* @param {number} end The last index of the previous segments.
|
||||
* @param {Function} create A factory function of new segments.
|
||||
* @returns {CodePathSegment[]} New segments.
|
||||
*/
|
||||
function makeSegments(context, begin, end, create) {
|
||||
const list = context.segmentsList;
|
||||
|
||||
const normalizedBegin = begin >= 0 ? begin : list.length + begin;
|
||||
const normalizedEnd = end >= 0 ? end : list.length + end;
|
||||
|
||||
const segments = [];
|
||||
|
||||
for (let i = 0; i < context.count; ++i) {
|
||||
const allPrevSegments = [];
|
||||
|
||||
for (let j = normalizedBegin; j <= normalizedEnd; ++j) {
|
||||
allPrevSegments.push(list[j][i]);
|
||||
}
|
||||
|
||||
segments.push(create(context.idGenerator.next(), allPrevSegments));
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
/**
|
||||
* `segments` becomes doubly in a `finally` block. Then if a code path exits by a
|
||||
* control statement (such as `break`, `continue`) from the `finally` block, the
|
||||
* destination's segments may be half of the source segments. In that case, this
|
||||
* merges segments.
|
||||
* @param {ForkContext} context An instance.
|
||||
* @param {CodePathSegment[]} segments Segments to merge.
|
||||
* @returns {CodePathSegment[]} The merged segments.
|
||||
*/
|
||||
function mergeExtraSegments(context, segments) {
|
||||
let currentSegments = segments;
|
||||
|
||||
while (currentSegments.length > context.count) {
|
||||
const merged = [];
|
||||
|
||||
for (
|
||||
let i = 0, length = (currentSegments.length / 2) | 0;
|
||||
i < length;
|
||||
++i
|
||||
) {
|
||||
merged.push(
|
||||
CodePathSegment.newNext(context.idGenerator.next(), [
|
||||
currentSegments[i],
|
||||
currentSegments[i + length],
|
||||
]),
|
||||
);
|
||||
}
|
||||
currentSegments = merged;
|
||||
}
|
||||
return currentSegments;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* A class to manage forking.
|
||||
*/
|
||||
class ForkContext {
|
||||
/**
|
||||
* @param {IdGenerator} idGenerator An identifier generator for segments.
|
||||
* @param {ForkContext|null} upper An upper fork context.
|
||||
* @param {number} count A number of parallel segments.
|
||||
*/
|
||||
constructor(idGenerator, upper, count) {
|
||||
this.idGenerator = idGenerator;
|
||||
this.upper = upper;
|
||||
this.count = count;
|
||||
this.segmentsList = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* The head segments.
|
||||
* @type {CodePathSegment[]}
|
||||
*/
|
||||
get head() {
|
||||
const list = this.segmentsList;
|
||||
|
||||
return list.length === 0 ? [] : list[list.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* A flag which shows empty.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get empty() {
|
||||
return this.segmentsList.length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* A flag which shows reachable.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get reachable() {
|
||||
const segments = this.head;
|
||||
|
||||
return segments.length > 0 && segments.some(isReachable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new segments from this context.
|
||||
* @param {number} begin The first index of previous segments.
|
||||
* @param {number} end The last index of previous segments.
|
||||
* @returns {CodePathSegment[]} New segments.
|
||||
*/
|
||||
makeNext(begin, end) {
|
||||
return makeSegments(this, begin, end, CodePathSegment.newNext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new segments from this context.
|
||||
* The new segments is always unreachable.
|
||||
* @param {number} begin The first index of previous segments.
|
||||
* @param {number} end The last index of previous segments.
|
||||
* @returns {CodePathSegment[]} New segments.
|
||||
*/
|
||||
makeUnreachable(begin, end) {
|
||||
return makeSegments(this, begin, end, CodePathSegment.newUnreachable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new segments from this context.
|
||||
* The new segments don't have connections for previous segments.
|
||||
* But these inherit the reachable flag from this context.
|
||||
* @param {number} begin The first index of previous segments.
|
||||
* @param {number} end The last index of previous segments.
|
||||
* @returns {CodePathSegment[]} New segments.
|
||||
*/
|
||||
makeDisconnected(begin, end) {
|
||||
return makeSegments(this, begin, end, CodePathSegment.newDisconnected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds segments into this context.
|
||||
* The added segments become the head.
|
||||
* @param {CodePathSegment[]} segments Segments to add.
|
||||
* @returns {void}
|
||||
*/
|
||||
add(segments) {
|
||||
assert(
|
||||
segments.length >= this.count,
|
||||
`${segments.length} >= ${this.count}`,
|
||||
);
|
||||
|
||||
this.segmentsList.push(mergeExtraSegments(this, segments));
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the head segments with given segments.
|
||||
* The current head segments are removed.
|
||||
* @param {CodePathSegment[]} segments Segments to add.
|
||||
* @returns {void}
|
||||
*/
|
||||
replaceHead(segments) {
|
||||
assert(
|
||||
segments.length >= this.count,
|
||||
`${segments.length} >= ${this.count}`,
|
||||
);
|
||||
|
||||
this.segmentsList.splice(-1, 1, mergeExtraSegments(this, segments));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds all segments of a given fork context into this context.
|
||||
* @param {ForkContext} context A fork context to add.
|
||||
* @returns {void}
|
||||
*/
|
||||
addAll(context) {
|
||||
assert(context.count === this.count);
|
||||
|
||||
const source = context.segmentsList;
|
||||
|
||||
for (let i = 0; i < source.length; ++i) {
|
||||
this.segmentsList.push(source[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all segments in this context.
|
||||
* @returns {void}
|
||||
*/
|
||||
clear() {
|
||||
this.segmentsList = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the root fork context.
|
||||
* @param {IdGenerator} idGenerator An identifier generator for segments.
|
||||
* @returns {ForkContext} New fork context.
|
||||
*/
|
||||
static newRoot(idGenerator) {
|
||||
const context = new ForkContext(idGenerator, null, 1);
|
||||
|
||||
context.add([CodePathSegment.newRoot(idGenerator.next())]);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an empty fork context preceded by a given context.
|
||||
* @param {ForkContext} parentContext The parent fork context.
|
||||
* @param {boolean} forkLeavingPath A flag which shows inside of `finally` block.
|
||||
* @returns {ForkContext} New fork context.
|
||||
*/
|
||||
static newEmpty(parentContext, forkLeavingPath) {
|
||||
return new ForkContext(
|
||||
parentContext.idGenerator,
|
||||
parentContext,
|
||||
(forkLeavingPath ? 2 : 1) * parentContext.count,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ForkContext;
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
'use strict';
|
||||
|
||||
/* eslint-disable react-internal/safe-string-coercion */
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Public Interface
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* A generator for unique ids.
|
||||
*/
|
||||
class IdGenerator {
|
||||
/**
|
||||
* @param {string} prefix Optional. A prefix of generated ids.
|
||||
*/
|
||||
constructor(prefix) {
|
||||
this.prefix = String(prefix);
|
||||
this.n = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates id.
|
||||
* @returns {string} A generated id.
|
||||
*/
|
||||
next() {
|
||||
this.n = (1 + this.n) | 0;
|
||||
|
||||
/* c8 ignore start */
|
||||
if (this.n < 0) {
|
||||
this.n = 1;
|
||||
} /* c8 ignore stop */
|
||||
|
||||
return this.prefix + this.n;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = IdGenerator;
|
||||
|
|
@ -9,6 +9,9 @@
|
|||
import type {Rule, Scope} from 'eslint';
|
||||
import type {CallExpression, DoWhileStatement, Node} from 'estree';
|
||||
|
||||
// @ts-expect-error untyped module
|
||||
import CodePathAnalyzer from '../code-path-analysis/code-path-analyzer';
|
||||
|
||||
/**
|
||||
* Catch all identifiers that begin with "use" followed by an uppercase Latin
|
||||
* character to exclude identifiers like "user".
|
||||
|
|
@ -184,9 +187,25 @@ const rule = {
|
|||
return getSourceCode().getScope(node);
|
||||
};
|
||||
|
||||
return {
|
||||
function hasFlowSuppression(node: Node, suppression: string) {
|
||||
const sourceCode = getSourceCode();
|
||||
const comments = sourceCode.getAllComments();
|
||||
const flowSuppressionRegex = new RegExp(
|
||||
'\\$FlowFixMe\\[' + suppression + '\\]',
|
||||
);
|
||||
return comments.some(
|
||||
commentNode =>
|
||||
flowSuppressionRegex.test(commentNode.value) &&
|
||||
commentNode.loc != null &&
|
||||
node.loc != null &&
|
||||
commentNode.loc.end.line === node.loc.start.line - 1,
|
||||
);
|
||||
}
|
||||
|
||||
const analyzer = new CodePathAnalyzer({
|
||||
// Maintain code segment path stack as we traverse.
|
||||
onCodePathSegmentStart: segment => codePathSegmentStack.push(segment),
|
||||
onCodePathSegmentStart: (segment: Rule.CodePathSegment) =>
|
||||
codePathSegmentStack.push(segment),
|
||||
onCodePathSegmentEnd: () => codePathSegmentStack.pop(),
|
||||
|
||||
// Maintain code path stack as we traverse.
|
||||
|
|
@ -199,7 +218,7 @@ const rule = {
|
|||
//
|
||||
// Everything is ok if all React Hooks are both reachable from the initial
|
||||
// segment and reachable from every final segment.
|
||||
onCodePathEnd(codePath, codePathNode) {
|
||||
onCodePathEnd(codePath: any, codePathNode: Node) {
|
||||
const reactHooksMap = codePathReactHooksMapStack.pop();
|
||||
if (reactHooksMap?.size === 0) {
|
||||
return;
|
||||
|
|
@ -508,6 +527,11 @@ const rule = {
|
|||
const cycled = cyclic.has(segment.id);
|
||||
|
||||
for (const hook of reactHooks) {
|
||||
// Skip reporting if this hook already has a relevant flow suppression.
|
||||
if (hasFlowSuppression(hook, 'react-rule-hook')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Report an error if a hook may be called more then once.
|
||||
// `use(...)` can be called in loops.
|
||||
if (
|
||||
|
|
@ -611,6 +635,16 @@ const rule = {
|
|||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
'*'(node: any) {
|
||||
analyzer.enterNode(node);
|
||||
},
|
||||
|
||||
'*:exit'(node: any) {
|
||||
analyzer.leaveNode(node);
|
||||
},
|
||||
|
||||
// Missed opportunity...We could visit all `Identifier`s instead of all
|
||||
// `CallExpression`s and check that _every use_ of a hook name is valid.
|
||||
|
|
@ -696,6 +730,10 @@ const rule = {
|
|||
|
||||
function getFunctionName(node: Node) {
|
||||
if (
|
||||
// @ts-expect-error parser-hermes produces these node types
|
||||
node.type === 'ComponentDeclaration' ||
|
||||
// @ts-expect-error parser-hermes produces these node types
|
||||
node.type === 'HookDeclaration' ||
|
||||
node.type === 'FunctionDeclaration' ||
|
||||
(node.type === 'FunctionExpression' && node.id)
|
||||
) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user