├── mkdocs_build.sh ├── mkdocs_build.bat ├── .gitignore ├── extension ├── .gitignore ├── .vscode │ ├── extensions.json │ ├── launch.json │ └── tasks.json ├── images │ └── pylyzer-logo.png ├── .vscodeignore ├── src │ ├── test │ │ ├── suite │ │ │ ├── extension.test.ts │ │ │ └── index.ts │ │ └── runTest.ts │ ├── commands.ts │ └── extension.ts ├── tsconfig.json ├── biome.json ├── LICENSE ├── webpack.config.js ├── README.md └── package.json ├── images ├── report.png ├── analysis.png ├── autoimport.gif ├── lsp_support.png ├── performance.png ├── pylyzer-logo.png ├── pyright_report.png ├── pylyzer-logo-with-letters.png ├── pylyzer-logo.svg └── pylyzer-logo-with-letters.svg ├── tests ├── foo │ ├── baz │ │ └── __init__.py │ ├── __init__.py │ └── bar.py ├── literal.py ├── warns.py ├── decl.py ├── property.py ├── err │ ├── property.py │ ├── type_spec.py │ └── class.py ├── widening.py ├── pyi.pyi ├── union.py ├── list.py ├── export.py ├── func.py ├── errors.py ├── narrowing.py ├── casting.py ├── abc.py ├── dict.py ├── pyi.py ├── call.py ├── typevar.py ├── test.py ├── shadowing.py ├── projection.py ├── collection.py ├── import.py ├── typespec.py ├── class.py └── test.rs ├── pyproject.toml ├── docs ├── assets │ ├── pylyzer-logo.png │ └── pylyzer-logo.svg ├── errors │ ├── warns.md │ └── errors.md ├── options │ ├── pyproject.md │ └── options.md └── editor.md ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature-request.yaml │ └── bug-report.yaml ├── workflows │ ├── stale-issues.yml │ ├── rust.yml │ ├── test.yml │ └── release.yml └── FUNDING.yml ├── crates ├── pylyzer_core │ ├── lib.rs │ ├── Cargo.toml │ ├── handle_err.rs │ └── analyze.rs ├── py2erg │ ├── lib.rs │ ├── ast_util.rs │ ├── Cargo.toml │ ├── error.rs │ └── gen_decl.rs └── pylyzer_wasm │ ├── README.md │ ├── Cargo.toml │ └── lib.rs ├── publish.sh ├── .cargo └── config.toml ├── .vscode └── settings.json ├── .pre-commit-config.yaml ├── cargo_publish.ps1 ├── cargo_publish.sh ├── LICENSE ├── mkdocs.yml ├── src ├── main.rs ├── copy.rs └── config.rs ├── setup.py ├── Cargo.toml ├── README.md └── Cargo.lock /mkdocs_build.sh: -------------------------------------------------------------------------------- 1 | cp README.md docs/index.md 2 | -------------------------------------------------------------------------------- /mkdocs_build.bat: -------------------------------------------------------------------------------- 1 | copy README.md docs\index.md 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | __pycache__/ 3 | test*.py 4 | /site 5 | .venv 6 | -------------------------------------------------------------------------------- /extension/.gitignore: -------------------------------------------------------------------------------- 1 | *.vsix 2 | node_modules 3 | dist 4 | example.py 5 | -------------------------------------------------------------------------------- /images/report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtshiba/pylyzer/HEAD/images/report.png -------------------------------------------------------------------------------- /tests/foo/baz/__init__.py: -------------------------------------------------------------------------------- 1 | i = 0 2 | 3 | class Bar: 4 | CONST = "foo.baz.bar" 5 | -------------------------------------------------------------------------------- /images/analysis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtshiba/pylyzer/HEAD/images/analysis.png -------------------------------------------------------------------------------- /images/autoimport.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtshiba/pylyzer/HEAD/images/autoimport.gif -------------------------------------------------------------------------------- /images/lsp_support.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtshiba/pylyzer/HEAD/images/lsp_support.png -------------------------------------------------------------------------------- /images/performance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtshiba/pylyzer/HEAD/images/performance.png -------------------------------------------------------------------------------- /images/pylyzer-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtshiba/pylyzer/HEAD/images/pylyzer-logo.png -------------------------------------------------------------------------------- /images/pyright_report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtshiba/pylyzer/HEAD/images/pyright_report.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "setuptools-rust", "wheel", "tomli"] 3 | -------------------------------------------------------------------------------- /docs/assets/pylyzer-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtshiba/pylyzer/HEAD/docs/assets/pylyzer-logo.png -------------------------------------------------------------------------------- /tests/foo/__init__.py: -------------------------------------------------------------------------------- 1 | from .bar import i, Bar, Baz, Qux 2 | 3 | from . import bar 4 | from . import baz 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | # This file cannot use the extension `.yaml`. 2 | blank_issues_enabled: false 3 | -------------------------------------------------------------------------------- /extension/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["amodio.tsl-problem-matcher", "biomejs.biome"] 3 | } 4 | -------------------------------------------------------------------------------- /extension/images/pylyzer-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtshiba/pylyzer/HEAD/extension/images/pylyzer-logo.png -------------------------------------------------------------------------------- /images/pylyzer-logo-with-letters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtshiba/pylyzer/HEAD/images/pylyzer-logo-with-letters.png -------------------------------------------------------------------------------- /crates/pylyzer_core/lib.rs: -------------------------------------------------------------------------------- 1 | mod analyze; 2 | mod handle_err; 3 | 4 | pub use analyze::{PythonAnalyzer, SimplePythonParser}; 5 | -------------------------------------------------------------------------------- /tests/literal.py: -------------------------------------------------------------------------------- 1 | name = "John" 2 | 3 | print(f"Hello, {name}!") 4 | print(f"Hello, {nome}!") # ERR 5 | print(f"Hello, {name + 1}!") # ERR 6 | -------------------------------------------------------------------------------- /crates/py2erg/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod ast_util; 2 | mod convert; 3 | mod error; 4 | mod gen_decl; 5 | 6 | pub use convert::*; 7 | pub use gen_decl::*; 8 | -------------------------------------------------------------------------------- /tests/warns.py: -------------------------------------------------------------------------------- 1 | # W0188: unused value 2 | 3 | 1 # Warn 4 | 5 | def f(): return "a" 6 | f() # Warn 7 | 8 | def f(): return None 9 | f() # OK 10 | -------------------------------------------------------------------------------- /tests/decl.py: -------------------------------------------------------------------------------- 1 | i: int 2 | if True: 3 | i = 1 4 | else: 5 | i = 2 6 | 7 | j: int 8 | if True: 9 | j = "1" # ERR 10 | else: 11 | j = "2" 12 | -------------------------------------------------------------------------------- /tests/foo/bar.py: -------------------------------------------------------------------------------- 1 | i = 0 2 | 3 | class Bar: 4 | CONST = "foo.bar" 5 | def f(self): return 1 6 | 7 | class Baz(Exception): 8 | CONST = "foo.baz" 9 | pass 10 | 11 | class Qux(Baz): 12 | pass 13 | -------------------------------------------------------------------------------- /tests/property.py: -------------------------------------------------------------------------------- 1 | class Foo: 2 | x: int 3 | def __init__(self, x): 4 | self.x = x 5 | 6 | @property 7 | def foo(self): 8 | return self.x 9 | 10 | f = Foo(1) 11 | assert f.foo + 1 == 2 12 | -------------------------------------------------------------------------------- /extension/.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/** 4 | node_modules/** 5 | src/** 6 | **/tsconfig.json 7 | # **/.eslintrc.json 8 | **/*.map 9 | **/*.ts 10 | .gitignore 11 | webpack.config.js 12 | biome.json 13 | -------------------------------------------------------------------------------- /docs/errors/warns.md: -------------------------------------------------------------------------------- 1 | # Pylyzer-specific warnings 2 | 3 | ## W0188: Used value 4 | 5 | ```python 6 | def f(x): return x 7 | 8 | f(1) # W0188: UnusedWarning: the evaluation result of the expression (: {1, }) is not used 9 | ``` 10 | -------------------------------------------------------------------------------- /tests/err/property.py: -------------------------------------------------------------------------------- 1 | class Foo: 2 | x: int 3 | def __init__(self, x): 4 | self.x = x 5 | 6 | @property 7 | def foo(self): 8 | return self.x 9 | 10 | f = Foo(1) 11 | print(f.foo + "a") # ERR 12 | -------------------------------------------------------------------------------- /tests/widening.py: -------------------------------------------------------------------------------- 1 | b = False 2 | if True: 3 | b = True 4 | if True: 5 | b = "a" # ERR 6 | 7 | counter = 100 # counter: Literal[100] 8 | while counter > 0: 9 | counter -= 1 # counter: Int 10 | counter -= 1.0 # counter: Float 11 | -------------------------------------------------------------------------------- /tests/pyi.pyi: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | x: typing.Any 4 | 5 | def f(x: int, y: int) -> int: ... 6 | 7 | class C: 8 | x: int 9 | y: int 10 | def __init__(self, x: int): ... 11 | def f(self, x: int) -> int: ... 12 | 13 | def g[T: C](x: T) -> T: ... 14 | -------------------------------------------------------------------------------- /docs/options/pyproject.md: -------------------------------------------------------------------------------- 1 | # `pyproject.toml` options 2 | 3 | ## `tool.pylyzer.python.path` 4 | 5 | Path to the Python interpreter to use. If not set, the default Python interpreter will be used. 6 | 7 | ```toml 8 | [tool.pylyzer.python] 9 | path = "path/to/python" 10 | ``` 11 | -------------------------------------------------------------------------------- /tests/err/type_spec.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, Mapping 2 | 3 | _: Mapping[int, str, str] = ... # ERR 4 | _: Mapping[int] = ... # ERR 5 | _: Callable[[int, str]] = ... # ERR 6 | _: Callable[int] = ... # ERR 7 | _: dict[int] = ... # ERR 8 | _: dict[int, int, int] = ... # ERR 9 | -------------------------------------------------------------------------------- /crates/pylyzer_wasm/README.md: -------------------------------------------------------------------------------- 1 | # pylyzer_wasm 2 | 3 | Wasm wrapper for pylyzer. 4 | 5 | ## Usage 6 | 7 | ```ts 8 | import { Analyzer } from 'pylyzer_wasm'; 9 | 10 | const analyzer = new Analyzer(); 11 | const errors = analyzer.check('print("Hello, World!")'); 12 | const locals = analyzer.dir(); 13 | ``` 14 | -------------------------------------------------------------------------------- /tests/union.py: -------------------------------------------------------------------------------- 1 | s: str | bytes = "" 2 | s2 = s.capitalize() 3 | s3 = s2.center(1) 4 | 5 | s4: str | bytes | bytearray = "" 6 | _ = s4.__len__() 7 | 8 | def f(x: str | bytes): 9 | return x.isalnum() 10 | 11 | def check(s: str | bytes | bytearray): 12 | if isinstance(s, (bytes, bytearray)): 13 | pass 14 | -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | os=$(uname -s) 2 | if [ "$os" = "Darwin" ]; then 3 | platform="macos" 4 | elif [ "$os" = "Linux" ]; then 5 | platform="linux" 6 | else 7 | echo "Unsupported platform: $os" 8 | exit 1 9 | fi 10 | cibuildwheel --output-dir dist --platform $platform 11 | twine upload -u mtshiba -p $PYPI_PASSWORD --skip-existing dist/* 12 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | r = "run" 3 | rd = "run --features debug" 4 | b = "build" 5 | bd = "build --features debug" 6 | r_re = "run --release" 7 | rd_re = "run --features debug --release" 8 | b_re = "build --release" 9 | bd_re = "build --features debug --release" 10 | i = "install --path ." 11 | di = "install --path . --debug --features debug" 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[python]": { 3 | "editor.defaultFormatter": "ms-python.black-formatter" 4 | }, 5 | "python.formatting.provider": "none", 6 | "[typescript]": { 7 | "editor.formatOnSave": true, 8 | "editor.defaultFormatter": "biomejs.biome" 9 | }, 10 | "cSpell.words": [ 11 | "indexmap" 12 | ] 13 | } -------------------------------------------------------------------------------- /tests/list.py: -------------------------------------------------------------------------------- 1 | l = [1, 2, 3] 2 | _ = l[1:2] 3 | _ = l[:] 4 | _ = l[1:] 5 | _ = l[:1] 6 | _ = l[1:1:1] 7 | print(l[2]) 8 | print(l["a"]) # ERR 9 | 10 | # OK 11 | for i in range(3): 12 | print(l[i]) 13 | # ERR 14 | for i in "abcd": 15 | print(l[i]) 16 | 17 | lis = "a,b,c".split(",") if True is not None else [] 18 | if "a" in lis: 19 | lis.remove("a") # OK 20 | -------------------------------------------------------------------------------- /tests/export.py: -------------------------------------------------------------------------------- 1 | import http.client 2 | 3 | test = 1 4 | 5 | def add(a, b): 6 | return a + b 7 | 8 | class C: 9 | x: int 10 | const = 1 11 | def __init__(self, x): 12 | self.x = x 13 | def method(self, y): return self.x + y 14 | 15 | class D(C): 16 | y: int 17 | def __init__(self, x, y): 18 | self.x = x 19 | self.y = y 20 | -------------------------------------------------------------------------------- /crates/py2erg/ast_util.rs: -------------------------------------------------------------------------------- 1 | use rustpython_parser::ast::located::Expr; 2 | 3 | pub fn accessor_name(expr: Expr) -> Option { 4 | match expr { 5 | Expr::Name(name) => Some(name.id.to_string()), 6 | Expr::Attribute(attr) => { 7 | accessor_name(*attr.value).map(|value| format!("{value}.{}", attr.attr)) 8 | } 9 | _ => None, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /docs/editor.md: -------------------------------------------------------------------------------- 1 | # Editor integrations 2 | 3 | ## VSCode 4 | 5 | * Install [the extension](https://marketplace.visualstudio.com/items?itemName=pylyzer.pylyzer). 6 | 7 | ## Neovim 8 | 9 | * [Setup mason](https://github.com/williamboman/mason.nvim). 10 | * Type `:MasonInstall pylyzer` in Neovim. 11 | 12 | > If you use [LunarVim](https://www.lunarvim.org/), no setup is required. Just install pylyzer and you are good to go. 13 | -------------------------------------------------------------------------------- /tests/func.py: -------------------------------------------------------------------------------- 1 | def f(x: int): 2 | for i in [1, 2, 3]: 3 | x += i 4 | print(x) 5 | return x 6 | 7 | i: int = f(1) 8 | 9 | def g(x: int): 10 | if True: 11 | x = "a" # ERR 12 | return x 13 | 14 | def h(x: str): 15 | if True: 16 | x = "a" # OK 17 | return x 18 | 19 | def var(*varargs, **kwargs): 20 | return varargs, kwargs 21 | 22 | _ = var(1, 2, 3, a=1, b=2, c=3) 23 | -------------------------------------------------------------------------------- /tests/errors.py: -------------------------------------------------------------------------------- 1 | # E0001 2 | 3 | def a(): return 1 4 | def a(): return "a" # OK 5 | 6 | print(a()) 7 | 8 | def g(): return f() 9 | 10 | def f(): return 1 11 | def f(): return "a" # E0001: Reassignment of a function referenced by other functions 12 | 13 | # E0002 14 | 15 | class C: 16 | def __init__(self): pass # OK 17 | class C: 18 | def __init__(a): pass # ERR 19 | 20 | # E0003 21 | 22 | class C: 23 | __init__ = 1 # ERR 24 | -------------------------------------------------------------------------------- /tests/narrowing.py: -------------------------------------------------------------------------------- 1 | def f(x: int | None): 2 | x + 1 # ERR 3 | if x != None: 4 | print(x + 1) # OK 5 | if isinstance(x, int): 6 | print(x + 1) # OK 7 | return None 8 | 9 | f(1) 10 | 11 | from typing import Optional 12 | 13 | x: Optional[int] = None 14 | if x is not None: 15 | x += 1 16 | 17 | def sb(s: str | bytes) -> None: 18 | if not isinstance(s, str): 19 | str(s, "ascii") 20 | return None 21 | -------------------------------------------------------------------------------- /tests/casting.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | s = "a" 4 | 5 | assert isinstance(s, int) # ERR 6 | 7 | # force cast to int 8 | i = typing.cast(int, s) 9 | print(i + 1) # OK 10 | 11 | l = typing.cast(list[str], [1, 2, 3]) 12 | _ = map(lambda x: x + "a", l) # OK 13 | 14 | d = typing.cast(dict[str, int], [1, 2, 3]) 15 | _ = map(lambda x: d["a"] + 1, d) # OK 16 | 17 | t = typing.cast(tuple[str, str], [1, 2, 3]) 18 | _ = map(lambda x: x + "a", t) # OK 19 | -------------------------------------------------------------------------------- /tests/err/class.py: -------------------------------------------------------------------------------- 1 | class Foo: 2 | def invalid_append(self): 3 | paths: list[str] = [] 4 | paths.append(self) # ERR 5 | 6 | class Bar: 7 | foos: list[Foo] 8 | 9 | def __init__(self, foos: list[Foo]) -> None: 10 | self.foos = foos 11 | 12 | def add_foo(self, foo: Foo): 13 | self.foos.append(foo) 14 | 15 | def invalid_add_foo(self): 16 | self.foos.append(1) # ERR 17 | 18 | _ = Bar([Bar([])]) # ERR 19 | -------------------------------------------------------------------------------- /tests/abc.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Sequence 2 | 3 | class Vec(Sequence): 4 | x: list[int] 5 | 6 | def __init__(self): 7 | self.x = [] 8 | 9 | def __getitem__(self, i: int) -> int: 10 | return self.x[i] 11 | 12 | def __iter__(self): 13 | return iter(self.x) 14 | 15 | def __len__(self) -> int: 16 | return len(self.x) 17 | 18 | def __contains__(self, i: int) -> bool: 19 | return i in self.x 20 | -------------------------------------------------------------------------------- /tests/dict.py: -------------------------------------------------------------------------------- 1 | dic = {"a": 1, "b": 2} 2 | 3 | def f(): 4 | dic = {"a": 1} 5 | _ = dic["b"] # ERR 6 | 7 | 8 | class TaskManager: 9 | def __init__(self): 10 | self.tasks: list[dict[str, int]] = [] 11 | 12 | def add_task(self, title: str, id: int): 13 | task = {title: id} 14 | self.tasks.append(task) 15 | 16 | def add_task2(self, title: str, id: int): 17 | task = {id: title} 18 | self.tasks.append(task) # ERR 19 | -------------------------------------------------------------------------------- /tests/pyi.py: -------------------------------------------------------------------------------- 1 | x = 1 2 | x + "a" # OK, because x: Any 3 | 4 | def f(x, y): 5 | return x + y 6 | 7 | class C: 8 | y = 1 9 | def __init__(self, x): 10 | self.x = x 11 | def f(self, x): 12 | return self.x + x 13 | 14 | print(f(1, 2)) # OK 15 | print(f("a", "b")) # ERR*2 16 | c = C(1) 17 | print(c.f(2)) # OK 18 | print(c.f("a")) # ERR 19 | _ = C("a") # ERR 20 | 21 | def g(x): 22 | pass 23 | 24 | print(g(c)) # OK 25 | print(g(1)) # ERR 26 | -------------------------------------------------------------------------------- /tests/call.py: -------------------------------------------------------------------------------- 1 | print("aaa", sep = ";", end = "") # OK 2 | print("a", sep=1) # ERR 3 | print("a", foo=None) # ERR 4 | 5 | def f(x, y=1): 6 | return x + y 7 | 8 | print(f(1, 2)) # OK 9 | print(f(1)) # OK 10 | print(f(1, y="a")) # ERR 11 | 12 | def g(first, second): 13 | pass 14 | 15 | g(**{"first": "bar", "second": 1}) # OK 16 | g(**[1, 2]) # ERR 17 | g(1, *[2]) # OK 18 | g(*[1, 2]) # OK 19 | g(1, 2, *[3, 4]) # ERR 20 | g(*1) # ERR 21 | g(*[1], **{"second": 1}) # OK 22 | 23 | _ = f(1, *[2]) # OK 24 | _ = f(**{"x": 1, "y": 2}) # OK 25 | -------------------------------------------------------------------------------- /crates/pylyzer_wasm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pylyzer_wasm" 3 | description = "Wasm wrapper for pylyzer" 4 | version.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | edition.workspace = true 8 | repository.workspace = true 9 | publish = false 10 | 11 | [dependencies] 12 | wasm-bindgen = "0.2" 13 | erg_common = { workspace = true } 14 | erg_compiler = { workspace = true } 15 | pylyzer_core = { version = "*", path = "../pylyzer_core" } 16 | 17 | [lib] 18 | crate-type = ["cdylib", "rlib"] 19 | path = "lib.rs" 20 | -------------------------------------------------------------------------------- /extension/src/test/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "node:assert"; 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | import * as vscode from "vscode"; 6 | // import * as myExtension from '../../extension'; 7 | 8 | suite("Extension Test Suite", () => { 9 | vscode.window.showInformationMessage("Start all tests."); 10 | 11 | test("Sample test", () => { 12 | assert.strictEqual(-1, [1, 2, 3].indexOf(5)); 13 | assert.strictEqual(-1, [1, 2, 3].indexOf(0)); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /docs/errors/errors.md: -------------------------------------------------------------------------------- 1 | # Pylyzer-specific errors 2 | 3 | ## E0001: Reassignment of a function referenced by other functions 4 | 5 | ```python 6 | def g(): return f() 7 | 8 | def f(): return 1 9 | def f(): return "a" # E0001: Reassignment of a function referenced by other functions 10 | 11 | print(g()) 12 | ``` 13 | 14 | ## E0002: `__init__` doesn't have a first parameter named `self` 15 | 16 | ```python 17 | class C: 18 | def __init__(a): pass # E0002 19 | ``` 20 | 21 | ## E0003: `__init__` as a member variable 22 | 23 | ```python 24 | class C: 25 | __init__ = 1 # E0003 26 | ``` 27 | -------------------------------------------------------------------------------- /extension/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that launches the extension inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ] 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: local 3 | hooks: 4 | - id: rustfmt 5 | name: rustfmt 6 | description: Check if all files follow the rustfmt style 7 | entry: cargo fmt --all 8 | language: system 9 | pass_filenames: false 10 | - id: cargo-test 11 | name: Cargo test 12 | entry: cargo test --features large_thread 13 | language: system 14 | pass_filenames: false 15 | - repo: https://github.com/pre-commit/pre-commit-hooks 16 | rev: v4.3.0 17 | hooks: 18 | - id: end-of-file-fixer 19 | - id: trailing-whitespace 20 | - id: check-merge-conflict 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yaml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea for this project 3 | labels: 4 | - enhancement 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: "Describe the feature" 9 | description: "A clear and concise description of what the feature is." 10 | placeholder: "Enter a detailed description of the feature" 11 | validations: 12 | required: true 13 | - type: textarea 14 | attributes: 15 | label: "Additional context" 16 | description: "Add any other context about the feature here." 17 | placeholder: "Enter any additional context" 18 | validations: 19 | required: false 20 | -------------------------------------------------------------------------------- /extension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2020", 5 | "moduleResolution": "node", 6 | "lib": ["ES2020"], 7 | "sourceMap": true, 8 | "esModuleInterop": true, 9 | "rootDir": "src", 10 | "strict": true /* enable all strict type-checking options */ 11 | /* Additional Checks */ 12 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 13 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 14 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 15 | }, 16 | "exclude": ["node_modules", ".vscode-test"] 17 | } 18 | -------------------------------------------------------------------------------- /tests/typevar.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | T = TypeVar("T") 4 | U = TypeVar("U", bound=int) 5 | IS = TypeVar("IS", int, str) 6 | def id(x: T) -> T: 7 | return x 8 | 9 | def id_int(x: U) -> U: 10 | return x 11 | 12 | def id_int_or_str(x: IS) -> IS: 13 | return x 14 | 15 | _ = id(1) + 1 # OK 16 | _ = id("a") + "b" # OK 17 | _ = id_int(1) # OK 18 | _ = id_int("a") # ERR 19 | _ = id_int_or_str(1) # OK 20 | _ = id_int_or_str("a") # OK 21 | _ = id_int_or_str(None) # ERR 22 | 23 | def id2[T](x: T) -> T: 24 | return x 25 | 26 | def id_int2[T: int](x: T) -> T: 27 | return x 28 | 29 | _ = id2(1) + 1 # OK 30 | _ = id2("a") + "b" # OK 31 | _ = id_int2(1) # OK 32 | _ = id_int2("a") # ERR 33 | -------------------------------------------------------------------------------- /extension/biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.8.2/schema.json", 3 | "organizeImports": { 4 | "enabled": true 5 | }, 6 | "formatter": { 7 | "enabled": true, 8 | "indentStyle": "tab", 9 | "lineWidth": 120, 10 | "ignore": ["dist/**", "out/**", ".vscode-test/**", ".vscode/**"] 11 | }, 12 | "linter": { 13 | "enabled": true, 14 | "rules": { 15 | "recommended": true 16 | }, 17 | "ignore": ["dist/**", "out/**", ".vscode-test/**", ".vscode/**"] 18 | }, 19 | "javascript": { 20 | "formatter": { 21 | "indentStyle": "tab", 22 | "lineWidth": 120 23 | } 24 | }, 25 | "json": { 26 | "formatter": { 27 | "indentStyle": "tab", 28 | "lineWidth": 120 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /extension/src/commands.ts: -------------------------------------------------------------------------------- 1 | import { Uri, commands } from "vscode"; 2 | // copied and modified from https://github.com/rust-lang/rust-analyzer/blob/27239fbb58a115915ffc1ce65ededc951eb00fd2/editors/code/src/commands.ts 3 | import type { LanguageClient, Location, Position } from "vscode-languageclient/node"; 4 | 5 | export async function showReferences( 6 | client: LanguageClient | undefined, 7 | uri: string, 8 | position: Position, 9 | locations: Location[], 10 | ) { 11 | if (client) { 12 | await commands.executeCommand( 13 | "editor.action.showReferences", 14 | Uri.parse(uri), 15 | client.protocol2CodeConverter.asPosition(position), 16 | locations.map(client.protocol2CodeConverter.asLocation), 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /extension/src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from "node:path"; 2 | 3 | import { runTests } from "@vscode/test-electron"; 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, "../../"); 10 | 11 | // The path to test runner 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, "./suite/index"); 14 | 15 | // Download VS Code, unzip it and run the integration test 16 | await runTests({ extensionDevelopmentPath, extensionTestsPath }); 17 | } catch (err) { 18 | console.error("Failed to run tests"); 19 | process.exit(1); 20 | } 21 | } 22 | 23 | main(); 24 | -------------------------------------------------------------------------------- /.github/workflows/stale-issues.yml: -------------------------------------------------------------------------------- 1 | name: Close inactive issues 2 | on: 3 | schedule: 4 | - cron: "30 1 * * *" 5 | 6 | jobs: 7 | close-issues: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | issues: write 11 | steps: 12 | - uses: actions/stale@v9 13 | with: 14 | repo-token: ${{ secrets.GITHUB_TOKEN }} 15 | stale-issue-message: "This issue is stale because it has been open for 60 days with no activity." 16 | close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale." 17 | stale-issue-label: "stale" 18 | exempt-issue-labels: "bug" 19 | days-before-issue-stale: 60 20 | days-before-issue-close: 14 21 | days-before-pr-stale: -1 22 | days-before-pr-close: -1 23 | -------------------------------------------------------------------------------- /cargo_publish.ps1: -------------------------------------------------------------------------------- 1 | if ($PWD.Path -like "*\pylyzer") { 2 | if ($null -eq $env:PYPI_PASSWORD) { 3 | echo "set PYPI_PASSWORD environment variable" 4 | exit 5 | } 6 | if ($args[0] -ne "--pip-only") { 7 | cd crates/py2erg 8 | echo "publish py2erg ..." 9 | cargo publish 10 | # from cargo 1.66 timeout is not needed 11 | # timeout 12 12 | cd ../../ 13 | cargo publish 14 | } 15 | maturin build --release 16 | $ver = cat Cargo.toml | rg "^version =" | sed -r 's/^version = "(.*)"/\1/' 17 | $whl = "target/wheels/$(ls target/wheels | Select-Object -ExpandProperty Name | rg "pylyzer-$ver")" 18 | python -m twine upload $whl -u mtshiba -p $env:PYPI_PASSWORD 19 | echo "completed" 20 | } else { 21 | echo "use this command in the project root" 22 | } 23 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: mtshiba # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /crates/py2erg/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "py2erg" 3 | description = "A Python -> Erg converter" 4 | version.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | edition.workspace = true 8 | repository.workspace = true 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [features] 13 | debug = ["erg_compiler/debug", "erg_common/debug"] 14 | japanese = ["erg_compiler/japanese", "erg_common/japanese"] 15 | simplified_chinese = ["erg_compiler/simplified_chinese", "erg_common/simplified_chinese"] 16 | traditional_chinese = ["erg_compiler/traditional_chinese", "erg_common/traditional_chinese"] 17 | 18 | [dependencies] 19 | rustpython-parser = { workspace = true } 20 | rustpython-ast = { workspace = true } 21 | erg_common = { workspace = true } 22 | erg_compiler = { workspace = true } 23 | 24 | [lib] 25 | path = "lib.rs" 26 | -------------------------------------------------------------------------------- /extension/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": ["$ts-webpack-watch", "$tslint-webpack-watch"], 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never", 13 | "group": "watchers" 14 | }, 15 | "group": { 16 | "kind": "build", 17 | "isDefault": true 18 | } 19 | }, 20 | { 21 | "type": "npm", 22 | "script": "watch-tests", 23 | "problemMatcher": "$tsc-watch", 24 | "isBackground": true, 25 | "presentation": { 26 | "reveal": "never", 27 | "group": "watchers" 28 | }, 29 | "group": "build" 30 | }, 31 | { 32 | "label": "tasks: watch-tests", 33 | "dependsOn": ["npm: watch", "npm: watch-tests"], 34 | "problemMatcher": [] 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /tests/test.py: -------------------------------------------------------------------------------- 1 | def add(x, y): 2 | return x + y 3 | 4 | print(add(1, 2)) 5 | print(add(1, "a")) # ERR 6 | add.x = 1 # ERR 7 | 8 | def add2(x: int, y: int) -> str: # ERR 9 | return x + y 10 | 11 | print(add2(1, 2)) 12 | 13 | for i in [1, 2, 3]: 14 | j = i + "aa" # ERR 15 | print(j) 16 | 17 | a: int # OK 18 | a = 1 19 | a: str # ERR 20 | a = "aa" if True else "bb" 21 | a: str # OK 22 | 23 | while "aaa": # ERR 24 | a += 1 # ERR 25 | break 26 | 27 | class C: 28 | x = 1 + "a" # ERR 29 | 30 | dic = {"a": 1, "b": 2} 31 | print(dic["c"]) # ERR 32 | 33 | def f(d1, d2: dict[str, int]): 34 | _ = d1["b"] # OK 35 | _ = d2["a"] # OK 36 | _ = d2[1] # ERR 37 | dic = {"a": 1} 38 | _ = dic["b"] # ERR 39 | 40 | i, j = 1, 2 41 | assert i == 1 42 | assert j == 2 43 | 44 | with open("test.py") as f: 45 | for line in f.readlines(): 46 | print("line: " + line) 47 | 48 | print(x := 1) 49 | print(x) 50 | -------------------------------------------------------------------------------- /crates/pylyzer_core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pylyzer_core" 3 | description = "pylyzer core" 4 | version.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | edition.workspace = true 8 | repository.workspace = true 9 | 10 | [features] 11 | debug = ["erg_compiler/debug", "erg_common/debug", "py2erg/debug"] 12 | large_thread = ["erg_compiler/large_thread", "erg_common/large_thread"] 13 | pretty = ["erg_compiler/pretty", "erg_common/pretty"] 14 | backtrace = ["erg_common/backtrace"] 15 | experimental = ["erg_compiler/experimental", "erg_common/experimental", "parallel"] 16 | parallel = ["erg_compiler/parallel", "erg_common/parallel"] 17 | 18 | [dependencies] 19 | erg_common = { workspace = true } 20 | erg_compiler = { workspace = true } 21 | rustpython-parser = { workspace = true } 22 | rustpython-ast = { workspace = true } 23 | py2erg = { version = "0.0.82", path = "../py2erg" } 24 | 25 | [lib] 26 | path = "lib.rs" 27 | -------------------------------------------------------------------------------- /cargo_publish.sh: -------------------------------------------------------------------------------- 1 | if [[ "$PWD" == */pylyzer ]]; then 2 | if [ "$1" != "--pip-only" ]; then 3 | cd crates/py2erg 4 | echo "publish py2erg ..." 5 | cargo publish 6 | cd ../pylyzer_core 7 | echo "publish pylyzer_core ..." 8 | cargo publish 9 | cd ../../ 10 | cargo publish 11 | if [ "$1" = "--cargo-only" ]; then 12 | exit 0 13 | fi 14 | fi 15 | if [ "$1" != "--cargo-only" ]; then 16 | if [ "$PYPI_PASSWORD" = "" ]; then 17 | echo "set PYPI_PASSWORD" 18 | exit 1 19 | fi 20 | maturin build --release 21 | ver=`cat Cargo.toml | rg "^version =" | sed -r 's/^version = "(.*)"/\1/'` 22 | whl=target/wheels/`ls target/wheels | rg "pylyzer-$ver"` 23 | python3 -m twine upload $whl -u mtshiba -p $PYPI_PASSWORD 24 | fi 25 | echo "completed" 26 | else 27 | echo "use this command in the project root" 28 | fi 29 | -------------------------------------------------------------------------------- /extension/src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from "node:path"; 2 | import glob from "glob"; 3 | import Mocha from "mocha"; 4 | 5 | export function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: "tdd", 9 | color: true, 10 | }); 11 | 12 | const testsRoot = path.resolve(__dirname, ".."); 13 | 14 | return new Promise((c, e) => { 15 | glob("**/**.test.js", { cwd: testsRoot }, (err, files) => { 16 | if (err) { 17 | return e(err); 18 | } 19 | 20 | // Add files to the test suite 21 | for (const f of files) { 22 | mocha.addFile(path.resolve(testsRoot, f)); 23 | } 24 | 25 | try { 26 | // Run the mocha test 27 | mocha.run((failures) => { 28 | if (failures > 0) { 29 | e(new Error(`${failures} tests failed.`)); 30 | } else { 31 | c(); 32 | } 33 | }); 34 | } catch (err) { 35 | console.error(err); 36 | e(err); 37 | } 38 | }); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths-ignore: 7 | - "docs/**" 8 | - "images/**" 9 | - "**.md" 10 | - "**.yml" 11 | - "LICENSE-**" 12 | - ".gitmessage" 13 | - ".pre-commit-config.yaml" 14 | pull_request: 15 | branches: [main] 16 | paths-ignore: 17 | - "docs/**" 18 | - "images/**" 19 | - "**.md" 20 | - "**.yml" 21 | - "LICENSE-**" 22 | - ".pre-commit-config.yaml" 23 | 24 | env: 25 | CARGO_TERM_COLOR: always 26 | 27 | jobs: 28 | build: 29 | 30 | runs-on: ubuntu-latest 31 | 32 | steps: 33 | - uses: actions/checkout@v2 34 | - name: Build 35 | run: | 36 | rustup update stable 37 | cargo install --debug --path . 38 | - name: Run tests 39 | run: cargo test --verbose --features large_thread 40 | - uses: actions-rs/cargo@v1 41 | with: 42 | command: clippy 43 | args: -- -D warnings 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /extension/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /tests/shadowing.py: -------------------------------------------------------------------------------- 1 | from typing import Literal 2 | 3 | i: int = 0 4 | i: str = "a" # OK 5 | 6 | if True: 7 | i = 1 # ERR 8 | else: 9 | i = 2 # ERR 10 | 11 | while False: 12 | i = 3 # ERR 13 | 14 | def f(x: int): 15 | i = 1 # OK 16 | return x + i 17 | 18 | if True: 19 | pass 20 | elif True: 21 | for i in []: pass 22 | pass 23 | elif True: 24 | for i in []: pass 25 | pass 26 | 27 | if True: 28 | pass 29 | elif True: 30 | with open("") as x: 31 | pass 32 | pass 33 | elif True: 34 | with open("") as x: 35 | pass 36 | pass 37 | 38 | if True: 39 | left, right = 1, 2 40 | if True: 41 | left, _ = 1, 2 42 | 43 | def func(label: str) -> str: 44 | if True: 45 | try: 46 | label_bytes = "aaa" 47 | except UnicodeEncodeError: 48 | return label 49 | else: 50 | label_bytes = label 51 | 52 | if True: 53 | label_bytes = label_bytes[1:] 54 | return label_bytes 55 | 56 | if True: 57 | y = 1 58 | else: 59 | y = "a" 60 | y: int | str 61 | y: Literal[1, "a"] # OK 62 | y: Literal[1, "b"] # ERR 63 | -------------------------------------------------------------------------------- /tests/projection.py: -------------------------------------------------------------------------------- 1 | def imaginary(x): 2 | return x.imag 3 | 4 | def imaginary2(x): 5 | return imaginary(x) 6 | 7 | assert imaginary(1) == 0 8 | assert imaginary(1.0) <= 0.0 9 | assert imaginary2(1) == 0 10 | assert imaginary2(1.0) <= 0.0 11 | print(imaginary("a")) # ERR 12 | 13 | class C: 14 | def method(self, x): return x 15 | def call_method(obj, x): 16 | return obj.method(x) 17 | def call_method2(obj, x): 18 | return call_method(obj, x) 19 | 20 | def call_foo(x): 21 | return x.foo("foo") # OK 22 | 23 | c = C() 24 | assert call_method(c, 1) == 1 25 | assert call_method(c, 1) == "a" # ERR 26 | assert call_method2(c, 1) == 1 27 | print(call_method(1, 1)) # ERR 28 | print(call_method(c)) # ERR 29 | 30 | def x_and_y(a): 31 | z: int = a.y 32 | return a.x + z 33 | 34 | class A: 35 | x: int 36 | y: int 37 | 38 | def __init__(self, x, y): 39 | self.x = x 40 | self.y = y 41 | 42 | class B: 43 | x: int 44 | 45 | def __init__(self, x): 46 | self.x = x 47 | 48 | a = A(1, 2) 49 | assert x_and_y(a) == 3 50 | b = B(3) 51 | _ = x_and_y(b) # ERR: B object has no attribute `y` 52 | -------------------------------------------------------------------------------- /tests/collection.py: -------------------------------------------------------------------------------- 1 | i_lis = [0] 2 | 3 | i_lis.append(1) 4 | i_lis.append("a") # ERR 5 | _ = i_lis[0:0] 6 | 7 | union_arr: list[int | str] = [] 8 | union_arr.append(1) 9 | union_arr.append("a") # OK 10 | union_arr.append(None) # ERR 11 | 12 | dic: dict[Literal["a", "b"], int] = {"a": 1} 13 | dic["b"] = 2 14 | _ = dic["a"] 15 | _ = dic["b"] 16 | _ = dic["c"] # ERR 17 | 18 | dic2: dict[str, int] = {"a": 1} 19 | _ = dic2["c"] # OK 20 | 21 | t: tuple[int, str] = (1, "a") 22 | _ = t[0] == 1 # OK 23 | _ = t[1] == 1 # ERR 24 | _ = t[0:1] 25 | 26 | s: set[int] = {1, 2} 27 | s.add(1) 28 | s.add("a") # ERR 29 | 30 | def f(s: Str): return None 31 | for i in getattr(1, "aaa", ()): 32 | f(i) 33 | 34 | assert 1 in [1, 2] 35 | assert 1 in {1, 2} 36 | assert 1 in {1: "a"} 37 | assert 1 in (1, 2) 38 | assert 1 in map(lambda x: x + 1, [0, 1, 2]) 39 | 40 | def func(d: dict, t: tuple, s: set): 41 | _ = d.get("a") 42 | s.add(1) 43 | for i in t: 44 | print(i) 45 | 46 | list_comp = [i + 1 for i in range(10)] 47 | assert list_comp[0] == 1 48 | set_comp = {i + 1 for i in range(10)} 49 | assert 1 in set_comp 50 | dict_comp = {i: i + 1 for i in range(10)} 51 | assert dict_comp[0] == 1 52 | -------------------------------------------------------------------------------- /tests/import.py: -------------------------------------------------------------------------------- 1 | import export 2 | import foo 3 | from . import foo 4 | from foo import bar, Bar 5 | from foo.bar import Baz 6 | from foo import baz 7 | import random 8 | from random import randint as rdi 9 | from datetime import datetime, timedelta 10 | import datetime as dt 11 | from http.client import HTTPResponse 12 | import http 13 | from math import * 14 | 15 | i = random.randint(0, 1) 16 | print(i + 1) 17 | rdi(0, 1, 2) # ERR 18 | 19 | print(export.test) 20 | print(export.add(1, 2)) 21 | assert export.add("a", "b") == 1 # ERR 22 | 23 | c = export.C(1) 24 | assert c.const == 1 25 | assert c.x == 2 26 | assert c.method(2) == 3 27 | 28 | d = export.D(1, 2) 29 | assert d.x == 1 30 | assert d.y == 2 31 | 32 | assert foo.i == 0 33 | assert Bar().f() == 1 34 | assert Bar.CONST == "foo.bar" 35 | assert Baz.CONST == "foo.baz" 36 | 37 | from foo.baz import Bar 38 | 39 | assert Bar.CONST == "foo.baz.bar" 40 | 41 | from glob import glob 42 | print(glob("*")) 43 | glob = None 44 | assert glob == None 45 | 46 | max_date = datetime.max 47 | max_delta = timedelta.max 48 | assert dt.datetime.max == max_date 49 | 50 | Resp = http.client.HTTPResponse 51 | assert export.http.client.HTTPResponse == Resp 52 | 53 | _ = bar.Baz 54 | 55 | _ = sin(acos(exp(0))) # OK 56 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: pylyzer 2 | theme: 3 | name: material 4 | logo: assets/pylyzer-logo.svg 5 | favicon: assets/pylyzer-logo.png 6 | features: 7 | - navigation.instant 8 | - navigation.tracking 9 | - content.code.annotate 10 | - toc.integrate 11 | - toc.follow 12 | - navigation.path 13 | - navigation.top 14 | - content.code.copy 15 | palette: 16 | - media: "(prefers-color-scheme: light)" 17 | scheme: default 18 | primary: indigo 19 | toggle: 20 | icon: material/weather-sunny 21 | name: Switch to dark mode 22 | - media: "(prefers-color-scheme: dark)" 23 | scheme: slate 24 | primary: indigo 25 | toggle: 26 | icon: material/weather-night 27 | name: Switch to light mode 28 | repo_url: https://github.com/mtshiba/pylyzer 29 | repo_name: pylyzer 30 | site_author: Shunsuke Shibayama 31 | site_url: https://mtshiba.github.io/pylyzer/ 32 | markdown_extensions: 33 | - toc: 34 | permalink: "#" 35 | - pymdownx.snippets: 36 | - pymdownx.magiclink: 37 | - attr_list: 38 | - md_in_html: 39 | - pymdownx.highlight: 40 | anchor_linenums: true 41 | - pymdownx.inlinehilite: 42 | - pymdownx.superfences: 43 | - markdown.extensions.attr_list: 44 | - pymdownx.keys: 45 | - pymdownx.tasklist: 46 | custom_checkbox: true 47 | - pymdownx.highlight: 48 | anchor_linenums: true 49 | plugins: 50 | - search -------------------------------------------------------------------------------- /docs/options/options.md: -------------------------------------------------------------------------------- 1 | # command line options 2 | 3 | ## --server 4 | 5 | Launch as a language server. 6 | 7 | ## --clear-cache 8 | 9 | Clear the cache files. 10 | 11 | ## --dump-decl 12 | 13 | Dump a type declarations file (d.er) after type checking. 14 | 15 | ```bash 16 | $ pylyzer --dump-decl test.py 17 | Start checking: test.py 18 | All checks OK: test.py 19 | 20 | $ ls 21 | test.py test.d.er 22 | ``` 23 | 24 | ## -c/--code 25 | 26 | Check code from the command line. 27 | 28 | ```bash 29 | $ pylyzer -c "print('hello world')" 30 | Start checking: string 31 | All checks OK: string 32 | ``` 33 | 34 | ## --disable 35 | 36 | Disable a default LSP feature. 37 | Default (disableable) features are: 38 | 39 | * codeAction 40 | * codeLens 41 | * completion 42 | * diagnostics 43 | * findReferences 44 | * gotoDefinition 45 | * hover 46 | * inlayHint 47 | * rename 48 | * semanticTokens 49 | * signatureHelp 50 | * documentLink 51 | 52 | ## --verbose 53 | 54 | Print process information verbosely. 55 | 56 | ## --no-infer-fn-type 57 | 58 | When a function type is not specified, no type inference is performed and the function type is assumed to be `Any`. 59 | 60 | ## --fast-error-report 61 | 62 | Simplify error reporting by eliminating to search for similar variables when a variable does not exist. 63 | 64 | ## --hurry 65 | 66 | Enable `--no-infer-fn-type` and `--fast-error-report`. 67 | 68 | ## --do-not-show-ext-errors 69 | 70 | Do not show errors from external libraries. 71 | 72 | ## --do-not-respect-pyi 73 | 74 | If specified, the actual `.py` types will be respected over the `.pyi` types. 75 | Applying this option may slow down the analysis. 76 | -------------------------------------------------------------------------------- /extension/webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | const path = require("node:path"); 4 | 5 | //@ts-check 6 | /** @typedef {import('webpack').Configuration} WebpackConfig **/ 7 | 8 | /** @type WebpackConfig */ 9 | const extensionConfig = { 10 | target: "node", // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 11 | mode: "none", // this leaves the source code as close as possible to the original (when packaging we set this to 'production') 12 | 13 | entry: "./src/extension.ts", // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 14 | output: { 15 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 16 | path: path.resolve(__dirname, "dist"), 17 | filename: "extension.js", 18 | libraryTarget: "commonjs2", 19 | }, 20 | externals: { 21 | vscode: "commonjs vscode", // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 22 | // modules added here also need to be added in the .vscodeignore file 23 | }, 24 | resolve: { 25 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 26 | extensions: [".ts", ".js"], 27 | }, 28 | module: { 29 | rules: [ 30 | { 31 | test: /\.ts$/, 32 | exclude: /node_modules/, 33 | use: [ 34 | { 35 | loader: "ts-loader", 36 | }, 37 | ], 38 | }, 39 | ], 40 | }, 41 | devtool: "nosources-source-map", 42 | infrastructureLogging: { 43 | level: "log", // enables logging required for problem matchers 44 | }, 45 | }; 46 | module.exports = [extensionConfig]; 47 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod config; 2 | mod copy; 3 | 4 | use els::Server; 5 | use erg_common::config::ErgMode; 6 | use erg_common::spawn::exec_new_thread; 7 | use erg_common::style::colors::RED; 8 | use erg_common::style::RESET; 9 | use pylyzer_core::{PythonAnalyzer, SimplePythonParser}; 10 | 11 | use crate::config::files_to_be_checked; 12 | use crate::copy::copy_dot_erg; 13 | 14 | fn run() { 15 | copy_dot_erg(); 16 | let cfg = config::parse_args(); 17 | if cfg.mode == ErgMode::LanguageServer { 18 | let lang_server = Server::::new(cfg, None); 19 | lang_server.run(); 20 | } else { 21 | let mut code = 0; 22 | let files = files_to_be_checked(); 23 | if files.is_empty() { 24 | let mut analyzer = PythonAnalyzer::new(cfg); 25 | code = analyzer.run(); 26 | } else { 27 | for path in files { 28 | match path { 29 | Err(invalid_file_or_pattern) => { 30 | if code == 0 { 31 | code = 1; 32 | } 33 | println!("{RED}Invalid file or pattern{RESET}: {invalid_file_or_pattern}"); 34 | } 35 | Ok(path) => { 36 | let cfg = cfg.inherit(path); 37 | let mut analyzer = PythonAnalyzer::new(cfg); 38 | let c = analyzer.run(); 39 | if c != 0 { 40 | code = 1; 41 | } 42 | } 43 | } 44 | } 45 | } 46 | std::process::exit(code); 47 | } 48 | } 49 | 50 | fn main() { 51 | exec_new_thread(run, "pylyzer"); 52 | } 53 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yaml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Create a report to help us improve 3 | labels: 4 | - bug 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: "Describe the bug" 9 | description: "A clear and concise description of what the bug is." 10 | placeholder: "Enter a detailed description of the bug" 11 | validations: 12 | required: true 13 | - type: textarea 14 | attributes: 15 | label: "Reproducible Code" 16 | description: "Provide code or steps needed to reproduce the bug." 17 | placeholder: "Enter code snippet or reproduction steps" 18 | validations: 19 | required: true 20 | - type: textarea 21 | attributes: 22 | label: "Environment" 23 | description: "Provide details such as OS, version, etc." 24 | placeholder: "OS: \nVersion: " 25 | validations: 26 | required: true 27 | - type: textarea 28 | attributes: 29 | label: "Expected behavior" 30 | description: "A clear and concise description of what you expected to happen." 31 | placeholder: "Enter what you expected to happen" 32 | validations: 33 | required: false 34 | - type: textarea 35 | attributes: 36 | label: "Error log" 37 | description: "Add error logs here. Language server errors may be logged in $ERG_PATH/els.log." 38 | validations: 39 | required: false 40 | - type: textarea 41 | attributes: 42 | label: "Screenshots" 43 | description: "Add screenshots to help explain your problem." 44 | placeholder: "Provide screenshot links or instructions to attach images" 45 | validations: 46 | required: false 47 | - type: textarea 48 | attributes: 49 | label: "Additional context" 50 | description: "Add any other context about the problem here." 51 | placeholder: "Enter any additional context" 52 | validations: 53 | required: false 54 | -------------------------------------------------------------------------------- /extension/README.md: -------------------------------------------------------------------------------- 1 | # vscode-pylyzer 2 | 3 | ![pylyzer_logo_with_letters](https://raw.githubusercontent.com/mtshiba/pylyzer/main/images/pylyzer-logo-with-letters.png) 4 | 5 | vsm-version 6 | Build status 7 | Build status 8 | 9 | `pylyzer` is a static code analyzer / language server for Python, written in Rust. 10 | 11 | ## Requirements 12 | 13 | You need to have the [pylyzer](https://github.com/mtshiba/pylyzer) installed on your system. 14 | 15 | To install it, run the following command: 16 | 17 | ```console 18 | pip install pylyzer 19 | ``` 20 | 21 | or 22 | 23 | ```console 24 | cargo install pylyzer 25 | ``` 26 | 27 | ## Commands 28 | 29 | | Command | Description | 30 | | - | - | 31 | | pylyzer.restartLanguageServer | Restart the language server | 32 | 33 | ## Settings 34 | 35 | | Setting | Description | Default | 36 | | - | - | - | 37 | | pylyzer.diagnostics | Enable diagnostics | true | 38 | | pylyzer.inlayHints | Enable inlay hints (this feature is unstable) | false | 39 | | pylyzer.semanticTokens | Enable semantic tokens | false | 40 | | pylyzer.hover | Enable hover | true | 41 | | pylyzer.completion | Enable completion | true | 42 | | pylyzer.smartCompletion | Enable smart completion (see [ELS features](https://github.com/erg-lang/erg/blob/main/crates/els/doc/features.md))| true | 43 | | pylyzer.deepCompletion | Enable deep completion | true | 44 | | pylyzer.signatureHelp | Enable signature help | true | 45 | | pylyzer.documentLink | Enable document link | true | 46 | | pylyzer.codeAction | Enable code action | true | 47 | | pylyzer.codeLens | Enable code lens | true | 48 | -------------------------------------------------------------------------------- /tests/typespec.py: -------------------------------------------------------------------------------- 1 | from typing import Union, Optional, Literal, Callable 2 | from collections.abc import Iterable, Mapping 3 | import collections 4 | 5 | i: Union[int, str] = 1 # OK 6 | j: Union[int, str] = "aa" # OK 7 | k: Union[list[int], str] = 1 # ERR 8 | l: Union[list[int], str] = [1] # OK 9 | o: Optional[int] = None # OK 10 | p: Optional[int] = "a" # ERR 11 | weekdays: Literal[1, 2, 3, 4, 5, 6, 7] = 1 # OK 12 | weekdays: Literal[1, 2, 3, 4, 5, 6, 7] = 8 # ERR 13 | _: tuple[int, ...] = (1, 2, 3) 14 | _: tuple[int, str] = (1, "a", 1) # OK, tuple[T, U, V] <: tuple[T, U] 15 | _: list[tuple[int, ...]] = [(1, 2, 3)] 16 | _: dict[str, dict[str, Union[int, str]]] = {"a": {"b": 1}} 17 | _: dict[str, dict[str, list[int]]] = {"a": {"b": [1]}} 18 | _: dict[str, dict[str, dict[str, int]]] = {"a": {"b": {"c": 1}}} 19 | _: dict[str, dict[str, Optional[int]]] = {"a": {"b": 1}} 20 | _: dict[str, dict[str, Literal[1, 2]]] = {"a": {"b": 1}} 21 | _: dict[str, dict[str, Callable[[int], int]]] = {"a": {"b": abs}} 22 | _: dict[str, dict[str, Callable[[int], None]]] = {"a": {"b": print}} 23 | _: dict[str, dict[str, Opional[int]]] = {"a": {"b": 1}} # ERR 24 | _: dict[str, dict[str, Union[int, str]]] = {"a": {"b": None}} # ERR 25 | _: dict[str, dict[str, list[int]]] = {"a": {"b": ["c"]}} # ERR 26 | _: dict[str, dict[str, Callable[[int], int]]] = {"a": {"b": print}} # ERR 27 | _: dict[str, dict[str, Optional[int]]] = {"a": {"b": "c"}} # ERR 28 | _: dict[str, dict[str, Literal[1, 2]]] = {"a": {"b": 3}} # ERR 29 | _: list[tuple[int, ...]] = [(1, "a", 3)] # ERR 30 | 31 | def f(x: Union[int, str]) -> None: 32 | pass 33 | 34 | f(1) # OK 35 | f(None) # ERR 36 | 37 | def g(x: int) -> int: 38 | return x 39 | 40 | _: Callable[[Union[int, str]], None] = f # OK 41 | _: Callable[[Union[int, str]], None] = g # ERR 42 | 43 | _: Iterable[int] = [1] # OK 44 | _: Iterable[int] = {1} # OK 45 | _: Iterable[int] = (1, 2) # OK 46 | _: Iterable[int] = ["a"] # ERR 47 | 48 | _: Mapping[str, int] = {"a": 1, "c": 2} # OK 49 | _: Mapping[str, int] = {1: "a", 2: "b"} # ERR 50 | 51 | def f(x: Union[int, str, None]): 52 | pass 53 | # OK 54 | f(1) 55 | f("a") 56 | f(None) 57 | 58 | i1 = 1 # type: int 59 | # ERR 60 | i2 = 1 # type: str 61 | i3 = 1 # type: ignore 62 | i3 + "a" # OK 63 | 64 | def f(it: Iterable): 65 | for i in it: 66 | print(i) 67 | 68 | def f2(it: collections.abc.Iterable): 69 | for i in it: 70 | print(i) 71 | 72 | def g(it: Iterable): 73 | for i in it: 74 | print(i + "a") # ERR 75 | -------------------------------------------------------------------------------- /crates/py2erg/error.rs: -------------------------------------------------------------------------------- 1 | use erg_common::error::{ErrorCore, ErrorKind, Location, SubMessage}; 2 | use erg_common::io::Input; 3 | use erg_common::switch_lang; 4 | use erg_compiler::error::CompileError; 5 | 6 | pub(crate) fn reassign_func_error( 7 | input: Input, 8 | loc: Location, 9 | caused_by: String, 10 | name: &str, 11 | ) -> CompileError { 12 | CompileError::new( 13 | ErrorCore::new( 14 | vec![SubMessage::only_loc(loc)], 15 | switch_lang!( 16 | "japanese" => format!("{name}は既に宣言され、参照されています。このような関数に再代入するのは望ましくありません"), 17 | "simplified_chinese" => format!("{name}已声明,已被引用。不建议再次赋值"), 18 | "traditional_chinese" => format!("{name}已宣告,已被引用。不建議再次賦值"), 19 | "english" => format!("{name} has already been declared and referenced. It is not recommended to reassign such a function"), 20 | ), 21 | 1, 22 | ErrorKind::AssignError, 23 | loc, 24 | ), 25 | input, 26 | caused_by, 27 | ) 28 | } 29 | 30 | pub(crate) fn self_not_found_error(input: Input, loc: Location, caused_by: String) -> CompileError { 31 | CompileError::new( 32 | ErrorCore::new( 33 | vec![SubMessage::only_loc(loc)], 34 | switch_lang!( 35 | "japanese" => format!("このメソッドは第一引数にselfを取るべきですが、見つかりませんでした"), 36 | "simplified_chinese" => format!("该方法应该有第一个参数self,但是没有找到"), 37 | "traditional_chinese" => format!("該方法應該有第一個參數self,但是沒有找到"), 38 | "english" => format!("This method should have the first parameter `self`, but it was not found"), 39 | ), 40 | 2, 41 | ErrorKind::NameError, 42 | loc, 43 | ), 44 | input, 45 | caused_by, 46 | ) 47 | } 48 | 49 | pub(crate) fn init_var_error(input: Input, loc: Location, caused_by: String) -> CompileError { 50 | CompileError::new( 51 | ErrorCore::new( 52 | vec![SubMessage::only_loc(loc)], 53 | switch_lang!( 54 | "japanese" => format!("`__init__`はメソッドです。メンバ変数として宣言するべきではありません"), 55 | "simplified_chinese" => format!("__init__是方法。不能宣告为变量"), 56 | "traditional_chinese" => format!("__init__是方法。不能宣告為變量"), 57 | "english" => format!("`__init__` should be a method. It should not be defined as a member variable"), 58 | ), 59 | 3, 60 | ErrorKind::NameError, 61 | loc, 62 | ), 63 | input, 64 | caused_by, 65 | ) 66 | } 67 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths-ignore: 7 | - "docs/**" 8 | - "images/**" 9 | - "**.md" 10 | - "**.yml" 11 | - "LICENSE-**" 12 | - ".gitmessage" 13 | - ".pre-commit-config.yaml" 14 | pull_request: 15 | branches: [main] 16 | paths-ignore: 17 | - "docs/**" 18 | - "images/**" 19 | - "**.md" 20 | - "**.yml" 21 | - "LICENSE-**" 22 | - ".pre-commit-config.yaml" 23 | 24 | env: 25 | CARGO_TERM_COLOR: always 26 | 27 | jobs: 28 | package-test: 29 | strategy: 30 | fail-fast: false 31 | matrix: 32 | os: [windows-latest, ubuntu-latest, macos-latest] 33 | runs-on: ${{ matrix.os }} 34 | steps: 35 | - uses: actions/checkout@v2 36 | - uses: actions/setup-python@v4 37 | with: 38 | python-version: '3.11' 39 | - name: Install 40 | run: | 41 | rustup update stable 42 | cargo install --path . 43 | - name: boto3 44 | continue-on-error: true 45 | run: | 46 | pip3 install boto3 47 | pylyzer -c "import boto3" || true 48 | - name: urllib3 49 | run: | 50 | pip3 install urllib3 51 | pylyzer -c "import urllib3" || true 52 | - name: setuptools 53 | run: | 54 | pip3 install setuptools 55 | pylyzer -c "import setuptools" || true 56 | - name: requests 57 | run: | 58 | pip3 install requests 59 | pylyzer -c "import requests" || true 60 | - name: certifi 61 | run: | 62 | pip3 install certifi 63 | pylyzer -c "import certifi" || true 64 | - name: charset-normalizer 65 | run: | 66 | pip3 install charset-normalizer 67 | pylyzer -c "import charset_normalizer" || true 68 | - name: idna 69 | run: | 70 | pip3 install idna 71 | pylyzer -c "import idna" || true 72 | - name: typing-extensions 73 | run: | 74 | pip3 install typing-extensions 75 | pylyzer -c "import typing_extensions" || true 76 | - name: python-dateutil 77 | run: | 78 | pip3 install python-dateutil 79 | pylyzer -c "import dateutil" || true 80 | - name: packaging 81 | run: | 82 | pip3 install packaging 83 | pylyzer -c "import packaging" || true 84 | - name: six 85 | run: | 86 | pip3 install six 87 | pylyzer -c "import six" || true 88 | - name: s3transfer 89 | run: | 90 | pip3 install s3transfer 91 | pylyzer -c "import s3transfer" || true 92 | - name: pyyaml 93 | run: | 94 | pip3 install pyyaml 95 | pylyzer -c "import yaml" || true 96 | - name: cryptography 97 | run: | 98 | pip3 install cryptography 99 | pylyzer -c "import cryptography" || true 100 | -------------------------------------------------------------------------------- /src/copy.rs: -------------------------------------------------------------------------------- 1 | use std::fs::{copy, create_dir_all, read_dir, remove_file, DirEntry}; 2 | use std::path::Path; 3 | 4 | use erg_common::env::{erg_path, python_site_packages}; 5 | 6 | fn copy_dir(from: impl AsRef, to: impl AsRef) -> std::io::Result<()> { 7 | let from = from.as_ref(); 8 | let to = to.as_ref(); 9 | if !from.exists() { 10 | return Ok(()); 11 | } 12 | if !to.exists() { 13 | create_dir_all(to)?; 14 | } 15 | for entry in read_dir(from)? { 16 | let entry = entry?; 17 | if entry.file_type()?.is_dir() { 18 | copy_dir(entry.path(), to.join(entry.file_name()))?; 19 | } else { 20 | copy(entry.path(), to.join(entry.file_name()))?; 21 | } 22 | } 23 | Ok(()) 24 | } 25 | 26 | pub(crate) fn copy_dot_erg() { 27 | if erg_path().exists() { 28 | return; 29 | } 30 | for site_packages in python_site_packages() { 31 | if site_packages.join(".erg").exists() { 32 | println!("Copying site-package/.erg to {}", erg_path().display()); 33 | copy_dir(site_packages.join(".erg"), erg_path()).expect("Failed to copy .erg"); 34 | } 35 | } 36 | } 37 | 38 | pub(crate) fn clear_cache() { 39 | for dir in read_dir(".").expect("Failed to read dir") { 40 | let Ok(dir) = dir else { 41 | continue; 42 | }; 43 | rec_clear_cache(dir); 44 | } 45 | for site_packages in python_site_packages() { 46 | for pkg in site_packages 47 | .read_dir() 48 | .expect("Failed to read site-packages") 49 | { 50 | let Ok(pkg) = pkg else { 51 | continue; 52 | }; 53 | rec_clear_cache(pkg); 54 | } 55 | } 56 | } 57 | 58 | fn rec_clear_cache(pkg: DirEntry) { 59 | if pkg.file_type().expect("Failed to get file type").is_dir() { 60 | let cache = if pkg.path().ends_with("__pycache__") { 61 | pkg.path() 62 | } else { 63 | pkg.path().join("__pycache__") 64 | }; 65 | if cache.exists() { 66 | let Ok(dir) = cache.read_dir() else { 67 | return; 68 | }; 69 | for cache_file in dir { 70 | let Ok(cache_file) = cache_file else { 71 | continue; 72 | }; 73 | if cache_file.file_name().to_string_lossy().ends_with(".d.er") { 74 | println!("Removing cache file {}", cache_file.path().display()); 75 | remove_file(cache_file.path()).expect("Failed to remove cache file"); 76 | } 77 | } 78 | } 79 | let Ok(dir) = pkg.path().read_dir() else { 80 | return; 81 | }; 82 | for entry in dir { 83 | let Ok(entry) = entry else { 84 | continue; 85 | }; 86 | rec_clear_cache(entry); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # To build the package, run `python -m build --wheel`. 2 | 3 | from pathlib import Path 4 | import os 5 | import shlex 6 | from glob import glob 7 | import shutil 8 | 9 | from setuptools import setup, Command 10 | from setuptools_rust import RustBin 11 | import tomli 12 | 13 | try: 14 | # setuptools >= 70.1.0 15 | from setuptools.command.bdist_wheel import bdist_wheel 16 | except ImportError: 17 | from wheel.bdist_wheel import bdist_wheel 18 | 19 | def removeprefix(string, prefix): 20 | if string.startswith(prefix): 21 | return string[len(prefix):] 22 | else: 23 | return string 24 | 25 | class BdistWheel(bdist_wheel): 26 | def get_tag(self): 27 | _, _, plat = super().get_tag() 28 | return "py3", "none", plat 29 | 30 | class Clean(Command): 31 | user_options = [] 32 | def initialize_options(self): 33 | pass 34 | def finalize_options(self): 35 | pass 36 | def run(self): 37 | # super().run() 38 | for d in ["build", "dist", "src/pylyzer.egg-info"]: 39 | shutil.rmtree(d, ignore_errors=True) 40 | 41 | with open("README.md", encoding="utf-8", errors="ignore") as fp: 42 | long_description = fp.read() 43 | 44 | with open("Cargo.toml", "rb") as fp: 45 | toml = tomli.load(fp) 46 | name = toml["package"]["name"] 47 | description = toml["package"]["description"] 48 | version = toml["workspace"]["package"]["version"] 49 | license = toml["workspace"]["package"]["license"] 50 | url = toml["workspace"]["package"]["repository"] 51 | 52 | cargo_args = ["--no-default-features"] 53 | 54 | home = os.path.expanduser("~") 55 | file_and_dirs = glob(home + "/" + ".erg/lib/**", recursive=True) 56 | paths = [Path(path) for path in file_and_dirs if os.path.isfile(path)] 57 | files = [(removeprefix(str(path.parent), home), str(path)) for path in paths] 58 | data_files = {} 59 | for key, value in files: 60 | if key in data_files: 61 | data_files[key].append(value) 62 | else: 63 | data_files[key] = [value] 64 | data_files = list(data_files.items()) 65 | 66 | setup( 67 | name=name, 68 | author="mtshiba", 69 | author_email="sbym1346@gmail.com", 70 | url=url, 71 | description=description, 72 | long_description=long_description, 73 | long_description_content_type="text/markdown", 74 | version=version, 75 | license=license, 76 | python_requires=">=3.7", 77 | rust_extensions=[ 78 | RustBin("pylyzer", args=cargo_args, cargo_manifest_args=["--locked"]) 79 | ], 80 | cmdclass={ 81 | "clean": Clean, 82 | "bdist_wheel": BdistWheel, 83 | }, 84 | classifiers=[ 85 | "Development Status :: 2 - Pre-Alpha", 86 | "Operating System :: OS Independent", 87 | "Programming Language :: Python", 88 | "Programming Language :: Rust", 89 | "Topic :: Software Development :: Quality Assurance", 90 | ], 91 | data_files=data_files, 92 | ) 93 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pylyzer" 3 | description = "A static code analyzer & language server for Python" 4 | version.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | edition.workspace = true 8 | repository.workspace = true 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [workspace] 13 | members = [ 14 | "crates/py2erg", 15 | "crates/pylyzer_core", 16 | "crates/pylyzer_wasm", 17 | ] 18 | 19 | [workspace.package] 20 | version = "0.0.82" 21 | authors = ["Shunsuke Shibayama "] 22 | license = "MIT OR Apache-2.0" 23 | edition = "2021" 24 | repository = "https://github.com/mtshiba/pylyzer" 25 | 26 | [workspace.dependencies] 27 | erg_common = { version = "0.6.53-nightly.5", features = ["py_compat", "els"] } 28 | erg_compiler = { version = "0.6.53-nightly.5", features = ["py_compat", "els"] } 29 | els = { version = "0.1.65-nightly.5", features = ["py_compat"] } 30 | # rustpython-parser = { version = "0.3.0", features = ["all-nodes-with-ranges", "location"] } 31 | # rustpython-ast = { version = "0.3.0", features = ["all-nodes-with-ranges", "location"] } 32 | rustpython-parser = { git = "https://github.com/RustPython/Parser", version = "0.4.0", features = ["all-nodes-with-ranges", "location"] } 33 | rustpython-ast = { git = "https://github.com/RustPython/Parser", version = "0.4.0", features = ["all-nodes-with-ranges", "location"] } 34 | # erg_compiler = { git = "https://github.com/erg-lang/erg", branch = "main", features = ["py_compat", "els"] } 35 | # erg_common = { git = "https://github.com/erg-lang/erg", branch = "main", features = ["py_compat", "els"] } 36 | # els = { git = "https://github.com/erg-lang/erg", branch = "main", features = ["py_compat"] } 37 | # erg_compiler = { path = "../erg/crates/erg_compiler", features = ["py_compat", "els"] } 38 | # erg_common = { path = "../erg/crates/erg_common", features = ["py_compat", "els"] } 39 | # els = { path = "../erg/crates/els", features = ["py_compat"] } 40 | 41 | [features] 42 | debug = ["erg_common/debug", "pylyzer_core/debug"] 43 | large_thread = ["erg_common/large_thread", "els/large_thread", "pylyzer_core/large_thread"] 44 | pretty = ["erg_common/pretty", "pylyzer_core/pretty"] 45 | backtrace = ["erg_common/backtrace", "els/backtrace", "pylyzer_core/backtrace"] 46 | experimental = ["erg_common/experimental", "els/experimental", "pylyzer_core/experimental", "parallel"] 47 | parallel = ["erg_common/parallel", "pylyzer_core/parallel"] 48 | japanese = ["erg_common/japanese", "els/japanese"] 49 | simplified_chinese = ["erg_common/simplified_chinese", "els/simplified_chinese"] 50 | traditional_chinese = ["erg_common/traditional_chinese", "els/traditional_chinese"] 51 | 52 | [dependencies] 53 | pylyzer_core = { version = "0.0.82", path = "./crates/pylyzer_core" } 54 | erg_common = { workspace = true } 55 | els = { workspace = true } 56 | glob = "0.3.2" 57 | indexmap = "2.7.1" 58 | 59 | [dev-dependencies] 60 | erg_compiler = { workspace = true } 61 | 62 | [profile.opt-with-dbg] 63 | inherits = "release" 64 | debug = true 65 | -------------------------------------------------------------------------------- /tests/class.py: -------------------------------------------------------------------------------- 1 | from typing import Self, List 2 | 3 | class Empty: pass 4 | emp = Empty() 5 | 6 | class x(): pass 7 | y = x() 8 | # multiple class definitions are allowed 9 | class x(): pass 10 | y = x() 11 | 12 | class C: 13 | def __init__(self, x: int, y): # y: Obj 14 | self.x = x 15 | self.y = y # y: Never 16 | def __add__(self, other: C): 17 | return C(self.x + other.x, self.y + other.y) 18 | def method(self): 19 | return self.x 20 | def id(self) -> Self: 21 | return self 22 | def id2(self) -> "C": 23 | return self 24 | 25 | c = C(1, 2) 26 | assert c.x == 1 27 | # OK, c.y == "a" is also OK (cause the checker doesn't know the type of C.y) 28 | assert c.y == 2 29 | assert c.z == 3 # ERR 30 | d = c + c 31 | assert d.x == 2 32 | assert d.x == "a" # ERR 33 | a = c.method() # OK 34 | _: int = a + 1 35 | b = C("a").method() # ERR 36 | assert c.id() == c 37 | 38 | class D: 39 | c: int 40 | def __add__(self, other: D): 41 | return D(self.c + other.c) 42 | def __sub__(self, other: C): 43 | return D(self.c - other.x) 44 | def __neg__(self): 45 | return D(-self.c) 46 | def __gt__(self, other: D): 47 | return self.c > other.c 48 | def __init__(self, c): 49 | self.c = c 50 | 51 | class E(D): 52 | def __add__(self, other: E): 53 | return E(self.c + other.c) 54 | def invalid(self): 55 | return self.d # ERR: E object has no attribute `d` 56 | 57 | c1 = D(1).c + 1 58 | d = D(1) + D(2) 59 | err = C(1, 2) + D(1) # ERR 60 | ok = D(1) - C(1, 2) # OK 61 | assert D(1) > D(0) 62 | c = -d # OK 63 | e = E(1) 64 | 65 | class F: 66 | def __init__(self, x: int, y: int = 1, z: int = 2): 67 | self.x = x 68 | self.y = y 69 | self.z = z 70 | 71 | _ = F(1) 72 | _ = F(1, 2) 73 | _ = F(1, z=1, y=2) 74 | 75 | class G(DoesNotExist): # ERR 76 | def foo(self): 77 | return 1 78 | 79 | g = G() 80 | assert g.foo() == 1 81 | 82 | class Value: 83 | value: object 84 | 85 | class H(Value): 86 | value: int 87 | 88 | def __init__(self, value): 89 | self.value = value 90 | 91 | def incremented(self): 92 | return H(self.value + 1) 93 | 94 | class MyList(list): 95 | @staticmethod 96 | def try_new(lis) -> "MyList" | None: 97 | if isinstance(lis, list): 98 | return MyList(lis) 99 | else: 100 | return None 101 | 102 | class Implicit: 103 | def __init__(self): 104 | self.foo = False 105 | 106 | def set_foo(self): 107 | self.foo = True 108 | 109 | class Cs: 110 | cs: list[C] 111 | cs2: List[C] 112 | cs_list: list[list[C]] 113 | 114 | def __init__(self, cs: list[C]): 115 | self.cs = cs 116 | self.cs2 = cs 117 | self.cs_list = [] 118 | 119 | def add(self, c: C): 120 | self.cs.append(c) 121 | self.cs2.append(c) 122 | self.cs_list.append([c]) 123 | 124 | class I: 125 | def __init__(self): 126 | self.ix: int = 1 127 | if True: 128 | self.init_y() 129 | 130 | def init_y(self): 131 | self.iy: int = 2 132 | 133 | def foo(self): 134 | self.iz: int = 1 # ERR 135 | 136 | i = I() 137 | _ = i.ix 138 | _ = i.iy # OK 139 | _ = i.iz # ERR 140 | -------------------------------------------------------------------------------- /crates/pylyzer_core/handle_err.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use erg_common::error::ErrorKind; 4 | use erg_common::log; 5 | use erg_common::style::{remove_style, StyledStr}; 6 | // use erg_common::style::{remove_style, StyledString, Color}; 7 | use erg_compiler::context::ModuleContext; 8 | use erg_compiler::error::{CompileError, CompileErrors}; 9 | 10 | fn project_root(path: &Path) -> Option { 11 | let mut parent = path.to_path_buf(); 12 | while parent.pop() { 13 | if parent.join("pyproject.toml").exists() || parent.join(".git").exists() { 14 | let path = if parent == Path::new("") { 15 | PathBuf::from(".") 16 | } else { 17 | parent 18 | }; 19 | return path.canonicalize().ok(); 20 | } 21 | } 22 | None 23 | } 24 | 25 | pub(crate) fn filter_errors(ctx: &ModuleContext, errors: CompileErrors) -> CompileErrors { 26 | let root = project_root(ctx.get_top_cfg().input.path()); 27 | errors 28 | .into_iter() 29 | .filter_map(|error| filter_error(root.as_deref(), ctx, error)) 30 | .collect() 31 | } 32 | 33 | fn handle_name_error(error: CompileError) -> Option { 34 | let main = &error.core.main_message; 35 | if main.contains("is already declared") 36 | || main.contains("cannot be assigned more than once") 37 | || { 38 | main.contains(" is not defined") && { 39 | let name = StyledStr::destyle(main.trim_end_matches(" is not defined")); 40 | name == "Any" 41 | || error 42 | .core 43 | .get_hint() 44 | .is_some_and(|hint| hint.contains(name)) 45 | } 46 | } 47 | { 48 | None 49 | } else { 50 | Some(error) 51 | } 52 | } 53 | 54 | fn filter_error( 55 | root: Option<&Path>, 56 | ctx: &ModuleContext, 57 | mut error: CompileError, 58 | ) -> Option { 59 | if ctx.get_top_cfg().do_not_show_ext_errors 60 | && error.input.path() != Path::new("") 61 | && root.is_some_and(|root| { 62 | error 63 | .input 64 | .path() 65 | .canonicalize() 66 | .is_ok_and(|path| path.starts_with(root.join(".venv")) || !path.starts_with(root)) 67 | }) 68 | { 69 | return None; 70 | } 71 | match error.core.kind { 72 | ErrorKind::FeatureError => { 73 | log!(err "this error is ignored:"); 74 | log!(err "{error}"); 75 | None 76 | } 77 | ErrorKind::InheritanceError => None, 78 | ErrorKind::VisibilityError => None, 79 | // exclude doc strings 80 | ErrorKind::UnusedWarning => { 81 | let code = error.input.reread_lines( 82 | error.core.loc.ln_begin().unwrap_or(1) as usize, 83 | error.core.loc.ln_end().unwrap_or(1) as usize, 84 | ); 85 | if code[0].trim().starts_with("\"\"\"") { 86 | None 87 | } else { 88 | for sub in error.core.sub_messages.iter_mut() { 89 | if let Some(hint) = &mut sub.hint { 90 | *hint = remove_style(hint); 91 | *hint = hint.replace("use discard function", "bind to `_` (`_ = ...`)"); 92 | } 93 | } 94 | Some(error) 95 | } 96 | } 97 | ErrorKind::NameError | ErrorKind::AssignError => handle_name_error(error), 98 | _ => Some(error), 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /images/pylyzer-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 39 | 44 | 49 | 54 | 59 | 64 | 65 | 67 | 72 | 78 | 83 | 89 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /docs/assets/pylyzer-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 39 | 44 | 49 | 54 | 59 | 64 | 65 | 67 | 72 | 78 | 83 | 89 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pylyzer", 3 | "displayName": "pylyzer", 4 | "description": "A fast Python static code analyzer & language server for VSCode", 5 | "publisher": "pylyzer", 6 | "version": "0.1.11", 7 | "engines": { 8 | "vscode": "^1.70.0" 9 | }, 10 | "categories": ["Programming Languages", "Linters"], 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/mtshiba/pylyzer.git" 14 | }, 15 | "icon": "images/pylyzer-logo.png", 16 | "main": "./dist/extension.js", 17 | "activationEvents": [ 18 | "workspaceContains:pyproject.toml", 19 | "workspaceContains:*/pyproject.toml", 20 | "onLanguage:python" 21 | ], 22 | "contributes": { 23 | "commands": [ 24 | { 25 | "title": "Restart the pylyzer language server", 26 | "category": "python", 27 | "command": "pylyzer.restartLanguageServer" 28 | } 29 | ], 30 | "languages": [ 31 | { 32 | "id": "python", 33 | "aliases": ["Python", "python"], 34 | "extensions": [".py", ".pyi"] 35 | } 36 | ], 37 | "configuration": { 38 | "type": "object", 39 | "title": "pylyzer", 40 | "properties": { 41 | "pylyzer.diagnostics": { 42 | "type": "boolean", 43 | "default": true, 44 | "markdownDescription": "Enable diagnostics" 45 | }, 46 | "pylyzer.inlayHints": { 47 | "type": "boolean", 48 | "default": false, 49 | "markdownDescription": "Enable inlay hints (this feature is unstable)" 50 | }, 51 | "pylyzer.semanticTokens": { 52 | "type": "boolean", 53 | "default": false, 54 | "markdownDescription": "Enable semantic tokens (this feature is unstable)" 55 | }, 56 | "pylyzer.hover": { 57 | "type": "boolean", 58 | "default": true, 59 | "markdownDescription": "Enable hover" 60 | }, 61 | "pylyzer.completion": { 62 | "type": "boolean", 63 | "default": true, 64 | "markdownDescription": "Enable completion" 65 | }, 66 | "pylyzer.smartCompletion": { 67 | "type": "boolean", 68 | "default": true, 69 | "markdownDescription": "Enable smart completion (see [ELS features](https://github.com/erg-lang/erg/blob/main/crates/els/doc/features.md))" 70 | }, 71 | "pylyzer.deepCompletion": { 72 | "type": "boolean", 73 | "default": true, 74 | "markdownDescription": "Enable deep completion (see [ELS features](https://github.com/erg-lang/erg/blob/main/crates/els/doc/features.md))" 75 | }, 76 | "pylyzer.signatureHelp": { 77 | "type": "boolean", 78 | "default": true, 79 | "markdownDescription": "Enable signature help" 80 | }, 81 | "pylyzer.documentLink": { 82 | "type": "boolean", 83 | "default": true, 84 | "markdownDescription": "Enable document link" 85 | }, 86 | "pylyzer.codeAction": { 87 | "type": "boolean", 88 | "default": true, 89 | "markdownDescription": "Enable code action" 90 | }, 91 | "pylyzer.codeLens": { 92 | "type": "boolean", 93 | "default": true, 94 | "markdownDescription": "Enable code lens" 95 | } 96 | } 97 | } 98 | }, 99 | "scripts": { 100 | "vscode:publish": "vsce publish", 101 | "vscode:prepublish": "npm run package", 102 | "vscode:package": "vsce package", 103 | "compile": "webpack", 104 | "watch": "webpack --watch", 105 | "package": "webpack --mode production --devtool hidden-source-map", 106 | "compile-tests": "tsc -p . --outDir out", 107 | "watch-tests": "tsc -p . -w --outDir out", 108 | "pretest": "npm run compile-tests && npm run compile && npm run lint", 109 | "test": "node ./out/test/runTest.js", 110 | "type-check": "tsc --noEmit", 111 | "lint": "biome lint .", 112 | "format": "biome format .", 113 | "lint:fix-suggested": "biome check --write .", 114 | "format:fix": "biome format --write ." 115 | }, 116 | "dependencies": { 117 | "vscode-languageclient": "^8.0.2" 118 | }, 119 | "devDependencies": { 120 | "@biomejs/biome": "^1.8.2", 121 | "@types/glob": "^8.0.0", 122 | "@types/mocha": "^10.0.1", 123 | "@types/node": "18.x", 124 | "@types/vscode": "^1.70.0", 125 | "@vscode/test-electron": "^2.2.1", 126 | "glob": "^8.0.3", 127 | "mocha": "^10.2.0", 128 | "ts-loader": "^9.4.2", 129 | "typescript": "^4.9.4", 130 | "webpack": "^5.75.0", 131 | "webpack-cli": "^5.0.1" 132 | }, 133 | "lint-staged": { 134 | "*": "biome format --write" 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | tags: 9 | - v[0-9]+.* 10 | 11 | jobs: 12 | create-release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: taiki-e/create-gh-release-action@v1 17 | with: 18 | token: ${{ secrets.GITHUB_TOKEN }} 19 | cargo: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: cargo-publish 24 | run: | 25 | rustup update stable 26 | cargo login ${{ secrets.CARGO_TOKEN }} 27 | chmod +x cargo_publish.sh 28 | ./cargo_publish.sh --cargo-only 29 | make-pypi-artifacts: 30 | strategy: 31 | matrix: 32 | include: 33 | - target: x86_64-unknown-linux-gnu 34 | platform: linux 35 | os: ubuntu-latest 36 | - target: x86_64-apple-darwin 37 | platform: macos 38 | os: macos-latest 39 | - target: x86_64-pc-windows-msvc 40 | platform: windows 41 | os: windows-latest 42 | runs-on: ${{ matrix.os }} 43 | steps: 44 | - uses: actions/checkout@v3 45 | - uses: actions/setup-python@v4 46 | with: 47 | python-version: "3.11" 48 | - name: setup-tools 49 | run: | 50 | rustup update stable 51 | pip3 install twine build cibuildwheel setuptools-rust tomli 52 | rustup target add ${{ matrix.target }} 53 | - name: build 54 | run: cibuildwheel --output-dir dist --platform ${{ matrix.platform }} 55 | env: 56 | # rust doesn't seem to be available for musl linux on i686 57 | CIBW_SKIP: '*-musllinux_i686' 58 | CIBW_ENVIRONMENT: 'PATH="$HOME/.cargo/bin:$PATH" CARGO_TERM_COLOR="always"' 59 | CIBW_ENVIRONMENT_WINDOWS: 'PATH="$UserProfile\.cargo\bin;$PATH"' 60 | CIBW_BEFORE_BUILD: rustup show 61 | CIBW_BEFORE_BUILD_LINUX: > 62 | curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain=stable --profile=minimal -y && 63 | rustup show 64 | # CIBW_BUILD_VERBOSITY: 1 65 | # - name: upload 66 | # run: | 67 | # twine upload -u mtshiba -p ${{ secrets.PYPI_PASSWORD }} --skip-existing dist/* 68 | # cargo build --release --target ${{ matrix.target }} 69 | # python3 -m build --wheel 70 | # maturin publish -u mtshiba -p ${{ secrets.PYPI_PASSWORD }} --target ${{ matrix.target }} --skip-existing 71 | - name: upload artifacts 72 | uses: actions/upload-artifact@v4 73 | with: 74 | name: dist-${{ matrix.platform }} 75 | path: dist 76 | publish-pypi-artifacts: 77 | needs: make-pypi-artifacts 78 | runs-on: ubuntu-latest 79 | permissions: 80 | # For pypi trusted publishing 81 | id-token: write 82 | steps: 83 | - name: download-artifacts 84 | uses: actions/download-artifact@v4 85 | with: 86 | pattern: dist-* 87 | path: dist 88 | merge-multiple: true 89 | - name: Publish to PyPi 90 | uses: pypa/gh-action-pypi-publish@release/v1 91 | with: 92 | skip-existing: true 93 | verbose: true 94 | upload-assets: 95 | needs: create-release 96 | strategy: 97 | matrix: 98 | include: 99 | - target: armv7-unknown-linux-gnueabihf 100 | os: ubuntu-latest 101 | - target: aarch64-unknown-linux-gnu 102 | os: ubuntu-latest 103 | - target: aarch64-apple-darwin 104 | os: macos-latest 105 | - target: x86_64-unknown-linux-gnu 106 | os: ubuntu-latest 107 | - target: x86_64-apple-darwin 108 | os: macos-latest 109 | - target: x86_64-pc-windows-msvc 110 | os: windows-latest 111 | runs-on: ${{ matrix.os }} 112 | steps: 113 | - uses: actions/checkout@v3 114 | - name: update-rustup 115 | run: rustup update stable 116 | - uses: taiki-e/upload-rust-binary-action@v1 117 | with: 118 | bin: pylyzer 119 | target: ${{ matrix.target }} 120 | env: 121 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 122 | # static linking to libc 123 | RUSTFLAGS: ${{ (matrix.target == 'x86_64-unknown-linux-gnu' && '-C target-feature=+crt-static') || '' }} 124 | -------------------------------------------------------------------------------- /extension/src/extension.ts: -------------------------------------------------------------------------------- 1 | import { type ExtensionContext, commands, window, workspace } from "vscode"; 2 | import { LanguageClient, type LanguageClientOptions, type ServerOptions } from "vscode-languageclient/node"; 3 | import fs from "node:fs"; 4 | import { showReferences } from "./commands"; 5 | 6 | let client: LanguageClient | undefined; 7 | 8 | async function startLanguageClient(context: ExtensionContext) { 9 | try { 10 | const executablePath = (() => { 11 | const fp = workspace.workspaceFolders?.at(0)?.uri.fsPath; 12 | const venvExecutablePath = `${fp}/.venv/bin/pylyzer`; 13 | if (fs.existsSync(venvExecutablePath)) { 14 | return venvExecutablePath; 15 | } 16 | const executablePath = workspace.getConfiguration("pylyzer").get("executablePath", ""); 17 | return executablePath === "" ? "pylyzer" : executablePath; 18 | })(); 19 | const enableDiagnostics = workspace.getConfiguration("pylyzer").get("diagnostics", true); 20 | const enableInlayHints = workspace.getConfiguration("pylyzer").get("inlayHints", false); 21 | const enableSemanticTokens = workspace.getConfiguration("pylyzer").get("semanticTokens", false); 22 | const enableHover = workspace.getConfiguration("pylyzer").get("hover", true); 23 | const enableCompletion = workspace.getConfiguration("pylyzer").get("completion", true); 24 | const smartCompletion = workspace.getConfiguration("pylyzer").get("smartCompletion", true); 25 | const deepCompletion = workspace.getConfiguration("pylyzer").get("deepCompletion", true); 26 | const enableSignatureHelp = workspace.getConfiguration("pylyzer").get("signatureHelp", true); 27 | const enableDocumentLink = workspace.getConfiguration("pylyzer").get("documentLink", true); 28 | const enableCodeAction = workspace.getConfiguration("pylyzer").get("codeAction", true); 29 | const enableCodeLens = workspace.getConfiguration("pylyzer").get("codeLens", true); 30 | /* optional features */ 31 | const checkOnType = workspace.getConfiguration("pylyzer").get("checkOnType", false); 32 | const args = ["--server"]; 33 | args.push("--"); 34 | if (!enableDiagnostics) { 35 | args.push("--disable"); 36 | args.push("diagnostic"); 37 | } 38 | if (!enableInlayHints) { 39 | args.push("--disable"); 40 | args.push("inlayHints"); 41 | } 42 | if (!enableSemanticTokens) { 43 | args.push("--disable"); 44 | args.push("semanticTokens"); 45 | } 46 | if (!enableHover) { 47 | args.push("--disable"); 48 | args.push("hover"); 49 | } 50 | if (!enableCompletion) { 51 | args.push("--disable"); 52 | args.push("completion"); 53 | } 54 | if (!smartCompletion) { 55 | args.push("--disable"); 56 | args.push("smartCompletion"); 57 | } 58 | if (!deepCompletion) { 59 | args.push("--disable"); 60 | args.push("deepCompletion"); 61 | } 62 | if (!enableSignatureHelp) { 63 | args.push("--disable"); 64 | args.push("signatureHelp"); 65 | } 66 | if (!enableDocumentLink) { 67 | args.push("--disable"); 68 | args.push("documentLink"); 69 | } 70 | if (!enableCodeAction) { 71 | args.push("--disable"); 72 | args.push("codeAction"); 73 | } 74 | if (!enableCodeLens) { 75 | args.push("--disable"); 76 | args.push("codeLens"); 77 | } 78 | if (checkOnType) { 79 | args.push("--enable"); 80 | args.push("checkOnType"); 81 | } 82 | const serverOptions: ServerOptions = { 83 | command: executablePath, 84 | args, 85 | }; 86 | const clientOptions: LanguageClientOptions = { 87 | documentSelector: [ 88 | { 89 | scheme: "file", 90 | language: "python", 91 | }, 92 | ], 93 | }; 94 | client = new LanguageClient("pylyzer", serverOptions, clientOptions); 95 | await client.start(); 96 | } catch (e) { 97 | window.showErrorMessage( 98 | "Failed to start the pylyzer language server. Please make sure you have pylyzer installed.", 99 | ); 100 | window.showErrorMessage(`Error: ${e}`); 101 | } 102 | } 103 | 104 | async function restartLanguageClient() { 105 | try { 106 | if (client === undefined) { 107 | throw new Error(); 108 | } 109 | await client.restart(); 110 | } catch (e) { 111 | window.showErrorMessage("Failed to restart the pylyzer language server."); 112 | window.showErrorMessage(`Error: ${e}`); 113 | } 114 | } 115 | 116 | export async function activate(context: ExtensionContext) { 117 | context.subscriptions.push(commands.registerCommand("pylyzer.restartLanguageServer", () => restartLanguageClient())); 118 | context.subscriptions.push( 119 | commands.registerCommand("pylyzer.showReferences", async (uri, position, locations) => { 120 | await showReferences(client, uri, position, locations); 121 | }), 122 | ); 123 | await startLanguageClient(context); 124 | } 125 | 126 | export function deactivate() { 127 | if (client) { 128 | return client.stop(); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /tests/test.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use erg_common::config::ErgConfig; 4 | use erg_common::io::Input; 5 | use erg_common::spawn::exec_new_thread; 6 | use erg_compiler::artifact::{CompleteArtifact, IncompleteArtifact}; 7 | use pylyzer_core::PythonAnalyzer; 8 | 9 | #[allow(clippy::result_large_err)] 10 | pub fn exec_analyzer(file_path: &'static str) -> Result { 11 | let cfg = ErgConfig { 12 | input: Input::file(PathBuf::from(file_path)), 13 | effect_check: false, 14 | ownership_check: false, 15 | ..Default::default() 16 | }; 17 | let mut analyzer = PythonAnalyzer::new(cfg); 18 | let py_code = analyzer.cfg.input.read(); 19 | analyzer.analyze(py_code, "exec") 20 | } 21 | 22 | fn _expect(file_path: &'static str, warns: usize, errors: usize) -> Result<(), String> { 23 | println!("Testing {file_path} ..."); 24 | match exec_analyzer(file_path) { 25 | Ok(artifact) => { 26 | if artifact.warns.len() != warns { 27 | eprintln!("warns: {}", artifact.warns); 28 | return Err(format!( 29 | "Expected {warns} warnings, found {}", 30 | artifact.warns.len() 31 | )); 32 | } 33 | if errors != 0 { 34 | return Err(format!("Expected {errors} errors, found 0")); 35 | } 36 | Ok(()) 37 | } 38 | Err(artifact) => { 39 | if artifact.warns.len() != warns { 40 | eprintln!("warns: {}", artifact.warns); 41 | return Err(format!( 42 | "Expected {warns} warnings, found {}", 43 | artifact.warns.len() 44 | )); 45 | } 46 | if artifact.errors.len() != errors { 47 | eprintln!("errors: {}", artifact.errors); 48 | return Err(format!( 49 | "Expected {errors} errors, found {}", 50 | artifact.errors.len() 51 | )); 52 | } 53 | Ok(()) 54 | } 55 | } 56 | } 57 | 58 | pub fn expect(file_path: &'static str, warns: usize, errors: usize) -> Result<(), String> { 59 | exec_new_thread(move || _expect(file_path, warns, errors), file_path) 60 | } 61 | 62 | #[test] 63 | fn exec_abc() -> Result<(), String> { 64 | expect("tests/abc.py", 0, 0) 65 | } 66 | 67 | #[test] 68 | fn exec_test() -> Result<(), String> { 69 | expect("tests/test.py", 0, 11) 70 | } 71 | 72 | #[test] 73 | fn exec_import() -> Result<(), String> { 74 | if Path::new("tests/__pycache__").exists() { 75 | std::fs::remove_dir_all("tests/__pycache__").unwrap(); 76 | } 77 | if Path::new("tests/foo/__pycache__").exists() { 78 | std::fs::remove_dir_all("tests/foo/__pycache__").unwrap(); 79 | } 80 | if Path::new("tests/bar/__pycache__").exists() { 81 | std::fs::remove_dir_all("tests/bar/__pycache__").unwrap(); 82 | } 83 | expect("tests/import.py", 1, 2) 84 | } 85 | 86 | #[test] 87 | fn exec_dict() -> Result<(), String> { 88 | expect("tests/dict.py", 0, 2) 89 | } 90 | 91 | #[test] 92 | fn exec_export() -> Result<(), String> { 93 | expect("tests/export.py", 0, 0) 94 | } 95 | 96 | #[test] 97 | fn exec_func() -> Result<(), String> { 98 | expect("tests/func.py", 0, 1) 99 | } 100 | 101 | #[test] 102 | fn exec_class() -> Result<(), String> { 103 | expect("tests/class.py", 0, 8) 104 | } 105 | 106 | #[test] 107 | fn exec_class_err() -> Result<(), String> { 108 | expect("tests/err/class.py", 0, 3) 109 | } 110 | 111 | #[test] 112 | fn exec_errors() -> Result<(), String> { 113 | expect("tests/errors.py", 0, 3) 114 | } 115 | 116 | #[test] 117 | fn exec_warns() -> Result<(), String> { 118 | expect("tests/warns.py", 2, 0) 119 | } 120 | 121 | #[test] 122 | fn exec_typespec() -> Result<(), String> { 123 | expect("tests/typespec.py", 0, 16) 124 | } 125 | 126 | #[test] 127 | fn exec_projection() -> Result<(), String> { 128 | expect("tests/projection.py", 0, 5) 129 | } 130 | 131 | #[test] 132 | fn exec_property() -> Result<(), String> { 133 | expect("tests/property.py", 0, 0) 134 | } 135 | 136 | #[test] 137 | fn exec_property_err() -> Result<(), String> { 138 | expect("tests/err/property.py", 0, 1) 139 | } 140 | 141 | #[test] 142 | fn exec_pyi() -> Result<(), String> { 143 | expect("tests/pyi.py", 0, 5) 144 | } 145 | 146 | #[test] 147 | fn exec_list() -> Result<(), String> { 148 | expect("tests/list.py", 0, 2) 149 | } 150 | 151 | #[test] 152 | fn exec_literal() -> Result<(), String> { 153 | expect("tests/literal.py", 0, 2) 154 | } 155 | 156 | #[test] 157 | fn exec_narrowing() -> Result<(), String> { 158 | expect("tests/narrowing.py", 0, 1) 159 | } 160 | 161 | #[test] 162 | fn exec_casting() -> Result<(), String> { 163 | expect("tests/casting.py", 4, 1) 164 | } 165 | 166 | #[test] 167 | fn exec_collection() -> Result<(), String> { 168 | expect("tests/collection.py", 0, 5) 169 | } 170 | 171 | #[test] 172 | fn exec_call() -> Result<(), String> { 173 | expect("tests/call.py", 0, 6) 174 | } 175 | 176 | #[test] 177 | fn exec_decl() -> Result<(), String> { 178 | expect("tests/decl.py", 0, 1) 179 | } 180 | 181 | #[test] 182 | fn exec_shadowing() -> Result<(), String> { 183 | expect("tests/shadowing.py", 0, 4) 184 | } 185 | 186 | #[test] 187 | fn exec_typevar() -> Result<(), String> { 188 | expect("tests/typevar.py", 0, 3) 189 | } 190 | 191 | #[test] 192 | fn exec_type_spec() -> Result<(), String> { 193 | expect("tests/err/type_spec.py", 0, 6) 194 | } 195 | 196 | #[test] 197 | fn exec_union() -> Result<(), String> { 198 | expect("tests/union.py", 0, 0) 199 | } 200 | 201 | #[test] 202 | fn exec_widening() -> Result<(), String> { 203 | expect("tests/widening.py", 0, 1) 204 | } 205 | -------------------------------------------------------------------------------- /crates/pylyzer_wasm/lib.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::prelude::*; 2 | 3 | use erg_common::error::ErrorCore; 4 | use erg_common::error::Location as Loc; 5 | use erg_common::traits::{Runnable, Stream}; 6 | use erg_compiler::context::ContextProvider; 7 | use erg_compiler::erg_parser::ast::VarName; 8 | use erg_compiler::error::CompileError; 9 | use erg_compiler::ty::Type as Ty; 10 | use erg_compiler::varinfo::VarInfo; 11 | use pylyzer_core::PythonAnalyzer; 12 | 13 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 14 | #[wasm_bindgen] 15 | pub enum CompItemKind { 16 | Method = 0, 17 | Function = 1, 18 | Constructor = 2, 19 | Field = 3, 20 | Variable = 4, 21 | Class = 5, 22 | Struct = 6, 23 | Interface = 7, 24 | Module = 8, 25 | Property = 9, 26 | Event = 10, 27 | Operator = 11, 28 | Unit = 12, 29 | Value = 13, 30 | Constant = 14, 31 | Enum = 15, 32 | EnumMember = 16, 33 | Keyword = 17, 34 | Text = 18, 35 | Color = 19, 36 | File = 20, 37 | Reference = 21, 38 | Customcolor = 22, 39 | Folder = 23, 40 | TypeParameter = 24, 41 | User = 25, 42 | Issue = 26, 43 | Snippet = 27, 44 | } 45 | 46 | #[derive(Debug, Clone)] 47 | #[wasm_bindgen] 48 | pub struct Location(Loc); 49 | 50 | impl From for Location { 51 | fn from(loc: Loc) -> Self { 52 | Self(loc) 53 | } 54 | } 55 | 56 | impl Location { 57 | pub const UNKNOWN: Location = Location(Loc::Unknown); 58 | } 59 | 60 | #[derive(Debug, Clone)] 61 | #[wasm_bindgen] 62 | #[allow(dead_code)] 63 | pub struct Type(Ty); 64 | 65 | #[derive(Debug, Clone)] 66 | #[wasm_bindgen] 67 | pub struct VarEntry { 68 | name: VarName, 69 | vi: VarInfo, 70 | } 71 | 72 | impl VarEntry { 73 | pub fn new(name: VarName, vi: VarInfo) -> Self { 74 | Self { name, vi } 75 | } 76 | } 77 | 78 | #[wasm_bindgen] 79 | impl VarEntry { 80 | pub fn name(&self) -> String { 81 | self.name.to_string() 82 | } 83 | pub fn item_kind(&self) -> CompItemKind { 84 | match &self.vi.t { 85 | Ty::Callable { .. } => CompItemKind::Function, 86 | Ty::Subr(subr) => { 87 | if subr.self_t().is_some() { 88 | CompItemKind::Method 89 | } else { 90 | CompItemKind::Function 91 | } 92 | } 93 | Ty::Quantified(quant) => match quant.as_ref() { 94 | Ty::Callable { .. } => CompItemKind::Function, 95 | Ty::Subr(subr) => { 96 | if subr.self_t().is_some() { 97 | CompItemKind::Method 98 | } else { 99 | CompItemKind::Function 100 | } 101 | } 102 | _ => unreachable!(), 103 | }, 104 | Ty::ClassType => CompItemKind::Class, 105 | Ty::TraitType => CompItemKind::Interface, 106 | Ty::Poly { name, .. } if &name[..] == "Module" => CompItemKind::Module, 107 | _ if self.vi.muty.is_const() => CompItemKind::Constant, 108 | _ => CompItemKind::Variable, 109 | } 110 | } 111 | pub fn typ(&self) -> String { 112 | self.vi.t.to_string() 113 | } 114 | } 115 | 116 | #[wasm_bindgen] 117 | impl Location { 118 | pub fn ln_begin(&self) -> Option { 119 | self.0.ln_begin() 120 | } 121 | 122 | pub fn ln_end(&self) -> Option { 123 | self.0.ln_end() 124 | } 125 | 126 | pub fn col_begin(&self) -> Option { 127 | self.0.col_begin() 128 | } 129 | 130 | pub fn col_end(&self) -> Option { 131 | self.0.col_end() 132 | } 133 | } 134 | 135 | #[derive(Debug, Clone)] 136 | #[wasm_bindgen(getter_with_clone)] 137 | pub struct Error { 138 | pub errno: usize, 139 | pub is_warning: bool, 140 | // pub kind: ErrorKind, 141 | pub loc: Location, 142 | pub desc: String, 143 | pub hint: Option, 144 | } 145 | 146 | fn find_fallback_loc(err: &ErrorCore) -> Loc { 147 | if err.loc == Loc::Unknown { 148 | for sub in &err.sub_messages { 149 | if sub.loc != Loc::Unknown { 150 | return sub.loc; 151 | } 152 | } 153 | Loc::Unknown 154 | } else { 155 | err.loc 156 | } 157 | } 158 | 159 | impl From for Error { 160 | fn from(err: CompileError) -> Self { 161 | let loc = Location(find_fallback_loc(&err.core)); 162 | let sub_msg = err 163 | .core 164 | .sub_messages 165 | .first() 166 | .map(|sub| { 167 | sub.msg 168 | .iter() 169 | .fold("\n".to_string(), |acc, s| acc + s + "\n") 170 | }) 171 | .unwrap_or_default(); 172 | let desc = err.core.main_message + &sub_msg; 173 | Self { 174 | errno: err.core.errno, 175 | is_warning: err.core.kind.is_warning(), 176 | // kind: err.kind(), 177 | loc, 178 | desc, 179 | hint: err 180 | .core 181 | .sub_messages 182 | .first() 183 | .and_then(|sub| sub.hint.clone()), 184 | } 185 | } 186 | } 187 | 188 | impl Error { 189 | pub const fn new( 190 | errno: usize, 191 | is_warning: bool, 192 | loc: Location, 193 | desc: String, 194 | hint: Option, 195 | ) -> Self { 196 | Self { 197 | errno, 198 | is_warning, 199 | loc, 200 | desc, 201 | hint, 202 | } 203 | } 204 | } 205 | 206 | #[wasm_bindgen] 207 | // #[derive()] 208 | pub struct Analyzer { 209 | analyzer: PythonAnalyzer, 210 | } 211 | 212 | impl Default for Analyzer { 213 | fn default() -> Self { 214 | Self::new() 215 | } 216 | } 217 | 218 | #[wasm_bindgen] 219 | impl Analyzer { 220 | pub fn new() -> Self { 221 | Analyzer { 222 | analyzer: PythonAnalyzer::default(), 223 | } 224 | } 225 | 226 | pub fn clear(&mut self) { 227 | self.analyzer.clear(); 228 | } 229 | 230 | pub fn start_message(&self) -> String { 231 | self.analyzer.start_message() 232 | } 233 | 234 | pub fn dir(&mut self) -> Box<[VarEntry]> { 235 | self.analyzer 236 | .dir() 237 | .into_iter() 238 | .map(|(n, vi)| VarEntry::new(n.clone(), vi.clone())) 239 | .collect::>() 240 | .into_boxed_slice() 241 | } 242 | 243 | pub fn check(&mut self, input: &str) -> Box<[Error]> { 244 | match self.analyzer.analyze(input.to_string(), "exec") { 245 | Ok(artifact) => artifact 246 | .warns 247 | .into_iter() 248 | .map(Error::from) 249 | .collect::>() 250 | .into_boxed_slice(), 251 | Err(mut err_artifact) => { 252 | err_artifact.errors.extend(err_artifact.warns); 253 | let errs = err_artifact 254 | .errors 255 | .into_iter() 256 | .map(Error::from) 257 | .collect::>(); 258 | errs.into_boxed_slice() 259 | } 260 | } 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pylyzer ⚡ 2 | 3 | > [!IMPORTANT] 4 | > pylyzer is now under the maintenance phase, which means that only bug fixes will be made and no new features will be added. 5 | The author is currently cooperating with the development of [astral-sh/ty](https://github.com/astral-sh/ty). Please try that instead! 6 | 7 | ![pylyzer_logo_with_letters](https://raw.githubusercontent.com/mtshiba/pylyzer/main/images/pylyzer-logo-with-letters.png) 8 | 9 | vsm-version 10 | Build status 11 | Build status 12 | 13 | `pylyzer` is a static code analyzer / language server for Python, written in Rust. 14 | 15 | ## Installation 16 | 17 | ### pip 18 | 19 | ```bash 20 | pip install pylyzer 21 | ``` 22 | 23 | ### cargo (Rust package manager) 24 | 25 | ```bash 26 | cargo install pylyzer --locked 27 | ``` 28 | 29 | ### build from source 30 | 31 | ```bash 32 | git clone https://github.com/mtshiba/pylyzer.git 33 | cargo install --path . --locked 34 | ``` 35 | 36 | Make sure that `cargo`/`rustc` is up-to-date, as pylyzer may be written with the latest (stable) language features. 37 | 38 | ### [GitHub Releases](https://github.com/mtshiba/pylyzer/releases/latest) 39 | 40 | ## How to use 41 | 42 | ### Check a single file 43 | 44 | ```sh 45 | pylyzer file.py 46 | ``` 47 | 48 | ### Check multiple files 49 | 50 | ```sh 51 | # glob patterns are supported 52 | pylyzer file1.py file2.py dir/file*.py 53 | ``` 54 | 55 | ### Check an entire package 56 | 57 | If you don't specify a file path, pylyzer will automatically search for the entry point. 58 | 59 | ```sh 60 | pylyzer 61 | ``` 62 | 63 | ### Start the language server 64 | 65 | This option is used when an LSP-aware editor requires arguments to start pylyzer. 66 | 67 | ```sh 68 | pylyzer --server 69 | ``` 70 | 71 | For other options, check [the manual](https://mtshiba.github.io/pylyzer/options/options/). 72 | 73 | ## What is the advantage over pylint, pyright, pytype, etc.? 74 | 75 | * Performance 🌟 76 | 77 | On average, pylyzer can inspect Python scripts more than __100 times faster__ than pytype and pyright [1](#1). This is largely due to the fact that pylyzer is implemented in Rust. 78 | 79 | ![performance](https://raw.githubusercontent.com/mtshiba/pylyzer/main/images/performance.png) 80 | 81 | * Reports readability 📖 82 | 83 | While pytype/pyright's error reports are illegible, pylyzer shows where the error occurred and provides clear error messages. 84 | 85 | ### pyright 86 | 87 | ![pyright_report](https://raw.githubusercontent.com/mtshiba/pylyzer/main/images/pyright_report.png) 88 | 89 | ### pylyzer 😃 90 | 91 | ![report](https://raw.githubusercontent.com/mtshiba/pylyzer/main/images/report.png) 92 | 93 | * Rich LSP support 📝 94 | 95 | pylyzer as a language server supports various features, such as completion and renaming (The language server is an adaptation of the Erg Language Server (ELS). For more information on the implemented features, please see [here](https://github.com/erg-lang/erg/tree/main/crates/els#readme)). 96 | 97 | ![lsp_support](https://raw.githubusercontent.com/mtshiba/pylyzer/main/images/lsp_support.png) 98 | 99 | ![autoimport](https://raw.githubusercontent.com/mtshiba/pylyzer/main/images/autoimport.gif) 100 | 101 | ## VSCode extension 102 | 103 | You can install the VSCode extension from the [Marketplace](https://marketplace.visualstudio.com/items?itemName=pylyzer.pylyzer) or from the command line: 104 | 105 | ```sh 106 | code --install-extension pylyzer.pylyzer 107 | ``` 108 | 109 | ## What is the difference from [Ruff](https://github.com/astral-sh/ruff)? 110 | 111 | [Ruff](https://github.com/astral-sh/ruff), like pylyzer, is a static code analysis tool for Python written in Rust, but Ruff is a linter and pylyzer is a type checker & language server. 112 | pylyzer does not perform linting & formatting, and Ruff does not perform type checking. 113 | 114 | ## How it works 115 | 116 | pylyzer uses the type checker of [the Erg programming language](https://erg-lang.org) internally. 117 | This language is a transpiled language that targets Python, and has a static type system. 118 | 119 | pylyzer converts Python ASTs to Erg ASTs and passes them to Erg's type checker. It then displays the results with appropriate modifications. 120 | 121 | ## Limitations 122 | 123 | * pylyzer's type inspector only assumes (potentially) statically typed code, so you cannot check any code uses reflections, such as `exec`, `setattr`, etc. 124 | 125 | * pylyzer (= Erg's type system) has its own type declarations for the Python standard APIs. Typing of all APIs is not complete and may result in an error that such an API does not exist. 126 | 127 | * Since pylyzer's type checking is conservative, you may encounter many (possibly false positive) errors. We are working on fixing this, but if you are concerned about editor errors, please turn off the diagnostics feature. 128 | 129 | ## TODOs 130 | 131 | * [x] type checking 132 | * [x] variable 133 | * [x] operator 134 | * [x] function/method 135 | * [x] class 136 | * [ ] `async/await` 137 | * [ ] user-defined abstract class 138 | * [x] type inference 139 | * [x] variable 140 | * [x] operator 141 | * [x] function/method 142 | * [x] class 143 | * [x] builtin modules analysis 144 | * [x] local scripts analysis 145 | * [x] local packages analysis 146 | * [x] LSP features 147 | * [x] diagnostics 148 | * [x] completion 149 | * [x] rename 150 | * [x] hover 151 | * [x] goto definition 152 | * [x] signature help 153 | * [x] find references 154 | * [x] document symbol 155 | * [x] call hierarchy 156 | * [x] collection types 157 | * [x] `list` 158 | * [x] `dict` 159 | * [x] `tuple` 160 | * [x] `set` 161 | * [ ] `typing` 162 | * [x] `Union` 163 | * [x] `Optional` 164 | * [x] `Literal` 165 | * [x] `Callable` 166 | * [x] `Any` 167 | * [x] `TypeVar` 168 | * [ ] `TypedDict` 169 | * [ ] `ClassVar` 170 | * [ ] `Generic` 171 | * [ ] `Protocol` 172 | * [ ] `Final` 173 | * [ ] `Annotated` 174 | * [ ] `TypeAlias` 175 | * [ ] `TypeGuard` 176 | * [x] type parameter syntax 177 | * [x] type narrowing 178 | * [ ] others 179 | * [ ] `collections.abc` 180 | * [x] `Collection` 181 | * [x] `Container` 182 | * [x] `Generator` 183 | * [x] `Iterable` 184 | * [x] `Iterator` 185 | * [x] `Mapping`, `MutableMapping` 186 | * [x] `Sequence`, `MutableSequence` 187 | * [ ] others 188 | * [x] type assertion (`typing.cast`) 189 | * [x] type narrowing (`is`, `isinstance`) 190 | * [x] `pyi` (stub) files support 191 | * [x] glob pattern file check 192 | * [x] type comment (`# type: ...`) 193 | * [x] virtual environment support 194 | * [x] package manager support 195 | * [x] `pip` 196 | * [x] `poetry` 197 | * [x] `uv` 198 | 199 | ## Join us! 200 | 201 | We are looking for contributors to help us improve pylyzer. If you are interested in contributing and have any questions, please feel free to contact us. 202 | 203 | * [Discord (Erg language)](https://discord.gg/kQBuaSUS46) 204 | * [#pylyzer](https://discord.com/channels/1006946336433774742/1056815981168697354) 205 | * [GitHub discussions](https://github.com/mtshiba/pylyzer/discussions) 206 | 207 | --- 208 | 209 | 1 The performance test was conducted on MacBook (Early 2016) with 1.1 GHz Intel Core m3 processor and 8 GB 1867 MHz LPDDR3 memory.[↩](#f1) 210 | -------------------------------------------------------------------------------- /crates/py2erg/gen_decl.rs: -------------------------------------------------------------------------------- 1 | use std::fs::{create_dir_all, File}; 2 | use std::io::{BufWriter, Write}; 3 | use std::path::Path; 4 | 5 | use erg_common::pathutil::{mod_name, NormalizedPathBuf}; 6 | use erg_common::set::Set; 7 | use erg_common::traits::LimitedDisplay; 8 | use erg_common::{log, Str}; 9 | use erg_compiler::build_package::{CheckStatus, PylyzerStatus}; 10 | use erg_compiler::hir::{ClassDef, Expr, HIR}; 11 | use erg_compiler::module::SharedModuleCache; 12 | use erg_compiler::ty::value::{GenTypeObj, TypeObj}; 13 | use erg_compiler::ty::{HasType, Type}; 14 | 15 | pub struct DeclFile { 16 | pub filename: String, 17 | pub code: String, 18 | } 19 | pub struct DeclFileGenerator { 20 | filename: String, 21 | namespace: String, 22 | imported: Set, 23 | code: String, 24 | } 25 | 26 | impl DeclFileGenerator { 27 | pub fn new(path: &NormalizedPathBuf, status: CheckStatus) -> std::io::Result { 28 | let (timestamp, hash) = { 29 | let metadata = std::fs::metadata(path)?; 30 | let dummy_hash = metadata.len(); 31 | (metadata.modified()?, dummy_hash) 32 | }; 33 | let status = PylyzerStatus { 34 | status, 35 | file: path.to_path_buf(), 36 | timestamp, 37 | hash, 38 | }; 39 | let code = format!("{status}\n"); 40 | Ok(Self { 41 | filename: path 42 | .file_name() 43 | .unwrap_or_default() 44 | .to_string_lossy() 45 | .replace(".py", ".d.er"), 46 | namespace: "".to_string(), 47 | imported: Set::new(), 48 | code, 49 | }) 50 | } 51 | 52 | pub fn gen_decl_er(mut self, hir: &HIR) -> DeclFile { 53 | for chunk in hir.module.iter() { 54 | self.gen_chunk_decl(chunk); 55 | } 56 | log!("code:\n{}", self.code); 57 | DeclFile { 58 | filename: self.filename, 59 | code: self.code, 60 | } 61 | } 62 | 63 | fn escape_type(&self, typ: String) -> String { 64 | typ.replace('%', "Type_") 65 | .replace("", "") 66 | .replace('/', ".") 67 | .trim_start_matches(self.filename.trim_end_matches(".d.er")) 68 | .trim_start_matches(&self.namespace) 69 | .to_string() 70 | } 71 | 72 | // e.g. `x: foo.Bar` => `foo = pyimport "foo"; x: foo.Bar` 73 | fn prepare_using_type(&mut self, typ: &Type) { 74 | let namespace = Str::rc( 75 | typ.namespace() 76 | .split('/') 77 | .next() 78 | .unwrap() 79 | .split('.') 80 | .next() 81 | .unwrap(), 82 | ); 83 | if namespace != self.namespace 84 | && !namespace.is_empty() 85 | && self.imported.insert(namespace.clone()) 86 | { 87 | self.code += &format!("{namespace} = pyimport \"{namespace}\"\n"); 88 | } 89 | } 90 | 91 | fn gen_chunk_decl(&mut self, chunk: &Expr) { 92 | match chunk { 93 | Expr::Def(def) => { 94 | let mut name = def 95 | .sig 96 | .ident() 97 | .inspect() 98 | .replace('\0', "") 99 | .replace(['%', '*'], "___"); 100 | let ref_t = def.sig.ident().ref_t(); 101 | self.prepare_using_type(ref_t); 102 | let typ = self.escape_type(ref_t.replace_failure().to_string_unabbreviated()); 103 | // Erg can automatically import nested modules 104 | // `import http.client` => `http = pyimport "http"` 105 | let decl = if ref_t.is_py_module() && ref_t.typarams()[0].is_str_value() { 106 | name = name.split('.').next().unwrap().to_string(); 107 | let full_path_str = ref_t.typarams()[0].to_string_unabbreviated(); 108 | let mod_name = mod_name(Path::new(full_path_str.trim_matches('"'))); 109 | let imported = if self.imported.insert(mod_name.clone()) { 110 | format!("{}.{mod_name} = pyimport \"{mod_name}\"", self.namespace) 111 | } else { 112 | "".to_string() 113 | }; 114 | if self.imported.insert(name.clone().into()) { 115 | format!( 116 | "{}.{name} = pyimport \"{mod_name}\"\n{imported}", 117 | self.namespace, 118 | ) 119 | } else { 120 | imported 121 | } 122 | } else { 123 | format!("{}.{name}: {typ}", self.namespace) 124 | }; 125 | self.code += &decl; 126 | } 127 | Expr::ClassDef(def) => { 128 | let class_name = def 129 | .sig 130 | .ident() 131 | .inspect() 132 | .replace('\0', "") 133 | .replace(['%', '*'], "___"); 134 | let src = format!("{}.{class_name}", self.namespace); 135 | let stash = std::mem::replace(&mut self.namespace, src); 136 | let decl = format!(".{class_name}: ClassType"); 137 | self.code += &decl; 138 | self.code.push('\n'); 139 | if let GenTypeObj::Subclass(class) = def.obj.as_ref() { 140 | let sup = class 141 | .sup 142 | .as_ref() 143 | .typ() 144 | .replace_failure() 145 | .to_string_unabbreviated(); 146 | self.prepare_using_type(class.sup.typ()); 147 | let sup = self.escape_type(sup); 148 | let decl = format!(".{class_name} <: {sup}\n"); 149 | self.code += &decl; 150 | } 151 | if let Some(TypeObj::Builtin { 152 | t: Type::Record(rec), 153 | .. 154 | }) = def.obj.base_or_sup() 155 | { 156 | for (attr, t) in rec.iter() { 157 | self.prepare_using_type(t); 158 | let typ = self.escape_type(t.replace_failure().to_string_unabbreviated()); 159 | let decl = format!("{}.{}: {typ}\n", self.namespace, attr.symbol); 160 | self.code += &decl; 161 | } 162 | } 163 | if let Some(TypeObj::Builtin { 164 | t: Type::Record(rec), 165 | .. 166 | }) = def.obj.additional() 167 | { 168 | for (attr, t) in rec.iter() { 169 | self.prepare_using_type(t); 170 | let typ = self.escape_type(t.replace_failure().to_string_unabbreviated()); 171 | let decl = format!("{}.{}: {typ}\n", self.namespace, attr.symbol); 172 | self.code += &decl; 173 | } 174 | } 175 | for attr in ClassDef::get_all_methods(&def.methods_list) { 176 | self.gen_chunk_decl(attr); 177 | } 178 | self.namespace = stash; 179 | } 180 | Expr::Dummy(dummy) => { 181 | for chunk in dummy.iter() { 182 | self.gen_chunk_decl(chunk); 183 | } 184 | } 185 | Expr::Compound(compound) => { 186 | for chunk in compound.iter() { 187 | self.gen_chunk_decl(chunk); 188 | } 189 | } 190 | Expr::Call(call) if call.control_kind().is_some() => { 191 | for arg in call.args.iter() { 192 | self.gen_chunk_decl(arg); 193 | } 194 | } 195 | Expr::Lambda(lambda) => { 196 | for arg in lambda.body.iter() { 197 | self.gen_chunk_decl(arg); 198 | } 199 | } 200 | _ => {} 201 | } 202 | self.code.push('\n'); 203 | } 204 | } 205 | 206 | fn dump_decl_er(path: &NormalizedPathBuf, hir: &HIR, status: CheckStatus) -> std::io::Result<()> { 207 | let decl_gen = DeclFileGenerator::new(path, status)?; 208 | let file = decl_gen.gen_decl_er(hir); 209 | let Some(dir) = path.parent().and_then(|p| p.canonicalize().ok()) else { 210 | return Ok(()); 211 | }; 212 | let cache_dir = dir.join("__pycache__"); 213 | if !cache_dir.exists() { 214 | let _ = create_dir_all(&cache_dir); 215 | } 216 | let path = cache_dir.join(file.filename); 217 | if !path.exists() { 218 | File::create(&path)?; 219 | } 220 | let f = File::options().write(true).open(path)?; 221 | let mut f = BufWriter::new(f); 222 | f.write_all(file.code.as_bytes()) 223 | } 224 | 225 | pub fn dump_decl_package(modules: &SharedModuleCache) { 226 | for (path, module) in modules.raw_iter() { 227 | if let Some(hir) = module.hir.as_ref() { 228 | let _ = dump_decl_er(path, hir, module.status); 229 | } 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /images/pylyzer-logo-with-letters.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 41 | 44 | 45 | 47 | 52 | 57 | 64 | 68 | 72 | 76 | 80 | 84 | 88 | 92 | 97 | 102 | 107 | 112 | 113 | 114 | 116 | 117 | 119 | 121 | 122 | 124 | 126 | 128 | 130 | 132 | 134 | 136 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::path::{Path, PathBuf}; 3 | use std::str::FromStr; 4 | 5 | use erg_common::config::{ErgConfig, ErgMode}; 6 | use erg_common::io::Input; 7 | use erg_common::pathutil::project_entry_file_of; 8 | use erg_common::switch_lang; 9 | use indexmap::IndexSet; 10 | 11 | use crate::copy::clear_cache; 12 | 13 | fn entry_file() -> Option { 14 | project_entry_file_of(&env::current_dir().ok()?).or_else(|| { 15 | let mut opt_path = None; 16 | for ent in Path::new(".").read_dir().ok()? { 17 | let ent = ent.ok()?; 18 | if ent.file_type().ok()?.is_file() { 19 | let path = ent.path(); 20 | if path.file_name().is_some_and(|name| name == "__init__.py") { 21 | return Some(path); 22 | } else if path.extension().is_some_and(|ext| ext == "py") { 23 | if opt_path.is_some() { 24 | return None; 25 | } else { 26 | opt_path = Some(path); 27 | } 28 | } 29 | } 30 | } 31 | opt_path 32 | }) 33 | } 34 | 35 | fn command_message() -> &'static str { 36 | switch_lang!( 37 | "japanese" => 38 | "\ 39 | USAGE: 40 | pylyzer [OPTIONS] [ARGS]... 41 | 42 | ARGS: 43 |