Compare commits

..

No commits in common. "main" and "v0.0.43" have entirely different histories.

81 changed files with 3971 additions and 8434 deletions

View File

@ -1,53 +0,0 @@
name: Bug report
description: Create a report to help us improve
labels:
- bug
body:
- type: textarea
attributes:
label: "Describe the bug"
description: "A clear and concise description of what the bug is."
placeholder: "Enter a detailed description of the bug"
validations:
required: true
- type: textarea
attributes:
label: "Reproducible Code"
description: "Provide code or steps needed to reproduce the bug."
placeholder: "Enter code snippet or reproduction steps"
validations:
required: true
- type: textarea
attributes:
label: "Environment"
description: "Provide details such as OS, version, etc."
placeholder: "OS: \nVersion: "
validations:
required: true
- type: textarea
attributes:
label: "Expected behavior"
description: "A clear and concise description of what you expected to happen."
placeholder: "Enter what you expected to happen"
validations:
required: false
- type: textarea
attributes:
label: "Error log"
description: "Add error logs here. Language server errors may be logged in $ERG_PATH/els.log."
validations:
required: false
- type: textarea
attributes:
label: "Screenshots"
description: "Add screenshots to help explain your problem."
placeholder: "Provide screenshot links or instructions to attach images"
validations:
required: false
- type: textarea
attributes:
label: "Additional context"
description: "Add any other context about the problem here."
placeholder: "Enter any additional context"
validations:
required: false

View File

@ -1,2 +0,0 @@
# This file cannot use the extension `.yaml`.
blank_issues_enabled: false

View File

@ -1,19 +0,0 @@
name: Feature request
description: Suggest an idea for this project
labels:
- enhancement
body:
- type: textarea
attributes:
label: "Describe the feature"
description: "A clear and concise description of what the feature is."
placeholder: "Enter a detailed description of the feature"
validations:
required: true
- type: textarea
attributes:
label: "Additional context"
description: "Add any other context about the feature here."
placeholder: "Enter any additional context"
validations:
required: false

View File

@ -26,71 +26,35 @@ jobs:
cargo login ${{ secrets.CARGO_TOKEN }}
chmod +x cargo_publish.sh
./cargo_publish.sh --cargo-only
make-pypi-artifacts:
pypi:
strategy:
matrix:
include:
# - target: armv7-unknown-linux-gnueabihf
# os: ubuntu-latest
# - target: aarch64-unknown-linux-gnu
# os: ubuntu-latest
# - target: aarch64-apple-darwin
# os: macos-latest
- target: x86_64-unknown-linux-gnu
platform: linux
os: ubuntu-latest
- target: x86_64-apple-darwin
platform: macos
os: macos-latest
- target: x86_64-pc-windows-msvc
platform: windows
os: windows-latest
# - target: i686-pc-windows-msvc
# os: windows-latest
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.11"
- name: setup-tools
run: |
rustup update stable
pip3 install twine build cibuildwheel setuptools-rust tomli
rustup target add ${{ matrix.target }}
- name: build
run: cibuildwheel --output-dir dist --platform ${{ matrix.platform }}
env:
# rust doesn't seem to be available for musl linux on i686
CIBW_SKIP: '*-musllinux_i686'
CIBW_ENVIRONMENT: 'PATH="$HOME/.cargo/bin:$PATH" CARGO_TERM_COLOR="always"'
CIBW_ENVIRONMENT_WINDOWS: 'PATH="$UserProfile\.cargo\bin;$PATH"'
CIBW_BEFORE_BUILD: rustup show
CIBW_BEFORE_BUILD_LINUX: >
curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain=stable --profile=minimal -y &&
rustup show
# CIBW_BUILD_VERBOSITY: 1
# - name: upload
# run: |
# twine upload -u mtshiba -p ${{ secrets.PYPI_PASSWORD }} --skip-existing dist/*
# cargo build --release --target ${{ matrix.target }}
# python3 -m build --wheel
# maturin publish -u mtshiba -p ${{ secrets.PYPI_PASSWORD }} --target ${{ matrix.target }} --skip-existing
- name: upload artifacts
uses: actions/upload-artifact@v4
with:
name: dist-${{ matrix.platform }}
path: dist
publish-pypi-artifacts:
needs: make-pypi-artifacts
runs-on: ubuntu-latest
permissions:
# For pypi trusted publishing
id-token: write
steps:
- name: download-artifacts
uses: actions/download-artifact@v4
with:
pattern: dist-*
path: dist
merge-multiple: true
- name: Publish to PyPi
uses: pypa/gh-action-pypi-publish@release/v1
with:
skip-existing: true
verbose: true
pip3 install maturin
- name: upload
run: |
maturin publish -u mtshiba -p ${{ secrets.PYPI_PASSWORD }} --target ${{ matrix.target }} --skip-existing
upload-assets:
needs: create-release
strategy:

View File

@ -31,6 +31,7 @@ jobs:
steps:
- uses: actions/checkout@v2
# installing pylyzer itself is required for the tests
- name: Build
run: |
rustup update stable

View File

@ -1,22 +0,0 @@
name: Close inactive issues
on:
schedule:
- cron: "30 1 * * *"
jobs:
close-issues:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: actions/stale@v9
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: "This issue is stale because it has been open for 60 days with no activity."
close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
stale-issue-label: "stale"
exempt-issue-labels: "bug"
days-before-issue-stale: 60
days-before-issue-close: 14
days-before-pr-stale: -1
days-before-pr-close: -1

View File

@ -1,99 +0,0 @@
name: Test
on:
push:
branches: [main]
paths-ignore:
- "docs/**"
- "images/**"
- "**.md"
- "**.yml"
- "LICENSE-**"
- ".gitmessage"
- ".pre-commit-config.yaml"
pull_request:
branches: [main]
paths-ignore:
- "docs/**"
- "images/**"
- "**.md"
- "**.yml"
- "LICENSE-**"
- ".pre-commit-config.yaml"
env:
CARGO_TERM_COLOR: always
jobs:
package-test:
strategy:
fail-fast: false
matrix:
os: [windows-latest, ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install
run: |
rustup update stable
cargo install --path .
- name: boto3
continue-on-error: true
run: |
pip3 install boto3
pylyzer -c "import boto3" || true
- name: urllib3
run: |
pip3 install urllib3
pylyzer -c "import urllib3" || true
- name: setuptools
run: |
pip3 install setuptools
pylyzer -c "import setuptools" || true
- name: requests
run: |
pip3 install requests
pylyzer -c "import requests" || true
- name: certifi
run: |
pip3 install certifi
pylyzer -c "import certifi" || true
- name: charset-normalizer
run: |
pip3 install charset-normalizer
pylyzer -c "import charset_normalizer" || true
- name: idna
run: |
pip3 install idna
pylyzer -c "import idna" || true
- name: typing-extensions
run: |
pip3 install typing-extensions
pylyzer -c "import typing_extensions" || true
- name: python-dateutil
run: |
pip3 install python-dateutil
pylyzer -c "import dateutil" || true
- name: packaging
run: |
pip3 install packaging
pylyzer -c "import packaging" || true
- name: six
run: |
pip3 install six
pylyzer -c "import six" || true
- name: s3transfer
run: |
pip3 install s3transfer
pylyzer -c "import s3transfer" || true
- name: pyyaml
run: |
pip3 install pyyaml
pylyzer -c "import yaml" || true
- name: cryptography
run: |
pip3 install cryptography
pylyzer -c "import cryptography" || true

2
.gitignore vendored
View File

@ -1,5 +1,3 @@
/target
__pycache__/
test*.py
/site
.venv

View File

@ -2,12 +2,5 @@
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter"
},
"python.formatting.provider": "none",
"[typescript]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "biomejs.biome"
},
"cSpell.words": [
"indexmap"
]
"python.formatting.provider": "none"
}

1128
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -12,25 +12,23 @@ repository.workspace = true
[workspace]
members = [
"crates/py2erg",
"crates/pylyzer_core",
"crates/pylyzer_wasm",
]
[workspace.package]
version = "0.0.82"
version = "0.0.43"
authors = ["Shunsuke Shibayama <sbym1346@gmail.com>"]
license = "MIT OR Apache-2.0"
edition = "2021"
repository = "https://github.com/mtshiba/pylyzer"
[workspace.dependencies]
erg_common = { version = "0.6.53-nightly.5", features = ["py_compat", "els"] }
erg_compiler = { version = "0.6.53-nightly.5", features = ["py_compat", "els"] }
els = { version = "0.1.65-nightly.5", features = ["py_compat"] }
# rustpython-parser = { version = "0.3.0", features = ["all-nodes-with-ranges", "location"] }
# rustpython-ast = { version = "0.3.0", features = ["all-nodes-with-ranges", "location"] }
rustpython-parser = { git = "https://github.com/RustPython/Parser", version = "0.4.0", features = ["all-nodes-with-ranges", "location"] }
rustpython-ast = { git = "https://github.com/RustPython/Parser", version = "0.4.0", features = ["all-nodes-with-ranges", "location"] }
erg_common = { version = "0.6.20-nightly.2", features = ["py_compat", "els"] }
erg_compiler = { version = "0.6.20-nightly.2", features = ["py_compat", "els"] }
els = { version = "0.1.32-nightly.2", features = ["py_compat"] }
rustpython-parser = { version = "0.3.0", features = ["all-nodes-with-ranges", "location"] }
rustpython-ast = { version = "0.3.0", features = ["all-nodes-with-ranges", "location"] }
# rustpython-parser = { git = "https://github.com/RustPython/Parser", version = "0.3.0", features = ["all-nodes-with-ranges", "location"] }
# rustpython-ast = { git = "https://github.com/RustPython/Parser", version = "0.3.0", features = ["all-nodes-with-ranges", "location"] }
# erg_compiler = { git = "https://github.com/erg-lang/erg", branch = "main", features = ["py_compat", "els"] }
# erg_common = { git = "https://github.com/erg-lang/erg", branch = "main", features = ["py_compat", "els"] }
# els = { git = "https://github.com/erg-lang/erg", branch = "main", features = ["py_compat"] }
@ -39,26 +37,19 @@ rustpython-ast = { git = "https://github.com/RustPython/Parser", version = "0.4.
# els = { path = "../erg/crates/els", features = ["py_compat"] }
[features]
debug = ["erg_common/debug", "pylyzer_core/debug"]
large_thread = ["erg_common/large_thread", "els/large_thread", "pylyzer_core/large_thread"]
pretty = ["erg_common/pretty", "pylyzer_core/pretty"]
backtrace = ["erg_common/backtrace", "els/backtrace", "pylyzer_core/backtrace"]
experimental = ["erg_common/experimental", "els/experimental", "pylyzer_core/experimental", "parallel"]
parallel = ["erg_common/parallel", "pylyzer_core/parallel"]
japanese = ["erg_common/japanese", "els/japanese"]
simplified_chinese = ["erg_common/simplified_chinese", "els/simplified_chinese"]
traditional_chinese = ["erg_common/traditional_chinese", "els/traditional_chinese"]
debug = ["erg_compiler/debug", "erg_common/debug", "py2erg/debug"]
large_thread = ["erg_compiler/large_thread", "erg_common/large_thread", "els/large_thread"]
pretty = ["erg_compiler/pretty", "erg_common/pretty"]
backtrace = ["erg_common/backtrace"]
experimental = ["els/experimental", "erg_compiler/experimental", "erg_common/experimental"]
[dependencies]
pylyzer_core = { version = "0.0.82", path = "./crates/pylyzer_core" }
erg_compiler = { workspace = true }
erg_common = { workspace = true }
els = { workspace = true }
glob = "0.3.2"
indexmap = "2.7.1"
rustpython-parser = { workspace = true }
rustpython-ast = { workspace = true }
py2erg = { version = "0.0.43", path = "./crates/py2erg" }
[dev-dependencies]
erg_compiler = { workspace = true }
[profile.opt-with-dbg]
inherits = "release"
debug = true
[lib]
path = "src/lib.rs"

184
README.md
View File

@ -1,9 +1,5 @@
# pylyzer ⚡
> [!IMPORTANT]
> pylyzer is now under the maintenance phase, which means that only bug fixes will be made and no new features will be added.
The author is currently cooperating with the development of [astral-sh/ty](https://github.com/astral-sh/ty). Please try that instead!
![pylyzer_logo_with_letters](https://raw.githubusercontent.com/mtshiba/pylyzer/main/images/pylyzer-logo-with-letters.png)
<a href="https://marketplace.visualstudio.com/items?itemName=pylyzer.pylyzer" target="_blank" rel="noreferrer noopener nofollow"><img src="https://img.shields.io/visual-studio-marketplace/v/pylyzer.pylyzer?style=flat&amp;label=VS%20Marketplace&amp;logo=visual-studio-code" alt="vsm-version"></a>
@ -14,62 +10,35 @@ The author is currently cooperating with the development of [astral-sh/ty](https
## Installation
### pip
### cargo (rust package manager)
```bash
pip install pylyzer
```
### cargo (Rust package manager)
```bash
cargo install pylyzer --locked
cargo install pylyzer
```
### build from source
```bash
git clone https://github.com/mtshiba/pylyzer.git
cargo install --path . --locked
cargo install --path .
```
Make sure that `cargo`/`rustc` is up-to-date, as pylyzer may be written with the latest (stable) language features.
Make sure that `cargo/rustc` is up-to-date, as pylyzer may be written with the latest language features.
### pip
```bash
pip install pylyzer
```
__If installed this way, you also need to [install Erg](https://github.com/mtshiba/ergup).__
```bash
curl -L https://github.com/mtshiba/ergup/raw/main/ergup.py | python3
```
### [GitHub Releases](https://github.com/mtshiba/pylyzer/releases/latest)
## How to use
### Check a single file
```sh
pylyzer file.py
```
### Check multiple files
```sh
# glob patterns are supported
pylyzer file1.py file2.py dir/file*.py
```
### Check an entire package
If you don't specify a file path, pylyzer will automatically search for the entry point.
```sh
pylyzer
```
### Start the language server
This option is used when an LSP-aware editor requires arguments to start pylyzer.
```sh
pylyzer --server
```
For other options, check [the manual](https://mtshiba.github.io/pylyzer/options/options/).
## What is the advantage over pylint, pyright, pytype, etc.?
* Performance 🌟
@ -78,18 +47,24 @@ On average, pylyzer can inspect Python scripts more than __100 times faster__ th
![performance](https://raw.githubusercontent.com/mtshiba/pylyzer/main/images/performance.png)
* Detailed analysis 🩺
pylyzer can do more than the type checking. For example, it can detect out-of-bounds accesses to lists and accesses to nonexistent keys in dicts.
![analysis](https://raw.githubusercontent.com/mtshiba/pylyzer/main/images/analysis.png)
* Reports readability 📖
While pytype/pyright's error reports are illegible, pylyzer shows where the error occurred and provides clear error messages.
### pyright
![pyright_report](https://raw.githubusercontent.com/mtshiba/pylyzer/main/images/pyright_report.png)
### pylyzer 😃
![report](https://raw.githubusercontent.com/mtshiba/pylyzer/main/images/report.png)
### pyright 🙃
![pyright_report](https://raw.githubusercontent.com/mtshiba/pylyzer/main/images/pyright_report.png)
* Rich LSP support 📝
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)).
@ -106,10 +81,10 @@ You can install the VSCode extension from the [Marketplace](https://marketplace.
code --install-extension pylyzer.pylyzer
```
## What is the difference from [Ruff](https://github.com/astral-sh/ruff)?
## What is the difference from [Ruff](https://github.com/charliermarsh/ruff)?
[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.
pylyzer does not perform linting & formatting, and Ruff does not perform type checking.
[Ruff](https://github.com/charliermarsh/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.
pylyzer does not perform linting, and Ruff does not perform type checking.
## How it works
@ -129,80 +104,43 @@ pylyzer converts Python ASTs to Erg ASTs and passes them to Erg's type checker.
## TODOs
* [x] type checking
* [x] variable
* [x] operator
* [x] function/method
* [x] class
* [ ] `async/await`
* [ ] user-defined abstract class
* [x] variable
* [x] operator
* [x] function/method
* [x] class
* [x] type inference
* [x] variable
* [x] operator
* [x] function/method
* [x] class
* [x] builtin modules analysis
* [x] local scripts analysis
* [x] local packages analysis
* [x] LSP features
* [x] diagnostics
* [x] completion
* [x] rename
* [x] hover
* [x] goto definition
* [x] signature help
* [x] find references
* [x] document symbol
* [x] call hierarchy
* [x] variable
* [x] operator
* [x] function/method
* [x] class
* [x] builtin modules resolving (partially)
* [x] local scripts resolving
* [ ] local packages resolving
* [x] collection types
* [x] `list`
* [x] `dict`
* [x] `tuple`
* [x] `set`
* [x] `list`
* [x] `dict`
* [x] `tuple`
* [ ] `typing`
* [x] `Union`
* [x] `Optional`
* [x] `Literal`
* [x] `Callable`
* [x] `Any`
* [x] `TypeVar`
* [ ] `TypedDict`
* [ ] `ClassVar`
* [ ] `Generic`
* [ ] `Protocol`
* [ ] `Final`
* [ ] `Annotated`
* [ ] `TypeAlias`
* [ ] `TypeGuard`
* [x] type parameter syntax
* [x] type narrowing
* [ ] others
* [ ] `collections.abc`
* [x] `Collection`
* [x] `Container`
* [x] `Generator`
* [x] `Iterable`
* [x] `Iterator`
* [x] `Mapping`, `MutableMapping`
* [x] `Sequence`, `MutableSequence`
* [ ] others
* [x] `Union`
* [x] `Optional`
* [x] `Literal`
* [x] `Callable`
* [ ] `TypedDict`
* [ ] type variable (`TypeVar`, `Generic`)
* [ ] `Protocol`
* [ ] `Final`
* [ ] `Annotated`
* [ ] `TypeAlias`
* [ ] type guard (`TypeGuard`)
* [ ] others
* `collections.abc`
* [x] `Iterable`
* [x] `Iterator`
* [x] `Mapping`
* [x] `Sequence`
* [ ] others
* [x] type assertion (`typing.cast`)
* [x] type narrowing (`is`, `isinstance`)
* [x] `pyi` (stub) files support
* [x] glob pattern file check
* [x] type comment (`# type: ...`)
* [x] virtual environment support
* [x] package manager support
* [x] `pip`
* [x] `poetry`
* [x] `uv`
## Join us!
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.
* [Discord (Erg language)](https://discord.gg/kQBuaSUS46)
* [#pylyzer](https://discord.com/channels/1006946336433774742/1056815981168697354)
* [GitHub discussions](https://github.com/mtshiba/pylyzer/discussions)
---

View File

@ -3,9 +3,6 @@ if [[ "$PWD" == */pylyzer ]]; then
cd crates/py2erg
echo "publish py2erg ..."
cargo publish
cd ../pylyzer_core
echo "publish pylyzer_core ..."
cargo publish
cd ../../
cargo publish
if [ "$1" = "--cargo-only" ]; then

View File

@ -11,9 +11,6 @@ repository.workspace = true
[features]
debug = ["erg_compiler/debug", "erg_common/debug"]
japanese = ["erg_compiler/japanese", "erg_common/japanese"]
simplified_chinese = ["erg_compiler/simplified_chinese", "erg_common/simplified_chinese"]
traditional_chinese = ["erg_compiler/traditional_chinese", "erg_common/traditional_chinese"]
[dependencies]
rustpython-parser = { workspace = true }

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,11 @@
use std::fs::{create_dir_all, File};
use std::fs::File;
use std::io::{BufWriter, Write};
use std::path::Path;
use erg_common::pathutil::{mod_name, NormalizedPathBuf};
use erg_common::set::Set;
use erg_common::io::Input;
use erg_common::log;
use erg_common::traits::LimitedDisplay;
use erg_common::{log, Str};
use erg_compiler::build_package::{CheckStatus, PylyzerStatus};
use erg_compiler::hir::{ClassDef, Expr, HIR};
use erg_compiler::module::SharedModuleCache;
use erg_compiler::context::register::{CheckStatus, PylyzerStatus};
use erg_compiler::hir::{Expr, HIR};
use erg_compiler::ty::value::{GenTypeObj, TypeObj};
use erg_compiler::ty::{HasType, Type};
@ -16,41 +13,41 @@ pub struct DeclFile {
pub filename: String,
pub code: String,
}
fn escape_type(typ: String) -> String {
typ.replace('%', "Type_").replace("<module>", "")
}
pub struct DeclFileGenerator {
filename: String,
namespace: String,
imported: Set<Str>,
code: String,
}
impl DeclFileGenerator {
pub fn new(path: &NormalizedPathBuf, status: CheckStatus) -> std::io::Result<Self> {
pub fn new(input: &Input, status: CheckStatus) -> Self {
let (timestamp, hash) = {
let metadata = std::fs::metadata(path)?;
let py_file_path = input.path();
let metadata = std::fs::metadata(py_file_path).unwrap();
let dummy_hash = metadata.len();
(metadata.modified()?, dummy_hash)
(metadata.modified().unwrap(), dummy_hash)
};
let status = PylyzerStatus {
status,
file: path.to_path_buf(),
file: input.path().into(),
timestamp,
hash,
};
let code = format!("{status}\n");
Ok(Self {
filename: path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.replace(".py", ".d.er"),
Self {
filename: input.filename().replace(".py", ".d.er"),
namespace: "".to_string(),
imported: Set::new(),
code,
})
}
}
pub fn gen_decl_er(mut self, hir: &HIR) -> DeclFile {
for chunk in hir.module.iter() {
pub fn gen_decl_er(mut self, hir: HIR) -> DeclFile {
for chunk in hir.module.into_iter() {
self.gen_chunk_decl(chunk);
}
log!("code:\n{}", self.code);
@ -60,35 +57,7 @@ impl DeclFileGenerator {
}
}
fn escape_type(&self, typ: String) -> String {
typ.replace('%', "Type_")
.replace("<module>", "")
.replace('/', ".")
.trim_start_matches(self.filename.trim_end_matches(".d.er"))
.trim_start_matches(&self.namespace)
.to_string()
}
// e.g. `x: foo.Bar` => `foo = pyimport "foo"; x: foo.Bar`
fn prepare_using_type(&mut self, typ: &Type) {
let namespace = Str::rc(
typ.namespace()
.split('/')
.next()
.unwrap()
.split('.')
.next()
.unwrap(),
);
if namespace != self.namespace
&& !namespace.is_empty()
&& self.imported.insert(namespace.clone())
{
self.code += &format!("{namespace} = pyimport \"{namespace}\"\n");
}
}
fn gen_chunk_decl(&mut self, chunk: &Expr) {
fn gen_chunk_decl(&mut self, chunk: Expr) {
match chunk {
Expr::Def(def) => {
let mut name = def
@ -96,29 +65,19 @@ impl DeclFileGenerator {
.ident()
.inspect()
.replace('\0', "")
.replace(['%', '*'], "___");
.replace('%', "___");
let ref_t = def.sig.ident().ref_t();
self.prepare_using_type(ref_t);
let typ = self.escape_type(ref_t.replace_failure().to_string_unabbreviated());
let typ = ref_t.replace_failure().to_string_unabbreviated();
let typ = escape_type(typ);
// Erg can automatically import nested modules
// `import http.client` => `http = pyimport "http"`
let decl = if ref_t.is_py_module() && ref_t.typarams()[0].is_str_value() {
let decl = if ref_t.is_py_module() {
name = name.split('.').next().unwrap().to_string();
let full_path_str = ref_t.typarams()[0].to_string_unabbreviated();
let mod_name = mod_name(Path::new(full_path_str.trim_matches('"')));
let imported = if self.imported.insert(mod_name.clone()) {
format!("{}.{mod_name} = pyimport \"{mod_name}\"", self.namespace)
} else {
"".to_string()
};
if self.imported.insert(name.clone().into()) {
format!(
"{}.{name} = pyimport \"{mod_name}\"\n{imported}",
self.namespace,
)
} else {
imported
}
format!(
"{}.{name} = pyimport {}",
self.namespace,
ref_t.typarams()[0]
)
} else {
format!("{}.{name}: {typ}", self.namespace)
};
@ -130,21 +89,15 @@ impl DeclFileGenerator {
.ident()
.inspect()
.replace('\0', "")
.replace(['%', '*'], "___");
.replace('%', "___");
let src = format!("{}.{class_name}", self.namespace);
let stash = std::mem::replace(&mut self.namespace, src);
let decl = format!(".{class_name}: ClassType");
self.code += &decl;
self.code.push('\n');
if let GenTypeObj::Subclass(class) = def.obj.as_ref() {
let sup = class
.sup
.as_ref()
.typ()
.replace_failure()
.to_string_unabbreviated();
self.prepare_using_type(class.sup.typ());
let sup = self.escape_type(sup);
if let GenTypeObj::Subclass(class) = &def.obj {
let sup = class.sup.as_ref().typ().to_string_unabbreviated();
let sup = escape_type(sup);
let decl = format!(".{class_name} <: {sup}\n");
self.code += &decl;
}
@ -154,8 +107,7 @@ impl DeclFileGenerator {
}) = def.obj.base_or_sup()
{
for (attr, t) in rec.iter() {
self.prepare_using_type(t);
let typ = self.escape_type(t.replace_failure().to_string_unabbreviated());
let typ = escape_type(t.to_string_unabbreviated());
let decl = format!("{}.{}: {typ}\n", self.namespace, attr.symbol);
self.code += &decl;
}
@ -166,66 +118,52 @@ impl DeclFileGenerator {
}) = def.obj.additional()
{
for (attr, t) in rec.iter() {
self.prepare_using_type(t);
let typ = self.escape_type(t.replace_failure().to_string_unabbreviated());
let typ = escape_type(t.to_string_unabbreviated());
let decl = format!("{}.{}: {typ}\n", self.namespace, attr.symbol);
self.code += &decl;
}
}
for attr in ClassDef::get_all_methods(&def.methods_list) {
for attr in def.methods.into_iter() {
self.gen_chunk_decl(attr);
}
self.namespace = stash;
}
Expr::Dummy(dummy) => {
for chunk in dummy.iter() {
for chunk in dummy.into_iter() {
self.gen_chunk_decl(chunk);
}
}
Expr::Compound(compound) => {
for chunk in compound.iter() {
self.gen_chunk_decl(chunk);
}
}
Expr::Call(call) if call.control_kind().is_some() => {
for arg in call.args.iter() {
self.gen_chunk_decl(arg);
}
}
Expr::Lambda(lambda) => {
for arg in lambda.body.iter() {
self.gen_chunk_decl(arg);
}
}
_ => {}
}
self.code.push('\n');
}
}
fn dump_decl_er(path: &NormalizedPathBuf, hir: &HIR, status: CheckStatus) -> std::io::Result<()> {
let decl_gen = DeclFileGenerator::new(path, status)?;
let file = decl_gen.gen_decl_er(hir);
let Some(dir) = path.parent().and_then(|p| p.canonicalize().ok()) else {
return Ok(());
};
let cache_dir = dir.join("__pycache__");
if !cache_dir.exists() {
let _ = create_dir_all(&cache_dir);
pub fn reserve_decl_er(input: Input) {
let mut dir = input.dir();
dir.push("__pycache__");
let pycache_dir = dir.as_path();
if !pycache_dir.exists() {
std::fs::create_dir(pycache_dir).unwrap();
}
let path = cache_dir.join(file.filename);
let filename = input.filename();
let mut path = pycache_dir.join(filename);
path.set_extension("d.er");
if !path.exists() {
File::create(&path)?;
let _f = File::create(path).unwrap();
}
let f = File::options().write(true).open(path)?;
let mut f = BufWriter::new(f);
f.write_all(file.code.as_bytes())
}
pub fn dump_decl_package(modules: &SharedModuleCache) {
for (path, module) in modules.raw_iter() {
if let Some(hir) = module.hir.as_ref() {
let _ = dump_decl_er(path, hir, module.status);
}
}
pub fn dump_decl_er(input: Input, hir: HIR, status: CheckStatus) {
let decl_gen = DeclFileGenerator::new(&input, status);
let file = decl_gen.gen_decl_er(hir);
let mut dir = input.dir();
dir.push("__pycache__");
let pycache_dir = dir.as_path();
let f = File::options()
.write(true)
.open(pycache_dir.join(file.filename))
.unwrap();
let mut f = BufWriter::new(f);
f.write_all(file.code.as_bytes()).unwrap();
}

View File

@ -1,26 +0,0 @@
[package]
name = "pylyzer_core"
description = "pylyzer core"
version.workspace = true
authors.workspace = true
license.workspace = true
edition.workspace = true
repository.workspace = true
[features]
debug = ["erg_compiler/debug", "erg_common/debug", "py2erg/debug"]
large_thread = ["erg_compiler/large_thread", "erg_common/large_thread"]
pretty = ["erg_compiler/pretty", "erg_common/pretty"]
backtrace = ["erg_common/backtrace"]
experimental = ["erg_compiler/experimental", "erg_common/experimental", "parallel"]
parallel = ["erg_compiler/parallel", "erg_common/parallel"]
[dependencies]
erg_common = { workspace = true }
erg_compiler = { workspace = true }
rustpython-parser = { workspace = true }
rustpython-ast = { workspace = true }
py2erg = { version = "0.0.82", path = "../py2erg" }
[lib]
path = "lib.rs"

View File

@ -1,100 +0,0 @@
use std::path::{Path, PathBuf};
use erg_common::error::ErrorKind;
use erg_common::log;
use erg_common::style::{remove_style, StyledStr};
// use erg_common::style::{remove_style, StyledString, Color};
use erg_compiler::context::ModuleContext;
use erg_compiler::error::{CompileError, CompileErrors};
fn project_root(path: &Path) -> Option<PathBuf> {
let mut parent = path.to_path_buf();
while parent.pop() {
if parent.join("pyproject.toml").exists() || parent.join(".git").exists() {
let path = if parent == Path::new("") {
PathBuf::from(".")
} else {
parent
};
return path.canonicalize().ok();
}
}
None
}
pub(crate) fn filter_errors(ctx: &ModuleContext, errors: CompileErrors) -> CompileErrors {
let root = project_root(ctx.get_top_cfg().input.path());
errors
.into_iter()
.filter_map(|error| filter_error(root.as_deref(), ctx, error))
.collect()
}
fn handle_name_error(error: CompileError) -> Option<CompileError> {
let main = &error.core.main_message;
if main.contains("is already declared")
|| main.contains("cannot be assigned more than once")
|| {
main.contains(" is not defined") && {
let name = StyledStr::destyle(main.trim_end_matches(" is not defined"));
name == "Any"
|| error
.core
.get_hint()
.is_some_and(|hint| hint.contains(name))
}
}
{
None
} else {
Some(error)
}
}
fn filter_error(
root: Option<&Path>,
ctx: &ModuleContext,
mut error: CompileError,
) -> Option<CompileError> {
if ctx.get_top_cfg().do_not_show_ext_errors
&& error.input.path() != Path::new("<string>")
&& root.is_some_and(|root| {
error
.input
.path()
.canonicalize()
.is_ok_and(|path| path.starts_with(root.join(".venv")) || !path.starts_with(root))
})
{
return None;
}
match error.core.kind {
ErrorKind::FeatureError => {
log!(err "this error is ignored:");
log!(err "{error}");
None
}
ErrorKind::InheritanceError => None,
ErrorKind::VisibilityError => None,
// exclude doc strings
ErrorKind::UnusedWarning => {
let code = error.input.reread_lines(
error.core.loc.ln_begin().unwrap_or(1) as usize,
error.core.loc.ln_end().unwrap_or(1) as usize,
);
if code[0].trim().starts_with("\"\"\"") {
None
} else {
for sub in error.core.sub_messages.iter_mut() {
if let Some(hint) = &mut sub.hint {
*hint = remove_style(hint);
*hint = hint.replace("use discard function", "bind to `_` (`_ = ...`)");
}
}
Some(error)
}
}
ErrorKind::NameError | ErrorKind::AssignError => handle_name_error(error),
_ => Some(error),
}
}

View File

@ -1,4 +0,0 @@
mod analyze;
mod handle_err;
pub use analyze::{PythonAnalyzer, SimplePythonParser};

View File

@ -1,19 +0,0 @@
[package]
name = "pylyzer_wasm"
description = "Wasm wrapper for pylyzer"
version.workspace = true
authors.workspace = true
license.workspace = true
edition.workspace = true
repository.workspace = true
publish = false
[dependencies]
wasm-bindgen = "0.2"
erg_common = { workspace = true }
erg_compiler = { workspace = true }
pylyzer_core = { version = "*", path = "../pylyzer_core" }
[lib]
crate-type = ["cdylib", "rlib"]
path = "lib.rs"

View File

@ -1,13 +0,0 @@
# pylyzer_wasm
Wasm wrapper for pylyzer.
## Usage
```ts
import { Analyzer } from 'pylyzer_wasm';
const analyzer = new Analyzer();
const errors = analyzer.check('print("Hello, World!")');
const locals = analyzer.dir();
```

View File

@ -1,262 +0,0 @@
use wasm_bindgen::prelude::*;
use erg_common::error::ErrorCore;
use erg_common::error::Location as Loc;
use erg_common::traits::{Runnable, Stream};
use erg_compiler::context::ContextProvider;
use erg_compiler::erg_parser::ast::VarName;
use erg_compiler::error::CompileError;
use erg_compiler::ty::Type as Ty;
use erg_compiler::varinfo::VarInfo;
use pylyzer_core::PythonAnalyzer;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[wasm_bindgen]
pub enum CompItemKind {
Method = 0,
Function = 1,
Constructor = 2,
Field = 3,
Variable = 4,
Class = 5,
Struct = 6,
Interface = 7,
Module = 8,
Property = 9,
Event = 10,
Operator = 11,
Unit = 12,
Value = 13,
Constant = 14,
Enum = 15,
EnumMember = 16,
Keyword = 17,
Text = 18,
Color = 19,
File = 20,
Reference = 21,
Customcolor = 22,
Folder = 23,
TypeParameter = 24,
User = 25,
Issue = 26,
Snippet = 27,
}
#[derive(Debug, Clone)]
#[wasm_bindgen]
pub struct Location(Loc);
impl From<Loc> for Location {
fn from(loc: Loc) -> Self {
Self(loc)
}
}
impl Location {
pub const UNKNOWN: Location = Location(Loc::Unknown);
}
#[derive(Debug, Clone)]
#[wasm_bindgen]
#[allow(dead_code)]
pub struct Type(Ty);
#[derive(Debug, Clone)]
#[wasm_bindgen]
pub struct VarEntry {
name: VarName,
vi: VarInfo,
}
impl VarEntry {
pub fn new(name: VarName, vi: VarInfo) -> Self {
Self { name, vi }
}
}
#[wasm_bindgen]
impl VarEntry {
pub fn name(&self) -> String {
self.name.to_string()
}
pub fn item_kind(&self) -> CompItemKind {
match &self.vi.t {
Ty::Callable { .. } => CompItemKind::Function,
Ty::Subr(subr) => {
if subr.self_t().is_some() {
CompItemKind::Method
} else {
CompItemKind::Function
}
}
Ty::Quantified(quant) => match quant.as_ref() {
Ty::Callable { .. } => CompItemKind::Function,
Ty::Subr(subr) => {
if subr.self_t().is_some() {
CompItemKind::Method
} else {
CompItemKind::Function
}
}
_ => unreachable!(),
},
Ty::ClassType => CompItemKind::Class,
Ty::TraitType => CompItemKind::Interface,
Ty::Poly { name, .. } if &name[..] == "Module" => CompItemKind::Module,
_ if self.vi.muty.is_const() => CompItemKind::Constant,
_ => CompItemKind::Variable,
}
}
pub fn typ(&self) -> String {
self.vi.t.to_string()
}
}
#[wasm_bindgen]
impl Location {
pub fn ln_begin(&self) -> Option<u32> {
self.0.ln_begin()
}
pub fn ln_end(&self) -> Option<u32> {
self.0.ln_end()
}
pub fn col_begin(&self) -> Option<u32> {
self.0.col_begin()
}
pub fn col_end(&self) -> Option<u32> {
self.0.col_end()
}
}
#[derive(Debug, Clone)]
#[wasm_bindgen(getter_with_clone)]
pub struct Error {
pub errno: usize,
pub is_warning: bool,
// pub kind: ErrorKind,
pub loc: Location,
pub desc: String,
pub hint: Option<String>,
}
fn find_fallback_loc(err: &ErrorCore) -> Loc {
if err.loc == Loc::Unknown {
for sub in &err.sub_messages {
if sub.loc != Loc::Unknown {
return sub.loc;
}
}
Loc::Unknown
} else {
err.loc
}
}
impl From<CompileError> for Error {
fn from(err: CompileError) -> Self {
let loc = Location(find_fallback_loc(&err.core));
let sub_msg = err
.core
.sub_messages
.first()
.map(|sub| {
sub.msg
.iter()
.fold("\n".to_string(), |acc, s| acc + s + "\n")
})
.unwrap_or_default();
let desc = err.core.main_message + &sub_msg;
Self {
errno: err.core.errno,
is_warning: err.core.kind.is_warning(),
// kind: err.kind(),
loc,
desc,
hint: err
.core
.sub_messages
.first()
.and_then(|sub| sub.hint.clone()),
}
}
}
impl Error {
pub const fn new(
errno: usize,
is_warning: bool,
loc: Location,
desc: String,
hint: Option<String>,
) -> Self {
Self {
errno,
is_warning,
loc,
desc,
hint,
}
}
}
#[wasm_bindgen]
// #[derive()]
pub struct Analyzer {
analyzer: PythonAnalyzer,
}
impl Default for Analyzer {
fn default() -> Self {
Self::new()
}
}
#[wasm_bindgen]
impl Analyzer {
pub fn new() -> Self {
Analyzer {
analyzer: PythonAnalyzer::default(),
}
}
pub fn clear(&mut self) {
self.analyzer.clear();
}
pub fn start_message(&self) -> String {
self.analyzer.start_message()
}
pub fn dir(&mut self) -> Box<[VarEntry]> {
self.analyzer
.dir()
.into_iter()
.map(|(n, vi)| VarEntry::new(n.clone(), vi.clone()))
.collect::<Vec<_>>()
.into_boxed_slice()
}
pub fn check(&mut self, input: &str) -> Box<[Error]> {
match self.analyzer.analyze(input.to_string(), "exec") {
Ok(artifact) => artifact
.warns
.into_iter()
.map(Error::from)
.collect::<Vec<_>>()
.into_boxed_slice(),
Err(mut err_artifact) => {
err_artifact.errors.extend(err_artifact.warns);
let errs = err_artifact
.errors
.into_iter()
.map(Error::from)
.collect::<Vec<_>>();
errs.into_boxed_slice()
}
}
}
}

View File

@ -1,75 +0,0 @@
# command line options
## --server
Launch as a language server.
## --clear-cache
Clear the cache files.
## --dump-decl
Dump a type declarations file (d.er) after type checking.
```bash
$ pylyzer --dump-decl test.py
Start checking: test.py
All checks OK: test.py
$ ls
test.py test.d.er
```
## -c/--code
Check code from the command line.
```bash
$ pylyzer -c "print('hello world')"
Start checking: string
All checks OK: string
```
## --disable
Disable a default LSP feature.
Default (disableable) features are:
* codeAction
* codeLens
* completion
* diagnostics
* findReferences
* gotoDefinition
* hover
* inlayHint
* rename
* semanticTokens
* signatureHelp
* documentLink
## --verbose
Print process information verbosely.
## --no-infer-fn-type
When a function type is not specified, no type inference is performed and the function type is assumed to be `Any`.
## --fast-error-report
Simplify error reporting by eliminating to search for similar variables when a variable does not exist.
## --hurry
Enable `--no-infer-fn-type` and `--fast-error-report`.
## --do-not-show-ext-errors
Do not show errors from external libraries.
## --do-not-respect-pyi
If specified, the actual `.py` types will be respected over the `.pyi` types.
Applying this option may slow down the analysis.

View File

@ -1,10 +0,0 @@
# `pyproject.toml` options
## `tool.pylyzer.python.path`
Path to the Python interpreter to use. If not set, the default Python interpreter will be used.
```toml
[tool.pylyzer.python]
path = "path/to/python"
```

28
docs/source/options.md Normal file
View File

@ -0,0 +1,28 @@
# command line options
## --server
Launch as a language server.
## --dump-decl
Dump a type declarations file (d.er) after type checking.
```bash
$ pylyzer --dump-decl test.py
Start checking: test.py
All checks OK: test.py
$ ls
test.py test.d.er
```
## -c/--code
Check code from the command line.
```bash
$ pylyzer -c "print('hello world')"
Start checking: string
All checks OK: string
```

View File

@ -1,3 +1,3 @@
{
"recommendations": ["amodio.tsl-problem-matcher", "biomejs.biome"]
"recommendations": ["amodio.tsl-problem-matcher", "rome.rome"]
}

View File

@ -9,4 +9,4 @@ src/**
**/*.ts
.gitignore
webpack.config.js
biome.json
rome.json

View File

@ -36,12 +36,4 @@ cargo install pylyzer
| - | - | - |
| pylyzer.diagnostics | Enable diagnostics | true |
| pylyzer.inlayHints | Enable inlay hints (this feature is unstable) | false |
| pylyzer.semanticTokens | Enable semantic tokens | false |
| pylyzer.hover | Enable hover | true |
| pylyzer.completion | Enable completion | true |
| pylyzer.smartCompletion | Enable smart completion (see [ELS features](https://github.com/erg-lang/erg/blob/main/crates/els/doc/features.md))| true |
| pylyzer.deepCompletion | Enable deep completion | true |
| pylyzer.signatureHelp | Enable signature help | true |
| pylyzer.documentLink | Enable document link | true |
| pylyzer.codeAction | Enable code action | true |
| pylyzer.codeLens | Enable code lens | true |

View File

@ -1,31 +0,0 @@
{
"$schema": "https://biomejs.dev/schemas/1.8.2/schema.json",
"organizeImports": {
"enabled": true
},
"formatter": {
"enabled": true,
"indentStyle": "tab",
"lineWidth": 120,
"ignore": ["dist/**", "out/**", ".vscode-test/**", ".vscode/**"]
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
},
"ignore": ["dist/**", "out/**", ".vscode-test/**", ".vscode/**"]
},
"javascript": {
"formatter": {
"indentStyle": "tab",
"lineWidth": 120
}
},
"json": {
"formatter": {
"indentStyle": "tab",
"lineWidth": 120
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,136 +1,103 @@
{
"name": "pylyzer",
"displayName": "pylyzer",
"description": "A fast Python static code analyzer & language server for VSCode",
"publisher": "pylyzer",
"version": "0.1.11",
"engines": {
"vscode": "^1.70.0"
},
"categories": ["Programming Languages", "Linters"],
"repository": {
"type": "git",
"url": "https://github.com/mtshiba/pylyzer.git"
},
"icon": "images/pylyzer-logo.png",
"main": "./dist/extension.js",
"activationEvents": [
"workspaceContains:pyproject.toml",
"workspaceContains:*/pyproject.toml",
"onLanguage:python"
],
"contributes": {
"commands": [
{
"title": "Restart the pylyzer language server",
"category": "python",
"command": "pylyzer.restartLanguageServer"
}
],
"languages": [
{
"id": "python",
"aliases": ["Python", "python"],
"extensions": [".py", ".pyi"]
}
],
"configuration": {
"type": "object",
"title": "pylyzer",
"properties": {
"pylyzer.diagnostics": {
"type": "boolean",
"default": true,
"markdownDescription": "Enable diagnostics"
},
"pylyzer.inlayHints": {
"type": "boolean",
"default": false,
"markdownDescription": "Enable inlay hints (this feature is unstable)"
},
"pylyzer.semanticTokens": {
"type": "boolean",
"default": false,
"markdownDescription": "Enable semantic tokens (this feature is unstable)"
},
"pylyzer.hover": {
"type": "boolean",
"default": true,
"markdownDescription": "Enable hover"
},
"pylyzer.completion": {
"type": "boolean",
"default": true,
"markdownDescription": "Enable completion"
},
"pylyzer.smartCompletion": {
"type": "boolean",
"default": true,
"markdownDescription": "Enable smart completion (see [ELS features](https://github.com/erg-lang/erg/blob/main/crates/els/doc/features.md))"
},
"pylyzer.deepCompletion": {
"type": "boolean",
"default": true,
"markdownDescription": "Enable deep completion (see [ELS features](https://github.com/erg-lang/erg/blob/main/crates/els/doc/features.md))"
},
"pylyzer.signatureHelp": {
"type": "boolean",
"default": true,
"markdownDescription": "Enable signature help"
},
"pylyzer.documentLink": {
"type": "boolean",
"default": true,
"markdownDescription": "Enable document link"
},
"pylyzer.codeAction": {
"type": "boolean",
"default": true,
"markdownDescription": "Enable code action"
},
"pylyzer.codeLens": {
"type": "boolean",
"default": true,
"markdownDescription": "Enable code lens"
}
}
}
},
"scripts": {
"vscode:publish": "vsce publish",
"vscode:prepublish": "npm run package",
"vscode:package": "vsce package",
"compile": "webpack",
"watch": "webpack --watch",
"package": "webpack --mode production --devtool hidden-source-map",
"compile-tests": "tsc -p . --outDir out",
"watch-tests": "tsc -p . -w --outDir out",
"pretest": "npm run compile-tests && npm run compile && npm run lint",
"test": "node ./out/test/runTest.js",
"type-check": "tsc --noEmit",
"lint": "biome lint .",
"format": "biome format .",
"lint:fix-suggested": "biome check --write .",
"format:fix": "biome format --write ."
},
"dependencies": {
"vscode-languageclient": "^8.0.2"
},
"devDependencies": {
"@biomejs/biome": "^1.8.2",
"@types/glob": "^8.0.0",
"@types/mocha": "^10.0.1",
"@types/node": "18.x",
"@types/vscode": "^1.70.0",
"@vscode/test-electron": "^2.2.1",
"glob": "^8.0.3",
"mocha": "^10.2.0",
"ts-loader": "^9.4.2",
"typescript": "^4.9.4",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.1"
},
"lint-staged": {
"*": "biome format --write"
}
"name": "pylyzer",
"displayName": "pylyzer",
"description": "A fast Python static code analyzer & language server for VSCode",
"publisher": "pylyzer",
"version": "0.1.7",
"engines": {
"vscode": "^1.70.0"
},
"categories": [
"Programming Languages",
"Linters"
],
"repository": {
"type": "git",
"url": "https://github.com/mtshiba/pylyzer.git"
},
"icon": "images/pylyzer-logo.png",
"main": "./dist/extension.js",
"activationEvents": [
"onLanguage:python"
],
"contributes": {
"commands": [
{
"title": "Restart the pylyzer language server",
"category": "python",
"command": "pylyzer.restartLanguageServer"
}
],
"languages": [
{
"id": "python",
"aliases": [
"Python",
"python"
],
"extensions": [
".py"
]
}
],
"configuration": {
"type": "object",
"title": "pylyzer",
"properties": {
"pylyzer.diagnostics": {
"type": "boolean",
"default": true,
"markdownDescription": "Enable diagnostics"
},
"pylyzer.inlayHints": {
"type": "boolean",
"default": false,
"markdownDescription": "Enable inlay hints (this feature is unstable)"
},
"pylyzer.smartCompletion": {
"type": "boolean",
"default": true,
"markdownDescription": "Enable smart completion (see [ELS features](https://github.com/erg-lang/erg/blob/main/crates/els/doc/features.md))"
}
}
}
},
"scripts": {
"vscode:publish": "vsce publish",
"vscode:prepublish": "npm run package",
"vscode:package": "vsce package",
"compile": "webpack",
"watch": "webpack --watch",
"package": "webpack --mode production --devtool hidden-source-map",
"compile-tests": "tsc -p . --outDir out",
"watch-tests": "tsc -p . -w --outDir out",
"pretest": "npm run compile-tests && npm run compile && npm run lint",
"test": "node ./out/test/runTest.js",
"type-check": "tsc --noEmit",
"lint": "rome check .",
"format": "rome format .",
"lint:fix": "rome check --apply .",
"lint:fix-suggested": "rome check --apply-suggested .",
"format:fix": "rome format --write ."
},
"dependencies": {
"vscode-languageclient": "^8.0.2"
},
"devDependencies": {
"@types/glob": "^8.0.0",
"@types/mocha": "^10.0.1",
"@types/node": "18.x",
"@types/vscode": "^1.70.0",
"@vscode/test-electron": "^2.2.1",
"glob": "^8.0.3",
"mocha": "^10.2.0",
"rome": "^10.0.1",
"ts-loader": "^9.4.2",
"typescript": "^4.9.4",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.1"
},
"lint-staged": {
"*": "rome format --write"
}
}

11
extension/rome.json Normal file
View File

@ -0,0 +1,11 @@
{
"linter": {
"rules": {
"recommended": true
},
"ignore": ["/dist/", "/out/", "/.vscode-test/"]
},
"formatter": {
"ignore": ["/dist/", "/out/", "/.vscode-test/"]
}
}

View File

@ -1,19 +1,19 @@
import { Uri, commands } from "vscode";
// copied and modified from https://github.com/rust-lang/rust-analyzer/blob/27239fbb58a115915ffc1ce65ededc951eb00fd2/editors/code/src/commands.ts
import type { LanguageClient, Location, Position } from "vscode-languageclient/node";
import { LanguageClient, Location, Position } from 'vscode-languageclient/node';
import { Uri, commands } from 'vscode';
export async function showReferences(
client: LanguageClient | undefined,
uri: string,
position: Position,
locations: Location[],
client: LanguageClient | undefined,
uri: string,
position: Position,
locations: Location[]
) {
if (client) {
await commands.executeCommand(
"editor.action.showReferences",
Uri.parse(uri),
client.protocol2CodeConverter.asPosition(position),
locations.map(client.protocol2CodeConverter.asLocation),
);
}
if (client) {
await commands.executeCommand(
"editor.action.showReferences",
Uri.parse(uri),
client.protocol2CodeConverter.asPosition(position),
locations.map(client.protocol2CodeConverter.asLocation)
);
}
}

View File

@ -1,6 +1,9 @@
import { type ExtensionContext, commands, window, workspace } from "vscode";
import { LanguageClient, type LanguageClientOptions, type ServerOptions } from "vscode-languageclient/node";
import fs from "node:fs";
import { ExtensionContext, commands, window, workspace } from "vscode";
import {
LanguageClient,
LanguageClientOptions,
ServerOptions,
} from "vscode-languageclient/node";
import { showReferences } from "./commands";
let client: LanguageClient | undefined;
@ -8,25 +11,16 @@ let client: LanguageClient | undefined;
async function startLanguageClient(context: ExtensionContext) {
try {
const executablePath = (() => {
const fp = workspace.workspaceFolders?.at(0)?.uri.fsPath;
const venvExecutablePath = `${fp}/.venv/bin/pylyzer`;
if (fs.existsSync(venvExecutablePath)) {
return venvExecutablePath;
}
const executablePath = workspace.getConfiguration("pylyzer").get<string>("executablePath", "");
const executablePath = workspace
.getConfiguration("pylyzer")
.get<string>("executablePath", "");
return executablePath === "" ? "pylyzer" : executablePath;
})();
const enableDiagnostics = workspace.getConfiguration("pylyzer").get<boolean>("diagnostics", true);
const enableInlayHints = workspace.getConfiguration("pylyzer").get<boolean>("inlayHints", false);
const enableSemanticTokens = workspace.getConfiguration("pylyzer").get<boolean>("semanticTokens", false);
const enableSemanticTokens = workspace.getConfiguration("pylyzer").get<boolean>("semanticTokens", true);
const enableHover = workspace.getConfiguration("pylyzer").get<boolean>("hover", true);
const enableCompletion = workspace.getConfiguration("pylyzer").get<boolean>("completion", true);
const smartCompletion = workspace.getConfiguration("pylyzer").get<boolean>("smartCompletion", true);
const deepCompletion = workspace.getConfiguration("pylyzer").get<boolean>("deepCompletion", true);
const enableSignatureHelp = workspace.getConfiguration("pylyzer").get<boolean>("signatureHelp", true);
const enableDocumentLink = workspace.getConfiguration("pylyzer").get<boolean>("documentLink", true);
const enableCodeAction = workspace.getConfiguration("pylyzer").get<boolean>("codeAction", true);
const enableCodeLens = workspace.getConfiguration("pylyzer").get<boolean>("codeLens", true);
/* optional features */
const checkOnType = workspace.getConfiguration("pylyzer").get<boolean>("checkOnType", false);
const args = ["--server"];
@ -47,34 +41,10 @@ async function startLanguageClient(context: ExtensionContext) {
args.push("--disable");
args.push("hover");
}
if (!enableCompletion) {
args.push("--disable");
args.push("completion");
}
if (!smartCompletion) {
args.push("--disable");
args.push("smartCompletion");
}
if (!deepCompletion) {
args.push("--disable");
args.push("deepCompletion");
}
if (!enableSignatureHelp) {
args.push("--disable");
args.push("signatureHelp");
}
if (!enableDocumentLink) {
args.push("--disable");
args.push("documentLink");
}
if (!enableCodeAction) {
args.push("--disable");
args.push("codeAction");
}
if (!enableCodeLens) {
args.push("--disable");
args.push("codeLens");
}
if (checkOnType) {
args.push("--enable");
args.push("checkOnType");
@ -114,11 +84,13 @@ async function restartLanguageClient() {
}
export async function activate(context: ExtensionContext) {
context.subscriptions.push(commands.registerCommand("pylyzer.restartLanguageServer", () => restartLanguageClient()));
context.subscriptions.push(
commands.registerCommand("pylyzer.restartLanguageServer", () => restartLanguageClient())
);
context.subscriptions.push(
commands.registerCommand("pylyzer.showReferences", async (uri, position, locations) => {
await showReferences(client, uri, position, locations);
}),
await showReferences(client, uri, position, locations)
})
);
await startLanguageClient(context);
}

View File

@ -1,4 +1,4 @@
import * as path from "node:path";
import * as path from "path";
import { runTests } from "@vscode/test-electron";

View File

@ -1,4 +1,4 @@
import * as assert from "node:assert";
import * as assert from "assert";
// You can import and use all API from the 'vscode' module
// as well as import your extension to test it

View File

@ -1,6 +1,6 @@
import * as path from "node:path";
import glob from "glob";
import * as path from "path";
import Mocha from "mocha";
import glob from "glob";
export function run(): Promise<void> {
// Create the mocha test
@ -18,9 +18,7 @@ export function run(): Promise<void> {
}
// Add files to the test suite
for (const f of files) {
mocha.addFile(path.resolve(testsRoot, f));
}
files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f)));
try {
// Run the mocha test

View File

@ -1,6 +1,8 @@
//@ts-check
const path = require("node:path");
"use strict";
const path = require("path");
//@ts-check
/** @typedef {import('webpack').Configuration} WebpackConfig **/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 415 KiB

After

Width:  |  Height:  |  Size: 279 KiB

View File

@ -1 +0,0 @@
cp README.md docs/index.md

View File

@ -1,11 +0,0 @@
os=$(uname -s)
if [ "$os" = "Darwin" ]; then
platform="macos"
elif [ "$os" = "Linux" ]; then
platform="linux"
else
echo "Unsupported platform: $os"
exit 1
fi
cibuildwheel --output-dir dist --platform $platform
twine upload -u mtshiba -p $PYPI_PASSWORD --skip-existing dist/*

View File

@ -1,2 +1,15 @@
[build-system]
requires = ["setuptools", "setuptools-rust", "wheel", "tomli"]
requires = ["maturin>=0.12"]
build-backend = "maturin"
[project]
name = "pylyzer"
description = "A fast static code analyzer & language server for Python"
requires-python = ">=3.7"
classifiers = [
"Development Status :: 2 - Pre-Alpha",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Rust",
"Topic :: Software Development :: Quality Assurance",
]

View File

@ -1,92 +0,0 @@
# To build the package, run `python -m build --wheel`.
from pathlib import Path
import os
import shlex
from glob import glob
import shutil
from setuptools import setup, Command
from setuptools_rust import RustBin
import tomli
try:
# setuptools >= 70.1.0
from setuptools.command.bdist_wheel import bdist_wheel
except ImportError:
from wheel.bdist_wheel import bdist_wheel
def removeprefix(string, prefix):
if string.startswith(prefix):
return string[len(prefix):]
else:
return string
class BdistWheel(bdist_wheel):
def get_tag(self):
_, _, plat = super().get_tag()
return "py3", "none", plat
class Clean(Command):
user_options = []
def initialize_options(self):
pass
def finalize_options(self):
pass
def run(self):
# super().run()
for d in ["build", "dist", "src/pylyzer.egg-info"]:
shutil.rmtree(d, ignore_errors=True)
with open("README.md", encoding="utf-8", errors="ignore") as fp:
long_description = fp.read()
with open("Cargo.toml", "rb") as fp:
toml = tomli.load(fp)
name = toml["package"]["name"]
description = toml["package"]["description"]
version = toml["workspace"]["package"]["version"]
license = toml["workspace"]["package"]["license"]
url = toml["workspace"]["package"]["repository"]
cargo_args = ["--no-default-features"]
home = os.path.expanduser("~")
file_and_dirs = glob(home + "/" + ".erg/lib/**", recursive=True)
paths = [Path(path) for path in file_and_dirs if os.path.isfile(path)]
files = [(removeprefix(str(path.parent), home), str(path)) for path in paths]
data_files = {}
for key, value in files:
if key in data_files:
data_files[key].append(value)
else:
data_files[key] = [value]
data_files = list(data_files.items())
setup(
name=name,
author="mtshiba",
author_email="sbym1346@gmail.com",
url=url,
description=description,
long_description=long_description,
long_description_content_type="text/markdown",
version=version,
license=license,
python_requires=">=3.7",
rust_extensions=[
RustBin("pylyzer", args=cargo_args, cargo_manifest_args=["--locked"])
],
cmdclass={
"clean": Clean,
"bdist_wheel": BdistWheel,
},
classifiers=[
"Development Status :: 2 - Pre-Alpha",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Rust",
"Topic :: Software Development :: Quality Assurance",
],
data_files=data_files,
)

View File

@ -2,163 +2,69 @@ use erg_common::config::ErgConfig;
use erg_common::error::{ErrorCore, ErrorKind, MultiErrorDisplay};
use erg_common::style::colors::{BLUE, GREEN, RED, YELLOW};
use erg_common::style::RESET;
use erg_common::traits::{ExitStatus, New, Runnable, Stream};
use erg_common::traits::{ExitStatus, Runnable, Stream};
use erg_common::Str;
use erg_compiler::artifact::{BuildRunnable, Buildable, CompleteArtifact, IncompleteArtifact};
use erg_compiler::build_package::GenericPackageBuilder;
use erg_compiler::context::{Context, ContextProvider, ModuleContext};
use erg_compiler::erg_parser::ast::{Module, VarName, AST};
use erg_compiler::erg_parser::build_ast::ASTBuildable;
use erg_compiler::context::register::CheckStatus;
use erg_compiler::context::ModuleContext;
use erg_compiler::erg_parser::ast::{Module, AST};
use erg_compiler::erg_parser::error::{
CompleteArtifact as ParseArtifact, IncompleteArtifact as IncompleteParseArtifact, ParseErrors,
ParserRunnerErrors,
};
use erg_compiler::erg_parser::parse::Parsable;
use erg_compiler::error::{CompileError, CompileErrors};
use erg_compiler::lower::ASTLowerer;
use erg_compiler::module::SharedCompilerResource;
use erg_compiler::varinfo::VarInfo;
use erg_compiler::GenericHIRBuilder;
use py2erg::{dump_decl_package, CommentStorage, ShadowingMode};
use rustpython_ast::source_code::{RandomLocator, SourceRange};
use py2erg::{dump_decl_er, reserve_decl_er, ShadowingMode};
use rustpython_ast::source_code::RandomLocator;
use rustpython_ast::{Fold, ModModule};
use rustpython_parser::{Parse, ParseErrorType};
use crate::handle_err;
#[derive(Debug, Default)]
pub struct SimplePythonParser {
cfg: ErgConfig,
}
pub struct SimplePythonParser {}
impl Parsable for SimplePythonParser {
fn parse(code: String) -> Result<ParseArtifact, IncompleteParseArtifact<Module, ParseErrors>> {
let mut slf = Self::new(ErgConfig::string(code.clone()));
slf.build_ast(code)
.map(|art| ParseArtifact::new(art.ast.module, art.warns.into()))
.map_err(|iart| {
IncompleteParseArtifact::new(
iart.ast.map(|art| art.module),
iart.errors.into(),
iart.warns.into(),
)
})
}
}
impl New for SimplePythonParser {
fn new(cfg: ErgConfig) -> Self {
Self { cfg }
}
}
impl ASTBuildable for SimplePythonParser {
fn build_ast(
&mut self,
code: String,
) -> Result<
ParseArtifact<AST, ParserRunnerErrors>,
IncompleteParseArtifact<AST, ParserRunnerErrors>,
> {
let filename = self.cfg.input.filename();
let mut comments = CommentStorage::new();
comments.read(&code);
let py_program = self.parse_py_code(code)?;
let py_program = ModModule::parse(&code, "<stdin>").map_err(|_err| ParseErrors::empty())?;
let mut locator = RandomLocator::new(&code);
// let mut locator = LinearLocator::new(&code);
let py_program = locator
.fold(py_program)
.map_err(|_err| ParseErrors::empty())?;
let shadowing = if cfg!(feature = "debug") {
ShadowingMode::Visible
} else {
ShadowingMode::Invisible
};
let converter = py2erg::ASTConverter::new(ErgConfig::default(), shadowing, comments);
let IncompleteArtifact {
object: Some(erg_module),
errors,
warns,
} = converter.convert_program(py_program)
else {
unreachable!()
};
let erg_ast = AST::new(erg_common::Str::rc(&filename), erg_module);
let converter = py2erg::ASTConverter::new(ErgConfig::default(), shadowing);
let IncompleteArtifact{ object: Some(erg_module), errors, warns } = converter.convert_program(py_program) else { unreachable!() };
if errors.is_empty() {
Ok(ParseArtifact::new(erg_ast, warns.into()))
Ok(ParseArtifact::new(erg_module, warns.into()))
} else {
Err(IncompleteParseArtifact::new(
Some(erg_ast),
errors.into(),
Some(erg_module),
warns.into(),
errors.into(),
))
}
}
}
impl SimplePythonParser {
pub fn parse_py_code(
&self,
code: String,
) -> Result<ModModule<SourceRange>, IncompleteParseArtifact<AST, ParserRunnerErrors>> {
let py_program = ModModule::parse(&code, "<stdin>").map_err(|err| {
let mut locator = RandomLocator::new(&code);
// let mut locator = LinearLocator::new(&py_code);
let err = locator.locate_error::<_, ParseErrorType>(err);
let msg = err.to_string();
let loc = err.location.unwrap_or_default();
let core = ErrorCore::new(
vec![],
msg,
0,
ErrorKind::SyntaxError,
erg_common::error::Location::range(
loc.row.get(),
loc.column.to_zero_indexed(),
loc.row.get(),
loc.column.to_zero_indexed(),
),
);
let err = CompileError::new(core, self.cfg.input.clone(), "".into());
IncompleteParseArtifact::new(
None,
ParserRunnerErrors::from(err),
ParserRunnerErrors::empty(),
)
})?;
let mut locator = RandomLocator::new(&code);
// let mut locator = LinearLocator::new(&code);
let module = locator
.fold(py_program)
.map_err(|_err| ParserRunnerErrors::empty())?;
Ok(module)
}
}
#[derive(Debug, Default)]
pub struct PythonAnalyzer {
pub cfg: ErgConfig,
checker: GenericPackageBuilder<SimplePythonParser, GenericHIRBuilder<SimplePythonParser>>,
}
impl New for PythonAnalyzer {
fn new(cfg: ErgConfig) -> Self {
let checker =
GenericPackageBuilder::new(cfg.clone(), SharedCompilerResource::new(cfg.clone()));
Self { checker, cfg }
}
}
impl ContextProvider for PythonAnalyzer {
fn dir(&self) -> erg_common::dict::Dict<&VarName, &VarInfo> {
self.checker.dir()
}
fn get_receiver_ctx(&self, receiver_name: &str) -> Option<&Context> {
self.checker.get_receiver_ctx(receiver_name)
}
fn get_var_info(&self, name: &str) -> Option<(&VarName, &VarInfo)> {
self.checker.get_var_info(name)
}
checker: ASTLowerer,
}
impl Runnable for PythonAnalyzer {
type Err = CompileError;
type Errs = CompileErrors;
const NAME: &'static str = "Python Analyzer";
fn new(cfg: ErgConfig) -> Self {
let checker = ASTLowerer::new(cfg.clone());
Self { checker, cfg }
}
#[inline]
fn cfg(&self) -> &ErgConfig {
&self.cfg
@ -189,31 +95,17 @@ impl Buildable for PythonAnalyzer {
let mod_name = Str::rc(&cfg.input.file_stem());
Self {
cfg: cfg.copy(),
checker: GenericPackageBuilder::new_with_cache(cfg, mod_name, shared),
}
}
fn inherit_with_name(cfg: ErgConfig, mod_name: Str, shared: SharedCompilerResource) -> Self {
Self {
cfg: cfg.copy(),
checker: GenericPackageBuilder::new_with_cache(cfg, mod_name, shared),
checker: ASTLowerer::new_with_cache(cfg, mod_name, shared),
}
}
fn build(&mut self, code: String, mode: &str) -> Result<CompleteArtifact, IncompleteArtifact> {
self.analyze(code, mode)
}
fn build_from_ast(
&mut self,
ast: AST,
mode: &str,
) -> Result<CompleteArtifact<erg_compiler::hir::HIR>, IncompleteArtifact<erg_compiler::hir::HIR>>
{
self.check(ast, CompileErrors::empty(), CompileErrors::empty(), mode)
}
fn pop_context(&mut self) -> Option<ModuleContext> {
self.checker.pop_context()
self.checker.pop_mod_ctx()
}
fn get_context(&self) -> Option<&ModuleContext> {
self.checker.get_context()
Some(self.checker.get_mod_ctx())
}
}
@ -221,22 +113,53 @@ impl BuildRunnable for PythonAnalyzer {}
impl PythonAnalyzer {
pub fn new(cfg: ErgConfig) -> Self {
New::new(cfg)
Runnable::new(cfg)
}
#[allow(clippy::result_large_err)]
fn check(
pub fn analyze(
&mut self,
erg_ast: AST,
mut errors: CompileErrors,
mut warns: CompileErrors,
py_code: String,
mode: &str,
) -> Result<CompleteArtifact, IncompleteArtifact> {
match self.checker.build_from_ast(erg_ast, mode) {
let filename = self.cfg.input.filename();
let py_program = ModModule::parse(&py_code, &filename).map_err(|err| {
let mut locator = RandomLocator::new(&py_code);
// let mut locator = LinearLocator::new(&py_code);
let err = locator.locate_error::<_, ParseErrorType>(err);
let msg = err.to_string();
let loc = err.location.unwrap();
let core = ErrorCore::new(
vec![],
msg,
0,
ErrorKind::SyntaxError,
erg_common::error::Location::range(
loc.row.get(),
loc.column.to_zero_indexed(),
loc.row.get(),
loc.column.to_zero_indexed(),
),
);
let err = CompileError::new(core, self.cfg.input.clone(), "".into());
IncompleteArtifact::new(None, CompileErrors::from(err), CompileErrors::empty())
})?;
let mut locator = RandomLocator::new(&py_code);
// let mut locator = LinearLocator::new(&py_code);
let py_program = locator.fold(py_program).unwrap();
let shadowing = if cfg!(feature = "debug") {
ShadowingMode::Visible
} else {
ShadowingMode::Invisible
};
let converter = py2erg::ASTConverter::new(self.cfg.copy(), shadowing);
let IncompleteArtifact{ object: Some(erg_module), mut errors, mut warns } = converter.convert_program(py_program) else { unreachable!() };
let erg_ast = AST::new(erg_common::Str::rc(&filename), erg_module);
erg_common::log!("AST:\n{erg_ast}");
match self.checker.lower(erg_ast, mode) {
Ok(mut artifact) => {
artifact.warns.extend(warns);
artifact.warns =
handle_err::filter_errors(self.get_context().unwrap(), artifact.warns);
handle_err::filter_errors(self.checker.get_mod_ctx(), artifact.warns);
if errors.is_empty() {
Ok(artifact)
} else {
@ -249,56 +172,18 @@ impl PythonAnalyzer {
}
Err(iart) => {
errors.extend(iart.errors);
let errors = handle_err::filter_errors(self.get_context().unwrap(), errors);
let errors = handle_err::filter_errors(self.checker.get_mod_ctx(), errors);
warns.extend(iart.warns);
let warns = handle_err::filter_errors(self.get_context().unwrap(), warns);
let warns = handle_err::filter_errors(self.checker.get_mod_ctx(), warns);
Err(IncompleteArtifact::new(iart.object, errors, warns))
}
}
}
#[allow(clippy::result_large_err)]
pub fn analyze(
&mut self,
py_code: String,
mode: &str,
) -> Result<CompleteArtifact, IncompleteArtifact> {
let filename = self.cfg.input.filename();
let parser = SimplePythonParser::new(self.cfg.copy());
let mut comments = CommentStorage::new();
comments.read(&py_code);
let py_program = parser
.parse_py_code(py_code)
.map_err(|iart| IncompleteArtifact::new(None, iart.errors.into(), iart.warns.into()))?;
let shadowing = if cfg!(feature = "debug") {
ShadowingMode::Visible
} else {
ShadowingMode::Invisible
};
let converter = py2erg::ASTConverter::new(self.cfg.copy(), shadowing, comments);
let IncompleteArtifact {
object: Some(erg_module),
errors,
warns,
} = converter.convert_program(py_program)
else {
unreachable!()
};
let erg_ast = AST::new(erg_common::Str::rc(&filename), erg_module);
erg_common::log!("AST:\n{erg_ast}");
let res = self.check(erg_ast, errors, warns, mode);
if self.cfg.mode.is_language_server() {
// mod_cache doesn't contains the current module
// we don't cache the current module's result for now
dump_decl_package(&self.checker.shared().mod_cache);
}
res
}
pub fn run(&mut self) -> i32 {
/*if self.cfg.dist_dir.is_some() {
pub fn run(&mut self) {
if self.cfg.dist_dir.is_some() {
reserve_decl_er(self.cfg.input.clone());
}*/
}
let py_code = self.cfg.input.read();
let filename = self.cfg.input.filename();
println!("{BLUE}Start checking{RESET}: {filename}");
@ -314,10 +199,14 @@ impl PythonAnalyzer {
}
println!("{GREEN}All checks OK{RESET}: {}", self.cfg.input.filename());
if self.cfg.dist_dir.is_some() {
dump_decl_package(&self.checker.shared().mod_cache);
dump_decl_er(
self.cfg.input.clone(),
artifact.object,
CheckStatus::Succeed,
);
println!("A declaration file has been generated to __pycache__ directory.");
}
0
std::process::exit(0);
}
Err(artifact) => {
if !artifact.warns.is_empty() {
@ -342,10 +231,14 @@ impl PythonAnalyzer {
};
// Even if type checking fails, some APIs are still valid, so generate a file
if self.cfg.dist_dir.is_some() {
dump_decl_package(&self.checker.shared().mod_cache);
dump_decl_er(
self.cfg.input.clone(),
artifact.object.unwrap(),
CheckStatus::Failed,
);
println!("A declaration file has been generated to __pycache__ directory.");
}
code
std::process::exit(code);
}
}
}

View File

@ -1,36 +1,10 @@
use std::env;
use std::path::{Path, PathBuf};
use std::path::PathBuf;
use std::str::FromStr;
use erg_common::config::{ErgConfig, ErgMode};
use erg_common::io::Input;
use erg_common::pathutil::project_entry_file_of;
use erg_common::switch_lang;
use indexmap::IndexSet;
use crate::copy::clear_cache;
fn entry_file() -> Option<PathBuf> {
project_entry_file_of(&env::current_dir().ok()?).or_else(|| {
let mut opt_path = None;
for ent in Path::new(".").read_dir().ok()? {
let ent = ent.ok()?;
if ent.file_type().ok()?.is_file() {
let path = ent.path();
if path.file_name().is_some_and(|name| name == "__init__.py") {
return Some(path);
} else if path.extension().is_some_and(|ext| ext == "py") {
if opt_path.is_some() {
return None;
} else {
opt_path = Some(path);
}
}
}
}
opt_path
})
}
fn command_message() -> &'static str {
switch_lang!(
@ -47,10 +21,7 @@ OPTIONS
--version/-V
--verbose 0|1|2
--server Language Serverを起動
--clear-cache
--code/-c cmd
--dump-decl
--disable feat ",
--code/-c cmd ",
"simplified_chinese" =>
"\
@ -65,10 +36,7 @@ OPTIONS
--version/-V
--verbose 0|1|2
--server Language Server
--clear-cache
--code/-c cmd
--dump-decl
--disable feat ",
--code/-c cmd ",
"traditional_chinese" =>
"\
@ -83,10 +51,7 @@ OPTIONS
--version/-V
--verbose 0|1|2
--server Language Server
--clear-cache
--code/-c cmd
--dump-decl
--disable feat ",
--code/-c cmd ",
"english" =>
"\
@ -101,10 +66,7 @@ OPTIONS
--version/-V show version
--verbose 0|1|2 verbosity level
--server start the Language Server
--clear-cache clear cache
--code/-c cmd program passed in as string
--dump-decl output type declaration file
--disable feat disable specified features",
--code/-c cmd program passed in as string",
)
}
@ -112,18 +74,12 @@ OPTIONS
pub(crate) fn parse_args() -> ErgConfig {
let mut args = env::args();
args.next(); // "pylyzer"
let mut cfg = ErgConfig {
effect_check: false,
ownership_check: false,
respect_pyi: true,
..ErgConfig::default()
};
let mut runtime_args: Vec<&'static str> = Vec::new();
let mut cfg = ErgConfig::default();
while let Some(arg) = args.next() {
match &arg[..] {
"--" => {
for arg in args {
runtime_args.push(Box::leak(arg.into_boxed_str()));
cfg.runtime_args.push(Box::leak(arg.into_boxed_str()));
}
break;
}
@ -148,34 +104,10 @@ pub(crate) fn parse_args() -> ErgConfig {
.parse::<u8>()
.expect("the value of `--verbose` is not a number");
}
"--disable" => {
let arg = args.next().expect("the value of `--disable` is not passed");
runtime_args.push(Box::leak(arg.into_boxed_str()));
}
"-V" | "--version" => {
println!("pylyzer {}", env!("CARGO_PKG_VERSION"));
std::process::exit(0);
}
"--clear-cache" => {
clear_cache();
std::process::exit(0);
}
"--no-infer-fn-type" => {
cfg.no_infer_fn_type = true;
}
"--fast-error-report" => {
cfg.fast_error_report = true;
}
"--hurry" => {
cfg.no_infer_fn_type = true;
cfg.fast_error_report = true;
}
"--do-not-show-ext-errors" => {
cfg.do_not_show_ext_errors = true;
}
"--do-not-respect-pyi" => {
cfg.respect_pyi = false;
}
other if other.starts_with('-') => {
println!(
"\
@ -195,75 +127,12 @@ For more information try `pylyzer --help`"
);
if let Some("--") = args.next().as_ref().map(|s| &s[..]) {
for arg in args {
runtime_args.push(Box::leak(arg.into_boxed_str()));
cfg.runtime_args.push(Box::leak(arg.into_boxed_str()));
}
}
break;
}
}
}
if !cfg.mode.is_language_server() && cfg.input.is_repl() {
if let Some(entry) = entry_file() {
cfg.input = Input::file(entry);
} else {
eprintln!("No entry file found in the current project");
std::process::exit(1);
}
}
cfg.runtime_args = runtime_args.into();
cfg
}
pub(crate) fn files_to_be_checked() -> IndexSet<Result<PathBuf, String>> {
let mut file_or_patterns = vec![];
let mut args = env::args().skip(1);
while let Some(arg) = &args.next() {
match arg.as_str() {
"--" => {
// Discard runtime args
break;
}
"--code" | "-c" | "--disable" | "--verbose" => {
// Skip options
let _ = &args.next();
continue;
}
file_or_pattern if file_or_pattern.starts_with("-") => {
// Skip flags
continue;
}
file_or_pattern => file_or_patterns.push(file_or_pattern.to_owned()),
}
}
let mut files = IndexSet::new();
for file_or_pattern in file_or_patterns {
if PathBuf::from(&file_or_pattern).is_file() {
files.insert(Ok(PathBuf::from(&file_or_pattern)));
} else {
let entries = glob::glob(&file_or_pattern);
match entries {
Err(_) => {
files.insert(Err(file_or_pattern));
continue;
}
Ok(entries) => {
let mut entries = entries.into_iter().peekable();
if entries.peek().is_none() {
files.insert(Err(file_or_pattern));
}
for entry in entries {
match entry {
Err(e) => eprintln!("err: {e}"),
Ok(path) if path.is_file() => {
files.insert(Ok(path));
}
_ => {}
}
}
}
}
}
}
files
}

View File

@ -1,89 +0,0 @@
use std::fs::{copy, create_dir_all, read_dir, remove_file, DirEntry};
use std::path::Path;
use erg_common::env::{erg_path, python_site_packages};
fn copy_dir(from: impl AsRef<Path>, to: impl AsRef<Path>) -> std::io::Result<()> {
let from = from.as_ref();
let to = to.as_ref();
if !from.exists() {
return Ok(());
}
if !to.exists() {
create_dir_all(to)?;
}
for entry in read_dir(from)? {
let entry = entry?;
if entry.file_type()?.is_dir() {
copy_dir(entry.path(), to.join(entry.file_name()))?;
} else {
copy(entry.path(), to.join(entry.file_name()))?;
}
}
Ok(())
}
pub(crate) fn copy_dot_erg() {
if erg_path().exists() {
return;
}
for site_packages in python_site_packages() {
if site_packages.join(".erg").exists() {
println!("Copying site-package/.erg to {}", erg_path().display());
copy_dir(site_packages.join(".erg"), erg_path()).expect("Failed to copy .erg");
}
}
}
pub(crate) fn clear_cache() {
for dir in read_dir(".").expect("Failed to read dir") {
let Ok(dir) = dir else {
continue;
};
rec_clear_cache(dir);
}
for site_packages in python_site_packages() {
for pkg in site_packages
.read_dir()
.expect("Failed to read site-packages")
{
let Ok(pkg) = pkg else {
continue;
};
rec_clear_cache(pkg);
}
}
}
fn rec_clear_cache(pkg: DirEntry) {
if pkg.file_type().expect("Failed to get file type").is_dir() {
let cache = if pkg.path().ends_with("__pycache__") {
pkg.path()
} else {
pkg.path().join("__pycache__")
};
if cache.exists() {
let Ok(dir) = cache.read_dir() else {
return;
};
for cache_file in dir {
let Ok(cache_file) = cache_file else {
continue;
};
if cache_file.file_name().to_string_lossy().ends_with(".d.er") {
println!("Removing cache file {}", cache_file.path().display());
remove_file(cache_file.path()).expect("Failed to remove cache file");
}
}
}
let Ok(dir) = pkg.path().read_dir() else {
return;
};
for entry in dir {
let Ok(entry) = entry else {
continue;
};
rec_clear_cache(entry);
}
}
}

45
src/handle_err.rs Normal file
View File

@ -0,0 +1,45 @@
use erg_common::error::ErrorKind;
use erg_common::log;
use erg_common::style::remove_style;
// use erg_common::style::{remove_style, StyledString, Color};
use erg_compiler::context::ModuleContext;
use erg_compiler::error::{CompileError, CompileErrors};
pub(crate) fn filter_errors(ctx: &ModuleContext, errors: CompileErrors) -> CompileErrors {
errors
.into_iter()
.filter_map(|error| filter_error(ctx, error))
.collect()
}
fn filter_error(_ctx: &ModuleContext, mut error: CompileError) -> Option<CompileError> {
match error.core.kind {
ErrorKind::FeatureError => {
log!(err "this error is ignored:");
log!(err "{error}");
None
}
ErrorKind::InheritanceError => None,
ErrorKind::VisibilityError => None,
// exclude doc strings
ErrorKind::UnusedWarning => {
let code = error.input.reread_lines(
error.core.loc.ln_begin().unwrap_or(1) as usize,
error.core.loc.ln_end().unwrap_or(1) as usize,
);
if code[0].trim().starts_with("\"\"\"") {
None
} else {
for sub in error.core.sub_messages.iter_mut() {
if let Some(hint) = &mut sub.hint {
*hint = remove_style(hint);
*hint = hint.replace("use discard function", "bind to `_` (`_ = ...`)");
}
}
Some(error)
}
}
// ErrorKind::AssignError => handle_assign_error(error),
_ => Some(error),
}
}

5
src/lib.rs Normal file
View File

@ -0,0 +1,5 @@
mod analyze;
mod config;
mod handle_err;
pub use analyze::PythonAnalyzer;

View File

@ -1,49 +1,22 @@
mod analyze;
mod config;
mod copy;
mod handle_err;
use analyze::{PythonAnalyzer, SimplePythonParser};
use els::Server;
use erg_common::config::ErgMode;
use erg_common::spawn::exec_new_thread;
use erg_common::style::colors::RED;
use erg_common::style::RESET;
use pylyzer_core::{PythonAnalyzer, SimplePythonParser};
use crate::config::files_to_be_checked;
use crate::copy::copy_dot_erg;
fn run() {
copy_dot_erg();
let cfg = config::parse_args();
if cfg.mode == ErgMode::LanguageServer {
let lang_server = Server::<PythonAnalyzer, SimplePythonParser>::new(cfg, None);
lang_server.run();
let mut lang_server = Server::<PythonAnalyzer, SimplePythonParser>::new(cfg);
lang_server.run().unwrap_or_else(|_| {
std::process::exit(1);
});
} else {
let mut code = 0;
let files = files_to_be_checked();
if files.is_empty() {
let mut analyzer = PythonAnalyzer::new(cfg);
code = analyzer.run();
} else {
for path in files {
match path {
Err(invalid_file_or_pattern) => {
if code == 0 {
code = 1;
}
println!("{RED}Invalid file or pattern{RESET}: {invalid_file_or_pattern}");
}
Ok(path) => {
let cfg = cfg.inherit(path);
let mut analyzer = PythonAnalyzer::new(cfg);
let c = analyzer.run();
if c != 0 {
code = 1;
}
}
}
}
}
std::process::exit(code);
let mut analyzer = PythonAnalyzer::new(cfg);
analyzer.run();
}
}

View File

@ -1,19 +0,0 @@
from collections.abc import Sequence
class Vec(Sequence):
x: list[int]
def __init__(self):
self.x = []
def __getitem__(self, i: int) -> int:
return self.x[i]
def __iter__(self):
return iter(self.x)
def __len__(self) -> int:
return len(self.x)
def __contains__(self, i: int) -> bool:
return i in self.x

View File

@ -8,17 +8,3 @@ def f(x, y=1):
print(f(1, 2)) # OK
print(f(1)) # OK
print(f(1, y="a")) # ERR
def g(first, second):
pass
g(**{"first": "bar", "second": 1}) # OK
g(**[1, 2]) # ERR
g(1, *[2]) # OK
g(*[1, 2]) # OK
g(1, 2, *[3, 4]) # ERR
g(*1) # ERR
g(*[1], **{"second": 1}) # OK
_ = f(1, *[2]) # OK
_ = f(**{"x": 1, "y": 2}) # OK

View File

@ -7,12 +7,3 @@ assert isinstance(s, int) # ERR
# force cast to int
i = typing.cast(int, s)
print(i + 1) # OK
l = typing.cast(list[str], [1, 2, 3])
_ = map(lambda x: x + "a", l) # OK
d = typing.cast(dict[str, int], [1, 2, 3])
_ = map(lambda x: d["a"] + 1, d) # OK
t = typing.cast(tuple[str, str], [1, 2, 3])
_ = map(lambda x: x + "a", t) # OK

View File

@ -1,4 +1,4 @@
from typing import Self, List
from typing import Self
class Empty: pass
emp = Empty()
@ -19,8 +19,6 @@ class C:
return self.x
def id(self) -> Self:
return self
def id2(self) -> "C":
return self
c = C(1, 2)
assert c.x == 1
@ -51,8 +49,6 @@ class D:
class E(D):
def __add__(self, other: E):
return E(self.c + other.c)
def invalid(self):
return self.d # ERR: E object has no attribute `d`
c1 = D(1).c + 1
d = D(1) + D(2)
@ -61,79 +57,3 @@ ok = D(1) - C(1, 2) # OK
assert D(1) > D(0)
c = -d # OK
e = E(1)
class F:
def __init__(self, x: int, y: int = 1, z: int = 2):
self.x = x
self.y = y
self.z = z
_ = F(1)
_ = F(1, 2)
_ = F(1, z=1, y=2)
class G(DoesNotExist): # ERR
def foo(self):
return 1
g = G()
assert g.foo() == 1
class Value:
value: object
class H(Value):
value: int
def __init__(self, value):
self.value = value
def incremented(self):
return H(self.value + 1)
class MyList(list):
@staticmethod
def try_new(lis) -> "MyList" | None:
if isinstance(lis, list):
return MyList(lis)
else:
return None
class Implicit:
def __init__(self):
self.foo = False
def set_foo(self):
self.foo = True
class Cs:
cs: list[C]
cs2: List[C]
cs_list: list[list[C]]
def __init__(self, cs: list[C]):
self.cs = cs
self.cs2 = cs
self.cs_list = []
def add(self, c: C):
self.cs.append(c)
self.cs2.append(c)
self.cs_list.append([c])
class I:
def __init__(self):
self.ix: int = 1
if True:
self.init_y()
def init_y(self):
self.iy: int = 2
def foo(self):
self.iz: int = 1 # ERR
i = I()
_ = i.ix
_ = i.iy # OK
_ = i.iz # ERR

View File

@ -1,51 +0,0 @@
i_lis = [0]
i_lis.append(1)
i_lis.append("a") # ERR
_ = i_lis[0:0]
union_arr: list[int | str] = []
union_arr.append(1)
union_arr.append("a") # OK
union_arr.append(None) # ERR
dic: dict[Literal["a", "b"], int] = {"a": 1}
dic["b"] = 2
_ = dic["a"]
_ = dic["b"]
_ = dic["c"] # ERR
dic2: dict[str, int] = {"a": 1}
_ = dic2["c"] # OK
t: tuple[int, str] = (1, "a")
_ = t[0] == 1 # OK
_ = t[1] == 1 # ERR
_ = t[0:1]
s: set[int] = {1, 2}
s.add(1)
s.add("a") # ERR
def f(s: Str): return None
for i in getattr(1, "aaa", ()):
f(i)
assert 1 in [1, 2]
assert 1 in {1, 2}
assert 1 in {1: "a"}
assert 1 in (1, 2)
assert 1 in map(lambda x: x + 1, [0, 1, 2])
def func(d: dict, t: tuple, s: set):
_ = d.get("a")
s.add(1)
for i in t:
print(i)
list_comp = [i + 1 for i in range(10)]
assert list_comp[0] == 1
set_comp = {i + 1 for i in range(10)}
assert 1 in set_comp
dict_comp = {i: i + 1 for i in range(10)}
assert dict_comp[0] == 1

29
tests/collections.py Normal file
View File

@ -0,0 +1,29 @@
i_arr = [0]
i_arr.append(1)
i_arr.append("a") # ERR
union_arr: list[int | str] = []
union_arr.append(1)
union_arr.append("a") # OK
union_arr.append(None) # ERR
dic = {"a": 1}
dic["b"] = 2
_ = dic["a"]
_ = dic["b"]
_ = dic["c"] # ERR
dic2: dict[str, int] = {"a": 1}
_ = dic2["c"] # OK
t: tuple[int, str] = (1, "a")
_ = t[0] == 1 # OK
_ = t[1] == 1 # ERR
def f(s: Str): return None
for i in getattr(1, "aaa", ()):
f(i)
assert 1 in [1, 2]
assert 1 in {1, 2}

View File

@ -1,11 +0,0 @@
i: int
if True:
i = 1
else:
i = 2
j: int
if True:
j = "1" # ERR
else:
j = "2"

View File

@ -1,18 +0,0 @@
dic = {"a": 1, "b": 2}
def f():
dic = {"a": 1}
_ = dic["b"] # ERR
class TaskManager:
def __init__(self):
self.tasks: list[dict[str, int]] = []
def add_task(self, title: str, id: int):
task = {title: id}
self.tasks.append(task)
def add_task2(self, title: str, id: int):
task = {id: title}
self.tasks.append(task) # ERR

View File

@ -1,18 +0,0 @@
class Foo:
def invalid_append(self):
paths: list[str] = []
paths.append(self) # ERR
class Bar:
foos: list[Foo]
def __init__(self, foos: list[Foo]) -> None:
self.foos = foos
def add_foo(self, foo: Foo):
self.foos.append(foo)
def invalid_add_foo(self):
self.foos.append(1) # ERR
_ = Bar([Bar([])]) # ERR

View File

@ -1,11 +0,0 @@
class Foo:
x: int
def __init__(self, x):
self.x = x
@property
def foo(self):
return self.x
f = Foo(1)
print(f.foo + "a") # ERR

View File

@ -1,8 +0,0 @@
from typing import Callable, Mapping
_: Mapping[int, str, str] = ... # ERR
_: Mapping[int] = ... # ERR
_: Callable[[int, str]] = ... # ERR
_: Callable[int] = ... # ERR
_: dict[int] = ... # ERR
_: dict[int, int, int] = ... # ERR

View File

@ -1,4 +1,4 @@
from .bar import i, Bar, Baz, Qux
from .bar import i
from . import bar
from . import baz

View File

@ -1,12 +1 @@
i = 0
class Bar:
CONST = "foo.bar"
def f(self): return 1
class Baz(Exception):
CONST = "foo.baz"
pass
class Qux(Baz):
pass
i = 0

View File

@ -1,4 +1 @@
i = 0
class Bar:
CONST = "foo.baz.bar"
i = 0

View File

@ -15,8 +15,3 @@ def h(x: str):
if True:
x = "a" # OK
return x
def var(*varargs, **kwargs):
return varargs, kwargs
_ = var(1, 2, 3, a=1, b=2, c=3)

View File

@ -1,8 +1,6 @@
import export
import foo
from . import foo
from foo import bar, Bar
from foo.bar import Baz
from foo import bar
from foo import baz
import random
from random import randint as rdi
@ -10,7 +8,6 @@ from datetime import datetime, timedelta
import datetime as dt
from http.client import HTTPResponse
import http
from math import *
i = random.randint(0, 1)
print(i + 1)
@ -30,13 +27,6 @@ assert d.x == 1
assert d.y == 2
assert foo.i == 0
assert Bar().f() == 1
assert Bar.CONST == "foo.bar"
assert Baz.CONST == "foo.baz"
from foo.baz import Bar
assert Bar.CONST == "foo.baz.bar"
from glob import glob
print(glob("*"))
@ -49,7 +39,3 @@ assert dt.datetime.max == max_date
Resp = http.client.HTTPResponse
assert export.http.client.HTTPResponse == Resp
_ = bar.Baz
_ = sin(acos(exp(0))) # OK

View File

@ -1,19 +0,0 @@
l = [1, 2, 3]
_ = l[1:2]
_ = l[:]
_ = l[1:]
_ = l[:1]
_ = l[1:1:1]
print(l[2])
print(l["a"]) # ERR
# OK
for i in range(3):
print(l[i])
# ERR
for i in "abcd":
print(l[i])
lis = "a,b,c".split(",") if True is not None else []
if "a" in lis:
lis.remove("a") # OK

View File

@ -1,5 +0,0 @@
name = "John"
print(f"Hello, {name}!")
print(f"Hello, {nome}!") # ERR
print(f"Hello, {name + 1}!") # ERR

View File

@ -7,14 +7,3 @@ def f(x: int | None):
return None
f(1)
from typing import Optional
x: Optional[int] = None
if x is not None:
x += 1
def sb(s: str | bytes) -> None:
if not isinstance(s, str):
str(s, "ascii")
return None

View File

@ -1,51 +1,17 @@
def imaginary(x):
return x.imag
def imaginary2(x):
return imaginary(x)
x.imag
assert imaginary(1) == 0
assert imaginary(1.0) <= 0.0
assert imaginary2(1) == 0
assert imaginary2(1.0) <= 0.0
print(imaginary("a")) # ERR
class C:
def method(self, x): return x
def call_method(obj, x):
return obj.method(x)
def call_method2(obj, x):
return call_method(obj, x)
def call_foo(x):
return x.foo("foo") # OK
obj.method(x)
c = C()
assert call_method(c, 1) == 1
assert call_method(c, 1) == "a" # ERR
assert call_method2(c, 1) == 1
print(call_method(1, 1)) # ERR
print(call_method(c)) # ERR
def x_and_y(a):
z: int = a.y
return a.x + z
class A:
x: int
y: int
def __init__(self, x, y):
self.x = x
self.y = y
class B:
x: int
def __init__(self, x):
self.x = x
a = A(1, 2)
assert x_and_y(a) == 3
b = B(3)
_ = x_and_y(b) # ERR: B object has no attribute `y`

View File

@ -1,11 +0,0 @@
class Foo:
x: int
def __init__(self, x):
self.x = x
@property
def foo(self):
return self.x
f = Foo(1)
assert f.foo + 1 == 2

View File

@ -1,25 +0,0 @@
x = 1
x + "a" # OK, because x: Any
def f(x, y):
return x + y
class C:
y = 1
def __init__(self, x):
self.x = x
def f(self, x):
return self.x + x
print(f(1, 2)) # OK
print(f("a", "b")) # ERR*2
c = C(1)
print(c.f(2)) # OK
print(c.f("a")) # ERR
_ = C("a") # ERR
def g(x):
pass
print(g(c)) # OK
print(g(1)) # ERR

View File

@ -1,13 +0,0 @@
import typing
x: typing.Any
def f(x: int, y: int) -> int: ...
class C:
x: int
y: int
def __init__(self, x: int): ...
def f(self, x: int) -> int: ...
def g[T: C](x: T) -> T: ...

View File

@ -1,5 +1,3 @@
from typing import Literal
i: int = 0
i: str = "a" # OK
@ -14,49 +12,3 @@ while False:
def f(x: int):
i = 1 # OK
return x + i
if True:
pass
elif True:
for i in []: pass
pass
elif True:
for i in []: pass
pass
if True:
pass
elif True:
with open("") as x:
pass
pass
elif True:
with open("") as x:
pass
pass
if True:
left, right = 1, 2
if True:
left, _ = 1, 2
def func(label: str) -> str:
if True:
try:
label_bytes = "aaa"
except UnicodeEncodeError:
return label
else:
label_bytes = label
if True:
label_bytes = label_bytes[1:]
return label_bytes
if True:
y = 1
else:
y = "a"
y: int | str
y: Literal[1, "a"] # OK
y: Literal[1, "b"] # ERR

View File

@ -10,8 +10,9 @@ def add2(x: int, y: int) -> str: # ERR
print(add2(1, 2))
# ERR
for i in [1, 2, 3]:
j = i + "aa" # ERR
j = i + "aa"
print(j)
a: int # OK
@ -30,12 +31,26 @@ class C:
dic = {"a": 1, "b": 2}
print(dic["c"]) # ERR
arr = [1, 2, 3]
print(arr[4]) # ERR
# OK
for i in range(3):
print(arr[i])
# ERR
for i in range(4):
print(arr[i])
def f(d1, d2: dict[str, int]):
_ = d1["b"] # OK
_ = d2["a"] # OK
_ = d2[1] # ERR
dic = {"a": 1}
_ = dic["b"] # ERR
arr = [1, 2, 3]
_ = arr[4] # ERR
for i in range(4):
print(arr[i]) # ERR
i, j = 1, 2
assert i == 1
@ -44,6 +59,3 @@ assert j == 2
with open("test.py") as f:
for line in f.readlines():
print("line: " + line)
print(x := 1)
print(x)

View File

@ -1,17 +1,15 @@
use std::path::{Path, PathBuf};
use std::path::PathBuf;
use erg_common::config::ErgConfig;
use erg_common::io::Input;
use erg_common::spawn::exec_new_thread;
use erg_common::traits::Stream;
use erg_compiler::artifact::{CompleteArtifact, IncompleteArtifact};
use pylyzer_core::PythonAnalyzer;
use pylyzer::PythonAnalyzer;
#[allow(clippy::result_large_err)]
pub fn exec_analyzer(file_path: &'static str) -> Result<CompleteArtifact, IncompleteArtifact> {
let cfg = ErgConfig {
input: Input::file(PathBuf::from(file_path)),
effect_check: false,
ownership_check: false,
..Default::default()
};
let mut analyzer = PythonAnalyzer::new(cfg);
@ -24,7 +22,6 @@ fn _expect(file_path: &'static str, warns: usize, errors: usize) -> Result<(), S
match exec_analyzer(file_path) {
Ok(artifact) => {
if artifact.warns.len() != warns {
eprintln!("warns: {}", artifact.warns);
return Err(format!(
"Expected {warns} warnings, found {}",
artifact.warns.len()
@ -37,14 +34,12 @@ fn _expect(file_path: &'static str, warns: usize, errors: usize) -> Result<(), S
}
Err(artifact) => {
if artifact.warns.len() != warns {
eprintln!("warns: {}", artifact.warns);
return Err(format!(
"Expected {warns} warnings, found {}",
artifact.warns.len()
));
}
if artifact.errors.len() != errors {
eprintln!("errors: {}", artifact.errors);
return Err(format!(
"Expected {errors} errors, found {}",
artifact.errors.len()
@ -59,35 +54,16 @@ pub fn expect(file_path: &'static str, warns: usize, errors: usize) -> Result<()
exec_new_thread(move || _expect(file_path, warns, errors), file_path)
}
#[test]
fn exec_abc() -> Result<(), String> {
expect("tests/abc.py", 0, 0)
}
#[test]
fn exec_test() -> Result<(), String> {
expect("tests/test.py", 0, 11)
expect("tests/test.py", 0, 15)
}
#[test]
fn exec_import() -> Result<(), String> {
if Path::new("tests/__pycache__").exists() {
std::fs::remove_dir_all("tests/__pycache__").unwrap();
}
if Path::new("tests/foo/__pycache__").exists() {
std::fs::remove_dir_all("tests/foo/__pycache__").unwrap();
}
if Path::new("tests/bar/__pycache__").exists() {
std::fs::remove_dir_all("tests/bar/__pycache__").unwrap();
}
expect("tests/import.py", 1, 2)
}
#[test]
fn exec_dict() -> Result<(), String> {
expect("tests/dict.py", 0, 2)
}
#[test]
fn exec_export() -> Result<(), String> {
expect("tests/export.py", 0, 0)
@ -100,12 +76,7 @@ fn exec_func() -> Result<(), String> {
#[test]
fn exec_class() -> Result<(), String> {
expect("tests/class.py", 0, 8)
}
#[test]
fn exec_class_err() -> Result<(), String> {
expect("tests/err/class.py", 0, 3)
expect("tests/class.py", 0, 4)
}
#[test]
@ -120,37 +91,12 @@ fn exec_warns() -> Result<(), String> {
#[test]
fn exec_typespec() -> Result<(), String> {
expect("tests/typespec.py", 0, 16)
expect("tests/typespec.py", 0, 7)
}
#[test]
fn exec_projection() -> Result<(), String> {
expect("tests/projection.py", 0, 5)
}
#[test]
fn exec_property() -> Result<(), String> {
expect("tests/property.py", 0, 0)
}
#[test]
fn exec_property_err() -> Result<(), String> {
expect("tests/err/property.py", 0, 1)
}
#[test]
fn exec_pyi() -> Result<(), String> {
expect("tests/pyi.py", 0, 5)
}
#[test]
fn exec_list() -> Result<(), String> {
expect("tests/list.py", 0, 2)
}
#[test]
fn exec_literal() -> Result<(), String> {
expect("tests/literal.py", 0, 2)
expect("tests/projection.py", 0, 4)
}
#[test]
@ -160,42 +106,22 @@ fn exec_narrowing() -> Result<(), String> {
#[test]
fn exec_casting() -> Result<(), String> {
expect("tests/casting.py", 4, 1)
expect("tests/casting.py", 1, 1)
}
#[test]
fn exec_collection() -> Result<(), String> {
expect("tests/collection.py", 0, 5)
fn exec_collections() -> Result<(), String> {
expect("tests/collections.py", 0, 4)
}
#[test]
fn exec_call() -> Result<(), String> {
expect("tests/call.py", 0, 6)
}
#[test]
fn exec_decl() -> Result<(), String> {
expect("tests/decl.py", 0, 1)
expect("tests/call.py", 0, 3)
}
#[test]
fn exec_shadowing() -> Result<(), String> {
expect("tests/shadowing.py", 0, 4)
}
#[test]
fn exec_typevar() -> Result<(), String> {
expect("tests/typevar.py", 0, 3)
}
#[test]
fn exec_type_spec() -> Result<(), String> {
expect("tests/err/type_spec.py", 0, 6)
}
#[test]
fn exec_union() -> Result<(), String> {
expect("tests/union.py", 0, 0)
expect("tests/shadowing.py", 0, 3)
}
#[test]

View File

@ -1,6 +1,5 @@
from typing import Union, Optional, Literal, Callable
from collections.abc import Iterable, Mapping
import collections
i: Union[int, str] = 1 # OK
j: Union[int, str] = "aa" # OK
@ -10,23 +9,6 @@ o: Optional[int] = None # OK
p: Optional[int] = "a" # ERR
weekdays: Literal[1, 2, 3, 4, 5, 6, 7] = 1 # OK
weekdays: Literal[1, 2, 3, 4, 5, 6, 7] = 8 # ERR
_: tuple[int, ...] = (1, 2, 3)
_: tuple[int, str] = (1, "a", 1) # OK, tuple[T, U, V] <: tuple[T, U]
_: list[tuple[int, ...]] = [(1, 2, 3)]
_: dict[str, dict[str, Union[int, str]]] = {"a": {"b": 1}}
_: dict[str, dict[str, list[int]]] = {"a": {"b": [1]}}
_: dict[str, dict[str, dict[str, int]]] = {"a": {"b": {"c": 1}}}
_: dict[str, dict[str, Optional[int]]] = {"a": {"b": 1}}
_: dict[str, dict[str, Literal[1, 2]]] = {"a": {"b": 1}}
_: dict[str, dict[str, Callable[[int], int]]] = {"a": {"b": abs}}
_: dict[str, dict[str, Callable[[int], None]]] = {"a": {"b": print}}
_: dict[str, dict[str, Opional[int]]] = {"a": {"b": 1}} # ERR
_: dict[str, dict[str, Union[int, str]]] = {"a": {"b": None}} # ERR
_: dict[str, dict[str, list[int]]] = {"a": {"b": ["c"]}} # ERR
_: dict[str, dict[str, Callable[[int], int]]] = {"a": {"b": print}} # ERR
_: dict[str, dict[str, Optional[int]]] = {"a": {"b": "c"}} # ERR
_: dict[str, dict[str, Literal[1, 2]]] = {"a": {"b": 3}} # ERR
_: list[tuple[int, ...]] = [(1, "a", 3)] # ERR
def f(x: Union[int, str]) -> None:
pass
@ -54,21 +36,3 @@ def f(x: Union[int, str, None]):
f(1)
f("a")
f(None)
i1 = 1 # type: int
# ERR
i2 = 1 # type: str
i3 = 1 # type: ignore
i3 + "a" # OK
def f(it: Iterable):
for i in it:
print(i)
def f2(it: collections.abc.Iterable):
for i in it:
print(i)
def g(it: Iterable):
for i in it:
print(i + "a") # ERR

View File

@ -1,32 +0,0 @@
from typing import TypeVar
T = TypeVar("T")
U = TypeVar("U", bound=int)
IS = TypeVar("IS", int, str)
def id(x: T) -> T:
return x
def id_int(x: U) -> U:
return x
def id_int_or_str(x: IS) -> IS:
return x
_ = id(1) + 1 # OK
_ = id("a") + "b" # OK
_ = id_int(1) # OK
_ = id_int("a") # ERR
_ = id_int_or_str(1) # OK
_ = id_int_or_str("a") # OK
_ = id_int_or_str(None) # ERR
def id2[T](x: T) -> T:
return x
def id_int2[T: int](x: T) -> T:
return x
_ = id2(1) + 1 # OK
_ = id2("a") + "b" # OK
_ = id_int2(1) # OK
_ = id_int2("a") # ERR

View File

@ -1,13 +0,0 @@
s: str | bytes = ""
s2 = s.capitalize()
s3 = s2.center(1)
s4: str | bytes | bytearray = ""
_ = s4.__len__()
def f(x: str | bytes):
return x.isalnum()
def check(s: str | bytes | bytearray):
if isinstance(s, (bytes, bytearray)):
pass

View File

@ -3,8 +3,3 @@ if True:
b = True
if True:
b = "a" # ERR
counter = 100 # counter: Literal[100]
while counter > 0:
counter -= 1 # counter: Int
counter -= 1.0 # counter: Float