Skip to content

Commit f326cc6

Browse files
authored
Added reportUnreachable diagnostic check. If enabled, it emits a diagnostic for code that is determined to be structurally unreachable or unreachable via type analysis. It does not report code that is within a code block that is gated by a conditional that is statically determined to be false, such as TYPE_CHECKING and version checks. This diagnostic check is off by default even in strict mode because there are legitimate reasons for unreachable code to exist in Python code. (#10581)
This change also modifies the messages that accompany tagged hints (the mechanism by which unreachable or unanalyzed code is displayed as "dimmed" in an editor). The message now distinguishes between code that is structurally unreachable (e.g. after a `return` or `raise` statement) and code that is not analyzed because it is in a conditional block that is statically evaluated as false. This addresses #10284.
1 parent c165c3f commit f326cc6

33 files changed

Lines changed: 204 additions & 72 deletions

docs/configuration.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ The following settings determine how different types should be evaluated.
5858

5959
- <a name="deprecateTypingAliases"></a> **deprecateTypingAliases** [boolean]: PEP 585 indicates that aliases to types in standard collections that were introduced solely to support generics are deprecated as of Python 3.9. This switch controls whether these are treated as deprecated. This applies only when pythonVersion is 3.9 or newer. The default value for this setting is `false` but may be switched to `true` in the future.
6060

61-
- <a name="enableReachabilityAnalysis"></a> **enableReachabilityAnalysis** [boolean]: If enabled, code that is determined to be unreachable by type analysis is reported using a tagged hint. This setting does not affect code that is determined to be unreachable regardless of type analysis; such code is always reported as unreachable. This setting also has no effect when using the command-line version of pyright because it never emits tagged hints for unreachable code.
61+
- <a name="enableReachabilityAnalysis"></a> **enableReachabilityAnalysis** [boolean]: If enabled, code that is determined to be unreachable by type analysis is reported using a tagged hint. This setting does not affect code that is determined to be unreachable independent of type analysis; such code is always reported as unreachable using a tagged hint. This setting also has no effect when using the command-line version of pyright because it never emits tagged hints for unreachable code.
6262

6363
- <a name="enableExperimentalFeatures"></a> **enableExperimentalFeatures** [boolean]: Enables a set of experimental (mostly undocumented) features that correspond to proposed or exploratory changes to the Python typing standard. These features will likely change or be removed, so they should not be used except for experimentation purposes. The default value for this setting is `false`.
6464

@@ -233,6 +233,8 @@ The following settings allow more fine grained control over the **typeCheckingMo
233233

234234
- <a name="reportMatchNotExhaustive"></a> **reportMatchNotExhaustive** [boolean or string, optional]: Generate or suppress diagnostics for a `match` statement that does not provide cases that exhaustively match against all potential types of the target expression. The default value for this setting is `"none"`.
235235

236+
- <a name="reportUnreachable"></a> **reportUnreachable** [boolean or string, optional]: Generate or suppress diagnostics for code that is determined to be structurally unreachable or unreachable by type analysis. The default value for this setting is `"none"`.
237+
236238
- <a name="reportImplicitOverride"></a> **reportImplicitOverride** [boolean or string, optional]: Generate or suppress diagnostics for overridden methods in a class that are missing an explicit `@override` decorator. The default value for this setting is `"none"`.
237239

238240
- <a name="reportShadowedImports"></a> **reportShadowedImports** [boolean or string, optional]: Generate or suppress diagnostics for files that are overriding a module in the stdlib. The default value for this setting is `"none"`.
@@ -439,6 +441,7 @@ The following table lists the default severity levels for each diagnostic rule w
439441
| reportShadowedImports | "none" | "none" | "none" | "none" |
440442
| reportUninitializedInstanceVariable | "none" | "none" | "none" | "none" |
441443
| reportUnnecessaryTypeIgnoreComment | "none" | "none" | "none" | "none" |
444+
| reportUnreachable | "none" | "none" | "none" | "none" |
442445
| reportUnusedCallResult | "none" | "none" | "none" | "none" |
443446

444447
## Overriding settings (in VS Code)

docs/type-concepts-advanced.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -601,9 +601,9 @@ If one of these conditional expressions evaluates statically to false, pyright d
601601

602602

603603
### Reachability
604-
Pyright performs “reachability analysis” to determine whether statements will be executed at runtime.
604+
Pyright performs “reachability analysis” to determine whether statements will be executed at runtime and whether it should analyze and report errors in code.
605605

606-
Reachability analysis is based on both non-type and type information. Non-type information includes statements that unconditionally affect code flow such as `continue`, `raise` and `return`. It also includes conditional statements (`if`, `elif`, or `while`) where the conditional expression is one of these [supported expression forms](type-concepts-advanced#static-conditional-evaluation). Type analysis is not performed on code determined to be unreachable using non-type information. Therefore, language server features like completion suggestions are not available for this code.
606+
Reachability analysis is based on both non-type and type information. Non-type information includes statements that affect code structure such as `continue`, `raise` and `return`. It also includes conditional statements (`if`, `elif`, or `while`) where the conditional expression is one of these [supported expression forms](type-concepts-advanced#static-conditional-evaluation). Type analysis is not performed on code determined to be unreachable using non-type information. Therefore, language server features like completion suggestions are not available for this code.
607607

608608
Here are some examples of code determined to be unreachable using non-type information.
609609

packages/pyright-internal/src/analyzer/analyzerNodeInfo.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ export function isCodeUnreachable(node: ParseNode): boolean {
212212
while (curNode) {
213213
const flowNode = getFlowNode(curNode);
214214
if (flowNode) {
215-
return !!(flowNode.flags & FlowFlags.Unreachable);
215+
return (flowNode.flags & (FlowFlags.UnreachableStaticCondition | FlowFlags.UnreachableStructural)) !== 0;
216216
}
217217
curNode = curNode.parent;
218218
}

packages/pyright-internal/src/analyzer/binder.ts

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -242,8 +242,13 @@ export class Binder extends ParseTreeWalker {
242242
private _dunderSlotsEntries: StringListNode[] | undefined;
243243

244244
// Flow node that is used for unreachable code.
245-
private static _unreachableFlowNode: FlowNode = {
246-
flags: FlowFlags.Unreachable,
245+
private static _unreachableStaticConditionFlowNode: FlowNode = {
246+
flags: FlowFlags.UnreachableStaticCondition,
247+
id: getUniqueFlowNodeId(),
248+
};
249+
250+
private static _unreachableStructuralFlowNode: FlowNode = {
251+
flags: FlowFlags.UnreachableStructural,
247252
id: getUniqueFlowNodeId(),
248253
};
249254

@@ -1241,7 +1246,7 @@ export class Binder extends ParseTreeWalker {
12411246
if (this._currentContinueTarget) {
12421247
this._addAntecedent(this._currentContinueTarget, this._currentFlowNode!);
12431248
}
1244-
this._currentFlowNode = Binder._unreachableFlowNode;
1249+
this._currentFlowNode = Binder._unreachableStructuralFlowNode;
12451250

12461251
// Continue nodes don't have any children.
12471252
return false;
@@ -1251,7 +1256,7 @@ export class Binder extends ParseTreeWalker {
12511256
if (this._currentBreakTarget) {
12521257
this._addAntecedent(this._currentBreakTarget, this._currentFlowNode!);
12531258
}
1254-
this._currentFlowNode = Binder._unreachableFlowNode;
1259+
this._currentFlowNode = Binder._unreachableStructuralFlowNode;
12551260

12561261
// Break nodes don't have any children.
12571262
return false;
@@ -1277,7 +1282,7 @@ export class Binder extends ParseTreeWalker {
12771282
this._finallyTargets.forEach((target) => {
12781283
this._addAntecedent(target, this._currentFlowNode!);
12791284
});
1280-
this._currentFlowNode = Binder._unreachableFlowNode;
1285+
this._currentFlowNode = Binder._unreachableStructuralFlowNode;
12811286
return false;
12821287
}
12831288

@@ -1351,15 +1356,17 @@ export class Binder extends ParseTreeWalker {
13511356

13521357
// Handle the if clause.
13531358
this._currentFlowNode =
1354-
constExprValue === false ? Binder._unreachableFlowNode : this._finishFlowLabel(thenLabel);
1359+
constExprValue === false
1360+
? Binder._unreachableStaticConditionFlowNode
1361+
: this._finishFlowLabel(thenLabel);
13551362
this.walk(node.d.ifSuite);
13561363
this._addAntecedent(postIfLabel, this._currentFlowNode);
13571364

13581365
// Now handle the else clause if it's present. If there
13591366
// are chained "else if" statements, they'll be handled
13601367
// recursively here.
13611368
this._currentFlowNode =
1362-
constExprValue === true ? Binder._unreachableFlowNode : this._finishFlowLabel(elseLabel);
1369+
constExprValue === true ? Binder._unreachableStaticConditionFlowNode : this._finishFlowLabel(elseLabel);
13631370
if (node.d.elseSuite) {
13641371
this.walk(node.d.elseSuite);
13651372
} else {
@@ -1395,14 +1402,14 @@ export class Binder extends ParseTreeWalker {
13951402

13961403
// Handle the while clause.
13971404
this._currentFlowNode =
1398-
constExprValue === false ? Binder._unreachableFlowNode : this._finishFlowLabel(thenLabel);
1405+
constExprValue === false ? Binder._unreachableStaticConditionFlowNode : this._finishFlowLabel(thenLabel);
13991406
this._bindLoopStatement(preLoopLabel, postWhileLabel, () => {
14001407
this.walk(node.d.whileSuite);
14011408
});
14021409
this._addAntecedent(preLoopLabel, this._currentFlowNode);
14031410

14041411
this._currentFlowNode =
1405-
constExprValue === true ? Binder._unreachableFlowNode : this._finishFlowLabel(elseLabel);
1412+
constExprValue === true ? Binder._unreachableStaticConditionFlowNode : this._finishFlowLabel(elseLabel);
14061413
if (node.d.elseSuite) {
14071414
this.walk(node.d.elseSuite);
14081415
}
@@ -1489,7 +1496,7 @@ export class Binder extends ParseTreeWalker {
14891496
this._addAntecedent(target, this._currentFlowNode!);
14901497
});
14911498

1492-
this._currentFlowNode = Binder._unreachableFlowNode;
1499+
this._currentFlowNode = Binder._unreachableStructuralFlowNode;
14931500
return false;
14941501
}
14951502

@@ -1619,7 +1626,9 @@ export class Binder extends ParseTreeWalker {
16191626
antecedent: this._currentFlowNode!,
16201627
preFinallyGate,
16211628
};
1622-
this._currentFlowNode = isAfterElseAndExceptsReachable ? postFinallyNode : Binder._unreachableFlowNode;
1629+
this._currentFlowNode = isAfterElseAndExceptsReachable
1630+
? postFinallyNode
1631+
: Binder._unreachableStructuralFlowNode;
16231632
}
16241633

16251634
return false;
@@ -2868,7 +2877,7 @@ export class Binder extends ParseTreeWalker {
28682877
private _finishFlowLabel(node: FlowLabel) {
28692878
// If there were no antecedents, this is unreachable.
28702879
if (node.antecedents.length === 0) {
2871-
return Binder._unreachableFlowNode;
2880+
return Binder._unreachableStructuralFlowNode;
28722881
}
28732882

28742883
// If there was only one antecedent and this is a simple
@@ -2989,7 +2998,7 @@ export class Binder extends ParseTreeWalker {
29892998
}
29902999

29913000
private _createFlowConditional(flags: FlowFlags, antecedent: FlowNode, expression: ExpressionNode): FlowNode {
2992-
if (antecedent.flags & FlowFlags.Unreachable) {
3001+
if (antecedent.flags & (FlowFlags.UnreachableStructural | FlowFlags.UnreachableStaticCondition)) {
29933002
return antecedent;
29943003
}
29953004
const staticValue = StaticExpressions.evaluateStaticBoolLikeExpression(
@@ -3003,7 +3012,7 @@ export class Binder extends ParseTreeWalker {
30033012
(staticValue === true && flags & FlowFlags.FalseCondition) ||
30043013
(staticValue === false && flags & FlowFlags.TrueCondition)
30053014
) {
3006-
return Binder._unreachableFlowNode;
3015+
return Binder._unreachableStaticConditionFlowNode;
30073016
}
30083017

30093018
const expressionList: CodeFlowReferenceExpressionNode[] = [];
@@ -3442,7 +3451,10 @@ export class Binder extends ParseTreeWalker {
34423451
}
34433452

34443453
private _isCodeUnreachable() {
3445-
return !!(this._currentFlowNode!.flags & FlowFlags.Unreachable);
3454+
return !!(
3455+
this._currentFlowNode!.flags &
3456+
(FlowFlags.UnreachableStaticCondition | FlowFlags.UnreachableStructural)
3457+
);
34463458
}
34473459

34483460
private _addExceptTargets(flowNode: FlowNode) {
@@ -3487,7 +3499,9 @@ export class Binder extends ParseTreeWalker {
34873499
}
34883500

34893501
private _addAntecedent(label: FlowLabel, antecedent: FlowNode) {
3490-
if (!(this._currentFlowNode!.flags & FlowFlags.Unreachable)) {
3502+
if (
3503+
!(this._currentFlowNode!.flags & (FlowFlags.UnreachableStructural | FlowFlags.UnreachableStaticCondition))
3504+
) {
34913505
// Don't add the same antecedent twice.
34923506
if (!label.antecedents.some((existing) => existing.id === antecedent.id)) {
34933507
label.antecedents.push(antecedent);

packages/pyright-internal/src/analyzer/checker.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2846,7 +2846,23 @@ export class Checker extends ParseTreeWalker {
28462846
const start = statement.start;
28472847
const lastStatement = statements[statements.length - 1];
28482848
const end = TextRange.getEnd(lastStatement);
2849-
this._evaluator.addUnreachableCode(statement, reachability, { start, length: end - start });
2849+
const textRange: TextRange = { start, length: end - start };
2850+
2851+
if (
2852+
reachability === Reachability.UnreachableByAnalysis ||
2853+
reachability === Reachability.UnreachableStructural
2854+
) {
2855+
this._evaluator.addDiagnosticForTextRange(
2856+
this._fileInfo,
2857+
DiagnosticRule.reportUnreachable,
2858+
reachability === Reachability.UnreachableStructural
2859+
? LocMessage.unreachableCodeStructure()
2860+
: LocMessage.unreachableCodeType(),
2861+
statement.nodeType === ParseNodeType.Error ? statement : statement.d.firstToken
2862+
);
2863+
}
2864+
2865+
this._evaluator.addUnreachableCode(statement, reachability, textRange);
28502866

28512867
reportedUnreachable = true;
28522868
}
@@ -7682,6 +7698,14 @@ export class Checker extends ParseTreeWalker {
76827698
LocMessage.unreachableExcept() + diagAddendum.getString(),
76837699
except.d.typeExpr
76847700
);
7701+
7702+
this._evaluator.addDiagnostic(
7703+
DiagnosticRule.reportUnreachable,
7704+
LocMessage.unreachableCodeType(),
7705+
except.d.exceptSuite,
7706+
except.d.exceptToken
7707+
);
7708+
76857709
this._evaluator.addUnreachableCode(
76867710
except,
76877711
Reachability.UnreachableByAnalysis,

packages/pyright-internal/src/analyzer/codeFlowEngine.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -492,7 +492,7 @@ export function getCodeFlowEngine(
492492
);
493493
}
494494

495-
if (curFlowNode.flags & FlowFlags.Unreachable) {
495+
if (curFlowNode.flags & (FlowFlags.UnreachableStaticCondition | FlowFlags.UnreachableStructural)) {
496496
// We can get here if there are nodes in a compound logical expression
497497
// (e.g. "False and x") that are never executed but are evaluated.
498498
return setCacheEntry(curFlowNode, NeverType.createNever(), /* isIncomplete */ false);
@@ -1302,14 +1302,18 @@ export function getCodeFlowEngine(
13021302
// If we've already visited this node, we can assume
13031303
// it wasn't reachable.
13041304
if (visitedFlowNodeSet.has(curFlowNode.id)) {
1305-
return cacheReachabilityResult(Reachability.UnreachableAlways);
1305+
return cacheReachabilityResult(Reachability.UnreachableStructural);
13061306
}
13071307

13081308
// Note that we've been here before.
13091309
visitedFlowNodeSet.add(curFlowNode.id);
13101310

1311-
if (curFlowNode.flags & FlowFlags.Unreachable) {
1312-
return cacheReachabilityResult(Reachability.UnreachableAlways);
1311+
if (curFlowNode.flags & FlowFlags.UnreachableStructural) {
1312+
return cacheReachabilityResult(Reachability.UnreachableStructural);
1313+
}
1314+
1315+
if (curFlowNode.flags & FlowFlags.UnreachableStaticCondition) {
1316+
return cacheReachabilityResult(Reachability.UnreachableStaticCondition);
13131317
}
13141318

13151319
if (curFlowNode === sourceFlowNode) {
@@ -1430,16 +1434,23 @@ export function getCodeFlowEngine(
14301434

14311435
const labelNode = curFlowNode as FlowLabel;
14321436
let unreachableByType = false;
1437+
let unreachableByStaticCondition = false;
14331438
for (const antecedent of labelNode.antecedents) {
14341439
const reachability = getFlowNodeReachabilityRecursive(antecedent, recursionCount);
14351440
if (reachability === Reachability.Reachable) {
14361441
return cacheReachabilityResult(reachability);
14371442
} else if (reachability === Reachability.UnreachableByAnalysis) {
14381443
unreachableByType = true;
1444+
} else if (reachability === Reachability.UnreachableStaticCondition) {
1445+
unreachableByStaticCondition = true;
14391446
}
14401447
}
14411448
return cacheReachabilityResult(
1442-
unreachableByType ? Reachability.UnreachableByAnalysis : Reachability.UnreachableAlways
1449+
unreachableByType
1450+
? Reachability.UnreachableByAnalysis
1451+
: unreachableByStaticCondition
1452+
? Reachability.UnreachableStaticCondition
1453+
: Reachability.UnreachableStructural
14431454
);
14441455
}
14451456

@@ -1526,7 +1537,10 @@ export function getCodeFlowEngine(
15261537
return startingConstraints;
15271538
}
15281539

1529-
if (curFlowNode.flags & (FlowFlags.Unreachable | FlowFlags.Start)) {
1540+
if (
1541+
curFlowNode.flags &
1542+
(FlowFlags.UnreachableStaticCondition | FlowFlags.UnreachableStructural | FlowFlags.Start)
1543+
) {
15301544
return startingConstraints;
15311545
}
15321546

packages/pyright-internal/src/analyzer/codeFlowTypes.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,15 @@ import {
3333
import { OperatorType } from '../parser/tokenizerTypes';
3434

3535
export enum FlowFlags {
36-
Unreachable = 1 << 0, // Unreachable code
37-
Start = 1 << 1, // Entry point
38-
BranchLabel = 1 << 2, // Junction for forward control flow
39-
LoopLabel = 1 << 3, // Junction for backward control flow
40-
Assignment = 1 << 4, // Assignment statement
41-
Unbind = 1 << 5, // Used with assignment to indicate target should be unbound
42-
WildcardImport = 1 << 6, // For "from X import *" statements
43-
TrueCondition = 1 << 7, // Condition known to be true
36+
UnreachableStructural = 1 << 0, // Code that is structurally unreachable (e.g. following a return statement)
37+
UnreachableStaticCondition = 1 << 1, // code that is unreachable due to a condition that the binder evaluates to False
38+
Start = 1 << 2, // Entry point
39+
BranchLabel = 1 << 3, // Junction for forward control flow
40+
LoopLabel = 1 << 4, // Junction for backward control flow
41+
Assignment = 1 << 5, // Assignment statement
42+
Unbind = 1 << 6, // Used with assignment to indicate target should be unbound
43+
WildcardImport = 1 << 7, // For "from X import *" statements
44+
TrueCondition = 1 << 8, // Condition known to be true
4445
FalseCondition = 1 << 9, // Condition known to be false
4546
Call = 1 << 10, // Call node
4647
PreFinallyGate = 1 << 11, // Injected edge that links pre-finally label and pre-try flow

0 commit comments

Comments
 (0)