Skip to content

Names in unreachable code (version, platform or TYPE_CHECKING branches) become a new local N each time #232

Open

Description

@Chaptyziok

Version: scip-python 0.6.6

Every name inside code that pyright considers unreachable is emitted as a fresh local N, a new symbol at each occurrence. This covers the branch of a sys.version_info or sys.platform check that doesn't match the indexing environment, the else of if TYPE_CHECKING:, and code after return. References to imported functions and module variables in those branches are lost.

demo/lib.py

def helper() -> int:
    return 1

demo/gated.py

import sys
from typing import TYPE_CHECKING

from demo.lib import helper

if sys.version_info >= (3, 8):
    a = 1
else:
    a = helper()

if TYPE_CHECKING:
    b = 1
else:
    b = helper()

if sys.platform == "win32":
    c = helper()

scip-python snapshot-dir output on Linux:

from demo.lib import helper
#                    ^^^^^^ reference  demo 0.1.0 `demo.lib`/helper().
    a = 1
#   ^ definition  demo 0.1.0 `demo.gated`/a.
    a = helper()
#   ^ reference local 0
#       ^^^^^^ reference local 1
    b = helper()
#   ^ reference local 2
#       ^^^^^^ reference local 3
    c = helper()
#   ^ reference local 4
#       ^^^^^^ reference local 5

Expected: helper should reference demo 0.1.0 `demo.lib`/helper(). everywhere, and a/b should reference demo 0.1.0 `demo.gated`/a./b.. An if creates no scope, so these names bind exactly as they do outside the branch. Which branch is "unreachable" depends only on the Python version and platform used for indexing, so the same source gives different indexes on different machines.

Cause: visitName and visitAssignment call evaluator.getDeclarationsForNameNode(node), whose skipUnreachableCode parameter defaults to true. That returns no declarations for unreachable nodes, and emitNameWithoutDeclaration then makes a new local each time. The member-access path in the same file already passes false.

Possible fix (packages/pyright-scip/src/treeVisitor.ts):

-            const decls = this.evaluator.getDeclarationsForNameNode(node.leftExpression) || [];
+            const decls = this.evaluator.getDeclarationsForNameNode(node.leftExpression, /* skipUnreachableCode */ false) || [];
-        const decls = this.evaluator.getDeclarationsForNameNode(node) || [];
+        const decls = this.evaluator.getDeclarationsForNameNode(node, /* skipUnreachableCode */ false) || [];

With this change, helper, a and b in the example resolve to their real symbols. c, which is only ever assigned inside the unreachable branch, stays one local. The existing snapshot outputs are unchanged. I can open a PR with a snapshot test.

Activity

  1. linear commented on Oct 21, 2026

    @linear
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions