Skip to content

fix: support reversed operands in static sys.version_info and platform comparisons - #11947

Open
Peter Hayes (HTMLPETE) wants to merge 3 commits into
microsoft:mainfrom
HTMLPETE:fix/reversed-sys-version-info-11927
Open

Peter Hayes (HTMLPETE) wants to merge 3 commits into
microsoft:mainfrom
HTMLPETE:fix/reversed-sys-version-info-11927

Conversation

@HTMLPETE

Copy link
Copy Markdown

Closes #11927

This change adds support for reversed binary comparison forms in staticExpressions.ts:

  • (tuple) [op] sys.version_info
  • (int) [op] sys.version_info[0]
  • 'string' [op] sys.platform
  • 'string' [op] os.name

Previously, only expressions with sys.version_info, sys.platform, or os.name on the left-hand side were statically evaluated for reachability and type checking.

Includes regression test coverage in StaticExpression3.

…m comparisons (microsoft#11927)

Signed-off-by: Peter Hayes <344978951+HTMLPETE@users.noreply.github.com>
@heejaechang

Copy link
Copy Markdown
Collaborator

🔴 Pyright CLI QA — 2c3eef9f — verdict: red

Automated CLI checks compared this change with its base revision.
27 focused check(s) completed.

Needs review: reportAssignmentType no longer reported

The base reports reportAssignmentType for this example; the PR does not. Is this change intended?

Reproduction and observed results

Create these files in an empty directory:

main.py

reveal_type(0)
import sys
if (3, 13) < sys.version_info:
    value: int = "not an int"

pyrightconfig.json

{
  "include": [
    "main.py"
  ],
  "pythonVersion": "3.13",
  "pythonPlatform": "Linux",
  "typeCheckingMode": "basic"
}

From that directory, run each built Pyright revision:

node <pyright-cli-entrypoint> --outputjson -p .

Base e6d4bda989a07e384ed087ce73af5b6b4d69b64d

{
  "exit_code": 1,
  "diagnostics": [
    {
      "file": "main.py",
      "severity": "error",
      "rule": "reportAssignmentType",
      "message": "Type \"Literal['not an int']\" is not assignable to declared type \"int\"\n  \"Literal['not an int']\" is not assignable to \"int\"",
      "range": {
        "start": [
          3,
          17
        ],
        "end": [
          3,
          29
        ]
      }
    },
    {
      "file": "main.py",
      "severity": "information",
      "rule": null,
      "message": "Type of \"0\" is \"Literal[0]\"",
      "range": {
        "start": [
          0,
          12
        ],
        "end": [
          0,
          13
        ]
      }
    }
  ]
}

PR 2c3eef9fef402248c1cee0345dfd26a4f49ebaad

{
  "exit_code": 0,
  "diagnostics": [
    {
      "file": "main.py",
      "severity": "information",
      "rule": null,
      "message": "Type of \"0\" is \"Literal[0]\"",
      "range": {
        "start": [
          0,
          12
        ],
        "end": [
          0,
          13
        ]
      }
    }
  ]
}

Diagnostic ranges use zero-based line and character positions.

Needs review: reportAttributeAccessIssue newly reported

The PR reports reportAttributeAccessIssue for this example; the base does not. Is this change intended?

Reproduction and observed results

Create these files in an empty directory:

main.py

reveal_type(0)
import sys
from dataclasses import dataclass
from typing import assert_type
@dataclass(frozen=b"linux" == sys.platform)
class Record:
    value: int
record = Record(1)
assert_type(record.value, int)
record.value = 2

pyrightconfig.json

{
  "include": [
    "main.py"
  ],
  "pythonVersion": "3.13",
  "pythonPlatform": "Linux",
  "typeCheckingMode": "basic"
}

From that directory, run each built Pyright revision:

node <pyright-cli-entrypoint> --outputjson -p .

Base e6d4bda989a07e384ed087ce73af5b6b4d69b64d

{
  "exit_code": 0,
  "diagnostics": [
    {
      "file": "main.py",
      "severity": "information",
      "rule": null,
      "message": "Type of \"0\" is \"Literal[0]\"",
      "range": {
        "start": [
          0,
          12
        ],
        "end": [
          0,
          13
        ]
      }
    }
  ]
}

PR 2c3eef9fef402248c1cee0345dfd26a4f49ebaad

{
  "exit_code": 1,
  "diagnostics": [
    {
      "file": "main.py",
      "severity": "error",
      "rule": "reportAttributeAccessIssue",
      "message": "Cannot assign to attribute \"value\" for class \"Record\"\n  Attribute \"value\" is read-only",
      "range": {
        "start": [
          9,
          7
        ],
        "end": [
          9,
          12
        ]
      }
    },
    {
      "file": "main.py",
      "severity": "information",
      "rule": null,
      "message": "Type of \"0\" is \"Literal[0]\"",
      "range": {
        "start": [
          0,
          12
        ],
        "end": [
          0,
          13
        ]
      }
    }
  ]
}

Diagnostic ranges use zero-based line and character positions.

Needs review: reportAssignmentType no longer reported

The base reports reportAssignmentType for this example; the PR does not. Is this change intended?

Reproduction and observed results

Create these files in an empty directory:

main.py

reveal_type(0)
import sys
if f"{sys.platform}" == sys.platform:
    value: int = "not an int"

pyrightconfig.json

{
  "include": [
    "main.py"
  ],
  "pythonVersion": "3.13",
  "pythonPlatform": "Linux",
  "typeCheckingMode": "basic"
}

From that directory, run each built Pyright revision:

node <pyright-cli-entrypoint> --outputjson -p .

Base e6d4bda989a07e384ed087ce73af5b6b4d69b64d

{
  "exit_code": 1,
  "diagnostics": [
    {
      "file": "main.py",
      "severity": "error",
      "rule": "reportAssignmentType",
      "message": "Type \"Literal['not an int']\" is not assignable to declared type \"int\"\n  \"Literal['not an int']\" is not assignable to \"int\"",
      "range": {
        "start": [
          3,
          17
        ],
        "end": [
          3,
          29
        ]
      }
    },
    {
      "file": "main.py",
      "severity": "information",
      "rule": null,
      "message": "Type of \"0\" is \"Literal[0]\"",
      "range": {
        "start": [
          0,
          12
        ],
        "end": [
          0,
          13
        ]
      }
    }
  ]
}

PR 2c3eef9fef402248c1cee0345dfd26a4f49ebaad

{
  "exit_code": 0,
  "diagnostics": [
    {
      "file": "main.py",
      "severity": "information",
      "rule": null,
      "message": "Type of \"0\" is \"Literal[0]\"",
      "range": {
        "start": [
          0,
          12
        ],
        "end": [
          0,
          13
        ]
      }
    }
  ]
}

Diagnostic ranges use zero-based line and character positions.

Needs review: reportAssignmentType no longer reported

The base reports reportAssignmentType for this example; the PR does not. Is this change intended?

Reproduction and observed results

Create these files in an empty directory:

main.py

reveal_type(0)
import sys
if 3j != sys.version_info[0]:
    value: int = "not an int"

pyrightconfig.json

{
  "include": [
    "main.py"
  ],
  "pythonVersion": "3.13",
  "pythonPlatform": "Linux",
  "typeCheckingMode": "basic"
}

From that directory, run each built Pyright revision:

node <pyright-cli-entrypoint> --outputjson -p .

Base e6d4bda989a07e384ed087ce73af5b6b4d69b64d

{
  "exit_code": 1,
  "diagnostics": [
    {
      "file": "main.py",
      "severity": "error",
      "rule": "reportAssignmentType",
      "message": "Type \"Literal['not an int']\" is not assignable to declared type \"int\"\n  \"Literal['not an int']\" is not assignable to \"int\"",
      "range": {
        "start": [
          3,
          17
        ],
        "end": [
          3,
          29
        ]
      }
    },
    {
      "file": "main.py",
      "severity": "information",
      "rule": null,
      "message": "Type of \"0\" is \"Literal[0]\"",
      "range": {
        "start": [
          0,
          12
        ],
        "end": [
          0,
          13
        ]
      }
    }
  ]
}

PR 2c3eef9fef402248c1cee0345dfd26a4f49ebaad

{
  "exit_code": 0,
  "diagnostics": [
    {
      "file": "main.py",
      "severity": "information",
      "rule": null,
      "message": "Type of \"0\" is \"Literal[0]\"",
      "range": {
        "start": [
          0,
          12
        ],
        "end": [
          0,
          13
        ]
      }
    }
  ]
}

Diagnostic ranges use zero-based line and character positions.

Needs review: reportAssignmentType no longer reported

The base reports reportAssignmentType for this example; the PR does not. Is this change intended?

Reproduction and observed results

Create these files in an empty directory:

main.py

reveal_type(0)
from typing import Literal
class Runtime:
    name: Literal["custom"] = "custom"
os = Runtime()
if "custom" == os.name:
    value: int = "not an int"

pyrightconfig.json

{
  "include": [
    "main.py"
  ],
  "pythonVersion": "3.13",
  "pythonPlatform": "Linux",
  "typeCheckingMode": "basic"
}

From that directory, run each built Pyright revision:

node <pyright-cli-entrypoint> --outputjson -p .

Base e6d4bda989a07e384ed087ce73af5b6b4d69b64d

{
  "exit_code": 1,
  "diagnostics": [
    {
      "file": "main.py",
      "severity": "error",
      "rule": "reportAssignmentType",
      "message": "Type \"Literal['not an int']\" is not assignable to declared type \"int\"\n  \"Literal['not an int']\" is not assignable to \"int\"",
      "range": {
        "start": [
          6,
          17
        ],
        "end": [
          6,
          29
        ]
      }
    },
    {
      "file": "main.py",
      "severity": "information",
      "rule": null,
      "message": "Type of \"0\" is \"Literal[0]\"",
      "range": {
        "start": [
          0,
          12
        ],
        "end": [
          0,
          13
        ]
      }
    }
  ]
}

PR 2c3eef9fef402248c1cee0345dfd26a4f49ebaad

{
  "exit_code": 0,
  "diagnostics": [
    {
      "file": "main.py",
      "severity": "information",
      "rule": null,
      "message": "Type of \"0\" is \"Literal[0]\"",
      "range": {
        "start": [
          0,
          12
        ],
        "end": [
          0,
          13
        ]
      }
    }
  ]
}

Diagnostic ranges use zero-based line and character positions.

…n evaluation

- Add _isPlainStringList to ensure bytes literals and format strings are never evaluated as static string matches
- Guard against imaginary numbers in reversed version comparisons (!node.d.leftExpr.d.isImaginary)
@HTMLPETE

Copy link
Copy Markdown
Author

Thanks Heejae Chang (@heejaechang) for running the QA bot!

Here is the breakdown of the flagged cases:

  1. (3, 13) < sys.version_info: Intended. When pythonVersion: "3.13", (3, 13) < (3, 13) evaluates statically to false. The body is dead code, which correctly suppresses unreachable assignment errors.
  2. "custom" == os.name: Intended. Follows the same static evaluation rules as os.name == "custom", evaluating to false when target platform is Linux (posix), correctly pruning the dead branch.
  3. b"linux" == sys.platform & f"{sys.platform}" == sys.platform: Fixed in 06b54fab. Added _isPlainStringList to ensure bytes tokens (StringTokenFlags.Bytes) and format strings (StringTokenFlags.Format) are never evaluated as static string matches against sys.platform / os.name.
  4. 3j != sys.version_info[0]: Fixed in 06b54fab. Added !node.d.leftExpr.d.isImaginary guard to prevent imaginary numbers from being treated as integers in reversed version comparisons.

@crypt0lith

Copy link
Copy Markdown
Contributor

This PR is a duplicate of #11929, and the author has opened duplicates of other PRs in the past (#11895). The author is encouraged to read the GitHub Acceptable Use Policy for spam and inauthentic activity.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Reversed sys.version_info comparison (tuple on the left) is not evaluated as a static condition.

3 participants