Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions packages/pyright-internal/src/analyzer/binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
GlobalNode,
IfNode,
ImportAsNode,
ImportFromAsNode,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue · Please address or respond

ImportFromAsNode is unused by this change, so this will fail unused-import checks. Remove the import.

ImportFromNode,
ImportNode,
IndexNode,
Expand Down Expand Up @@ -3071,13 +3072,28 @@ export class Binder extends ParseTreeWalker {
if (!this._moduleSymbolOnly) {
const dummyScopeGenerator = new DummyScopeGenerator(this._currentScope, this._nodeInfo);
dummyScopeGenerator.walk(statement);

// Assignments in unreachable code still make names local, matching
// CPython. Bind those names without type-checking the dead code.
this._bindNamesInUnreachableCode(statement);
}
}
}

return false;
}

private _bindNamesInUnreachableCode(node: ParseNode) {
const bindTarget = (target: ExpressionNode) => {
this._bindPossibleTupleNamedTarget(target);
};
const bindName = (name: NameNode) => {
this._bindNameToScope(this._currentScope, name);
};

new UnreachableNameBinder(bindTarget, bindName).walk(node);
}

private _createStartFlowNode() {
const flowNode: FlowNode = {
flags: FlowFlags.Start,
Expand Down Expand Up @@ -4865,6 +4881,110 @@ export class ReturnFinder extends ParseTreeWalker {
}
}

// Binds assignment targets in unreachable code so they still create local
// symbols, matching CPython (an assignment after `return` makes the name
// local for the entire function). Nested functions and classes are skipped
// because DummyScopeGenerator already created their scopes.
class UnreachableNameBinder extends ParseTreeWalker {
constructor(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning · Non-blocking recommendation

Unreachable global and nonlocal directives are not processed, so a later assignment can be bound to the current local scope rather than the declared scope. Handle these directives before binding targets and add focused unreachable-directive coverage.

private readonly _bindTarget: (target: ExpressionNode) => void,
private readonly _bindName: (name: NameNode) => void
) {
super();
}

override visitAssignment(node: AssignmentNode): boolean {
this._bindTarget(node.d.leftExpr);
return true;
}

override visitAugmentedAssignment(node: AugmentedAssignmentNode): boolean {
this._bindTarget(node.d.leftExpr);
return true;
}

override visitTypeAnnotation(node: TypeAnnotationNode): boolean {
this._bindTarget(node.d.valueExpr);
return true;
}

override visitAssignmentExpression(node: AssignmentExpressionNode): boolean {
this._bindName(node.d.name);
return true;
}

override visitFor(node: ForNode): boolean {
this._bindTarget(node.d.targetExpr);
return true;
}

override visitWith(node: WithNode): boolean {
node.d.withItems.forEach((item) => {
if (item.d.target) {
this._bindTarget(item.d.target);
}
});
return true;
}

override visitDel(node: DelNode): boolean {
node.d.targets.forEach((target) => {
this._bindTarget(target);
});
return true;
}

override visitExcept(node: ExceptNode): boolean {
if (node.d.name) {
this._bindName(node.d.name);
}
return true;
}

override visitImportAs(node: ImportAsNode): boolean {
if (node.d.alias) {
this._bindName(node.d.alias);
} else if (node.d.module.d.nameParts.length > 0) {
this._bindName(node.d.module.d.nameParts[0]);
}
return false;
}

override visitImportFrom(node: ImportFromNode): boolean {
node.d.imports.forEach((importSymbolNode) => {
this._bindName(importSymbolNode.d.alias || importSymbolNode.d.name);
});
return false;
}

override visitFunction(node: FunctionNode): boolean {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning · Non-blocking recommendation

This walker skips function/class headers but descends into lambda bodies. That misses assignment expressions in decorators, defaults, and class bases that bind in the enclosing scope, while incorrectly treating assignment expressions in a lambda body as enclosing-scope bindings. Please handle those scope boundaries explicitly and add regressions for both cases.

this._bindName(node.d.name);
return false;
}

override visitClass(node: ClassNode): boolean {
this._bindName(node.d.name);
return false;
}

override visitTypeAlias(node: TypeAliasNode): boolean {
this._bindName(node.d.name);
return false;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning · Non-blocking recommendation

Returning false for nested functions and classes skips decorators, default values, bases, and keywords, which are evaluated in the enclosing scope. Assignment expressions in those header expressions therefore miss their enclosing binding; traverse the headers while excluding only nested bodies.


override visitPatternAs(node: PatternAsNode): boolean {
if (node.d.target) {
this._bindName(node.d.target);
}
return true;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue · Please address or respond

UnreachableNameBinder does not process global or nonlocal declarations. Consequently, an unreachable global x; x = ... or nonlocal x; x = ... binds the assignment directly in the current scope, incorrectly making it local. Preserve the declarations' binding semantics before binding targets and cover unreachable declaration cases.


override visitPatternCapture(node: PatternCaptureNode): boolean {
this._bindName(node.d.target);
return false;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning · Non-blocking recommendation

UnreachableNameBinder has no lambda scope boundary. Walking lambda: (x := 1) will bind x in the enclosing scope even though the assignment expression belongs to the lambda's scope. Skip lambda bodies and add a regression case.

}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning · Non-blocking recommendation

This new visitor duplicates the primary binder's syntax and scope classification, and the missing directive and scope-boundary cases already demonstrate drift. Reuse or centralize the scope-aware binding classification so future syntax additions do not update only one path.


// Creates dummy scopes for classes or functions within a parse tree.
// This is needed in cases where the parse tree has been determined
// to be unreachable. There are code paths where the type evaluator
Expand Down
25 changes: 25 additions & 0 deletions packages/pyright-internal/src/tests/samples/unbound7.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# This sample tests that assignments in unreachable code still make
# a name local, matching CPython.

variable = "global"


def example_with_local():
# This should generate an error because the later assignment makes
# "variable" a local, so this read is unbound.
return variable
variable = "local"


def example_without_local():
return variable


def outer():
def inner():
# This should not generate an error; the assignment below binds
# "variable" in outer even though it is unreachable.
nonlocal variable

return
variable = "local"
6 changes: 6 additions & 0 deletions packages/pyright-internal/src/tests/typeEvaluator2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,12 @@ test('Unbound6', () => {
TestUtils.validateResults(analysisResults, 8);
});

test('Unbound7', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['unbound7.py']);

TestUtils.validateResults(analysisResults, 1);
});

test('LiteralForLoop1', () => {
const configOptions = new ConfigOptions(Uri.empty());
configOptions.diagnosticRuleSet.reportPossiblyUnboundVariable = 'error';
Expand Down