Compare commits

..

179 Commits

Author SHA1 Message Date
Shunsuke Shibayama b7e78b310c Update README.md 2025-05-10 11:10:28 +09:00
Shunsuke Shibayama 83bd0ffc61 build: update version (v0.0.82) 2025-02-25 17:36:36 +09:00
Loïc Riegel f0a44bf272
Fix: checking multiple files (#126)
* fix: remove duplicates when checking multiple files

* fix: exit 1 when providing invalid file or patterns

* wip

* keep order of insertion, and works with options or flags in the middle

* alphabetical order

* remove useless type annotation

* return IndexSet directly instead of Vec

* remove .clone()
2025-02-24 17:24:27 +09:00
Shunsuke Shibayama 7871d31c93 build: update version (v0.0.81) 2025-02-22 17:56:59 +09:00
Shunsuke Shibayama 7248c00448 feat: attrs can be registered in methods other than `__init__` 2025-02-22 10:56:21 +09:00
Shunsuke Shibayama 5fb0c71a63 fix: `List` type bug 2025-02-21 21:22:13 +09:00
Shunsuke Shibayama fd4b5895f3 feat: multiple files & glob input 2025-02-21 13:24:07 +09:00
Shunsuke Shibayama 71f5da0963 build: update version (v0.0.80) 2025-02-19 01:29:57 +09:00
Shunsuke Shibayama 3003076cef Update convert.rs 2025-02-19 00:11:07 +09:00
Shunsuke Shibayama f653c3fe16 feat: add `--do-not-respect-pyi` option
With this change, by default `.pyi` file types will be respected and `.py` files will not be parsed.
2025-02-18 22:29:34 +09:00
Shunsuke Shibayama 8b9437ef7a feat: deep completion can be disabled 2025-02-09 12:41:25 +09:00
Shunsuke Shibayama b9b757d896 build: update version (v0.0.79) 2025-02-09 11:59:04 +09:00
Shunsuke Shibayama 5882f18c46 Update bug-report.yaml 2025-02-09 01:35:27 +09:00
Shunsuke Shibayama 001183db79 fix: minor bugs 2025-02-08 17:04:56 +09:00
Shunsuke Shibayama acd50996ae chore: bug_report.md -> bug_report.yaml 2025-02-06 19:14:54 +09:00
Shunsuke Shibayama 640f654b1f build: update version (v0.0.78) 2025-01-29 17:45:21 +09:00
Shunsuke Shibayama 4caac7c325 build: update deps 2025-01-29 17:37:49 +09:00
Shunsuke Shibayama 814c46996d fix: decl file generator panics 2025-01-22 02:45:16 +09:00
Shunsuke Shibayama b52b017c37 fix: decl file generation bug 2025-01-19 15:22:38 +09:00
Shunsuke Shibayama 1aa3f53d5d build: update version (v0.0.77) 2025-01-17 16:22:05 +09:00
Shunsuke Shibayama 50c7b7a252 fix: clippy warns 2025-01-17 02:58:25 +09:00
Shunsuke Shibayama 1083e2f140 fix: collection bugs 2025-01-05 00:38:40 +09:00
Shunsuke Shibayama dedfa8443b feat: support `@property` 2025-01-01 20:56:40 +09:00
Shunsuke Shibayama accd453a12 build: update version (v0.0.76) 2024-12-29 14:17:58 +09:00
Shunsuke Shibayama 9cd1846216 fix: type inference bugs 2024-12-29 14:06:12 +09:00
Shunsuke Shibayama faec281fa9 fix: type specification parsing bugs 2024-12-28 18:43:07 +09:00
Shunsuke Shibayama 0da66dceca fix: slice bug 2024-12-27 20:21:05 +09:00
Shunsuke Shibayama deb6736f9f feat: support named-expr (walrus operator) 2024-12-27 20:09:50 +09:00
Shunsuke Shibayama 2a98535d4c feat: ABC implementation check 2024-12-27 15:46:59 +09:00
Shunsuke Shibayama 233b7ee382 build: update version (v0.0.75) 2024-12-25 02:59:00 +09:00
Shunsuke Shibayama c985e798f0 build: update deps 2024-12-25 02:52:25 +09:00
Shunsuke Shibayama 088c22a84c fix: cache clearing 2024-12-25 02:44:57 +09:00
Shunsuke Shibayama 18386546b4 Update README.md 2024-12-18 09:34:53 +09:00
Shunsuke Shibayama 365530a220 build: update version (v0.0.74) 2024-12-16 23:38:13 +09:00
Shunsuke Shibayama 12844b19f4 build: update deps 2024-12-16 23:37:03 +09:00
Shunsuke Shibayama 25c37cbc28 build: update version (v0.0.73) 2024-12-07 22:24:42 +09:00
Shunsuke Shibayama a24164bbee fix: import resolution bug 2024-12-07 22:23:25 +09:00
Shunsuke Shibayama 4c57e6ead7 fix: type decl generator 2024-11-30 11:36:38 +09:00
Shunsuke Shibayama fa4cefd327 build: update version (v0.0.72) 2024-11-26 21:47:41 +09:00
Shunsuke Shibayama f06940bb4c fix: language server bug 2024-11-26 21:41:17 +09:00
Shunsuke Shibayama a0d713b2d2 fix: eliminate `unwrap`s 2024-11-23 19:41:10 +09:00
Shunsuke Shibayama 9a9215ef58 build: update version (v0.0.71) 2024-11-21 01:44:25 +09:00
Shunsuke Shibayama 5bc13dc463 Update handle_err.rs 2024-11-14 01:54:21 +09:00
Shunsuke Shibayama 6e644dacd9 feat: detect the project entry file 2024-11-14 01:44:52 +09:00
Shunsuke Shibayama 16fef0b04a build: update version (v0.0.70) 2024-11-10 16:35:29 +09:00
Shunsuke Shibayama 4afbd2922d feat: add some analysis simplification options 2024-11-10 16:28:15 +09:00
Shunsuke Shibayama 5a81c4c517 build: update version (v0.0.69) 2024-11-04 18:20:46 +09:00
Shunsuke Shibayama f2153372d0 feat: cache cleaning option 2024-11-04 18:05:33 +09:00
Shunsuke Shibayama a710224ccc build: update deps 2024-11-03 17:38:38 +09:00
Shunsuke Shibayama d4c2a27356 build: update version (v0.0.68) 2024-10-20 16:04:42 +09:00
Shunsuke Shibayama e01c99d2c0 feat: support `else` block in `for, while` 2024-10-20 16:02:40 +09:00
Shunsuke Shibayama a7801eb145 feat: use union types for variable defined in if-else 2024-10-20 15:31:49 +09:00
Shunsuke Shibayama 64169a3962 fix: clippy warnings 2024-10-19 15:15:58 +09:00
Shunsuke Shibayama 96a1a03d9f
Update issue templates 2024-10-19 15:08:22 +09:00
Shunsuke Shibayama 4b5d10b029 build: update version (v0.0.67) 2024-10-19 15:02:51 +09:00
Shunsuke Shibayama bb6b731fbf fix: name shadowing bug 2024-10-19 14:58:56 +09:00
Shunsuke Shibayama 97020008b5 doc: update 2024-10-16 16:47:59 +09:00
Shunsuke Shibayama 0157455816 chore(ext): use venv pylyzer if exists 2024-10-16 15:01:11 +09:00
Shunsuke Shibayama 460ca54cf8 build: update version (v0.0.66) 2024-10-16 03:06:37 +09:00
Shunsuke Shibayama 00e0ae46f2 chore: add settings for the extension 2024-10-16 02:02:51 +09:00
Shunsuke Shibayama c1f37108a0 fix: misalignment of error display 2024-10-13 18:31:06 +09:00
Shunsuke Shibayama 8621139e64 build: update version (v0.0.65) 2024-10-06 19:04:58 +09:00
Shunsuke Shibayama a0747938dd feat: support `async def` (partially) 2024-10-06 19:03:09 +09:00
Shunsuke Shibayama c4b7aa7faa feat: support pyi files 2024-10-06 18:46:19 +09:00
Shunsuke Shibayama fddc571eea fix: typing/collections.abc types bug 2024-10-05 16:40:29 +09:00
Shunsuke Shibayama 74163c48b8 feat: support type comment 2024-10-05 12:56:17 +09:00
Shunsuke Shibayama d3d3bdb58a feat: support `tuple[T, ...]` syntax 2024-10-05 12:17:21 +09:00
Shunsuke Shibayama 02c217e001 build: update version (v0.0.64) 2024-10-04 16:12:49 +09:00
Shunsuke Shibayama d650569de6 feat: support `typing.Any` 2024-10-04 16:07:40 +09:00
Shunsuke Shibayama ec539b013f fix: type specification bugs 2024-10-04 15:57:22 +09:00
Shunsuke Shibayama 1bfb52f801 build: update version (v0.0.63) 2024-09-26 19:02:01 +09:00
Shunsuke Shibayama b854eb7126 fix: minor bugs
update deps
2024-09-26 18:18:27 +09:00
Shunsuke Shibayama a1ffcf0a35 feat: support f-string 2024-09-21 13:37:53 +09:00
Shunsuke Shibayama 2fd2b37411 build: update version (v0.0.62) 2024-09-19 18:15:41 +09:00
Shunsuke Shibayama 4e335ac3c1 Update test.yml 2024-09-19 18:13:50 +09:00
Shunsuke Shibayama fa15f92276 build: update deps & add test cases 2024-09-19 18:11:47 +09:00
Shunsuke Shibayama 56e016e915 Update config.rs 2024-09-03 17:18:23 +09:00
Shunsuke Shibayama 31d7ad451d Update test.yml 2024-09-03 17:17:53 +09:00
Shunsuke Shibayama a209a67e55 test: add real-package tests 2024-09-02 12:52:57 +09:00
Shunsuke Shibayama e899546687 Update convert.rs 2024-08-22 14:39:52 +09:00
Shunsuke Shibayama 18af09b5e4 Update README.md 2024-08-21 12:20:46 +09:00
Shunsuke Shibayama 42c11642fd build: update version (v0.0.61) 2024-08-20 22:35:13 +09:00
Shunsuke Shibayama e3a9720159 fix: signature help not working 2024-08-20 22:32:44 +09:00
Shunsuke Shibayama b6a368257f feat: add `pylyzer_wasm` crate 2024-08-20 14:19:11 +09:00
Shunsuke Shibayama 6828ddcf56 refactor: add `pylyzer_core` crate 2024-08-20 12:46:27 +09:00
Shunsuke Shibayama f5503d6f9e feat: support arguments expansion 2024-08-20 02:53:19 +09:00
Shunsuke Shibayama d448aaf974 test: add union type tests 2024-08-20 02:20:06 +09:00
Shunsuke Shibayama 94221a6419 fix: TypeVar bug 2024-08-20 02:19:34 +09:00
Shunsuke Shibayama 6e88efebe8 feat: string literal type (forward reference) 2024-08-19 21:48:38 +09:00
Shunsuke Shibayama dcae47070a Update README.md 2024-08-19 15:59:02 +09:00
Shunsuke Shibayama 96b6db2904 doc: update docs 2024-08-18 14:22:42 +09:00
Shunsuke Shibayama d9d074e088 Create pyproject.md 2024-08-18 13:46:08 +09:00
Shunsuke Shibayama 745d62cf77 build: update version (v0.0.60) 2024-08-18 13:36:56 +09:00
Shunsuke Shibayama 67a65b5c52 feat: support glob import 2024-08-18 13:35:00 +09:00
Shunsuke Shibayama ce12285143 feat: support `TypeVar` & type parameter syntax
User class generics are not yet supported
2024-08-18 13:30:22 +09:00
Shunsuke Shibayama 582906ed92 Update gen_decl.rs 2024-08-17 12:33:32 +09:00
Shunsuke Shibayama 172cd5257b build: update version (v0.0.59) 2024-08-16 15:32:52 +09:00
Shunsuke Shibayama bbe828db94 feat: type spec of reassignment 2024-08-16 15:30:47 +09:00
Shunsuke Shibayama 11527b305f
Merge pull request #98 from Bing-su/fix/setup.py
feat: Version-independent wheel files
2024-08-14 15:33:44 +09:00
Dowon 513f112d6a
feat: Version-independent wheel files 2024-08-11 17:07:50 +09:00
Shunsuke Shibayama 4c471b8261 build: update version (v0.0.58) 2024-08-10 20:19:39 +09:00
Shunsuke Shibayama 8cf472f469 fix: false positive name errors 2024-08-10 20:18:09 +09:00
Shunsuke Shibayama 55d44bc5a4 build: update version (v0.0.57) 2024-08-08 15:02:22 +09:00
Shunsuke Shibayama 14d7fd3c33 fix: default param type mismatch errors don't report 2024-08-08 15:01:41 +09:00
Shunsuke Shibayama 7440f2fa46 fix: class importing bug 2024-08-06 19:40:46 +09:00
Shunsuke Shibayama bff700f17c build: update version (v0.0.56) 2024-07-07 13:03:48 +09:00
Shunsuke Shibayama 3c46a0340d feat: support workspace diagnostics
and fix some bugs
2024-07-07 12:57:52 +09:00
Shunsuke Shibayama 11b3940f32 build: update version (v0.0.55) 2024-07-05 23:49:30 +09:00
Shunsuke Shibayama 974269a2ba Update setup.py 2024-07-05 23:33:57 +09:00
Shunsuke Shibayama e64baf4107
Merge pull request #94 from unvalley/upgrade-biome
chore: upgrade biome to v1.8.2
2024-06-26 17:15:16 +09:00
Shunsuke Shibayama 0717723ca7 Revert "style: format files"
This reverts commit 5e9a7f9215.
2024-06-26 17:11:18 +09:00
Shunsuke Shibayama 5e9a7f9215 style: format files 2024-06-26 17:07:32 +09:00
unvalley f1f13285af chore: upgrade biome to v1.8.2 2024-06-22 20:46:22 +09:00
Shunsuke Shibayama 679a2fdadd Update gen_decl.rs 2024-06-19 01:27:49 +09:00
Shunsuke Shibayama a088185612 feat: support slice 2024-06-16 21:30:11 +09:00
Shunsuke Shibayama 91427c607d
Merge pull request #92 from GreasySlug/main
Replace `Rome` with `Biome`
2024-05-24 01:12:17 +09:00
GreasySlug 0d155ee0cd style: auto format and fix 2024-05-22 00:08:05 +09:00
GreasySlug c8863ca77c chore: replace `rome` with `biome` since it has been deprecated 2024-05-22 00:04:58 +09:00
Shunsuke Shibayama a59528ba7c build: update version (v0.0.54) 2024-05-05 15:13:29 +09:00
Shunsuke Shibayama fc26c8578c Update release.yml 2024-04-30 02:04:47 +09:00
Shunsuke Shibayama f24a10f30d build: update version (v0.0.53) 2024-04-30 00:13:56 +09:00
Shunsuke Shibayama b35a26bba4 update deps 2024-04-29 23:55:01 +09:00
Shunsuke Shibayama 70c23905ae update deps 2024-03-13 03:02:26 +09:00
Shunsuke Shibayama 9fd1bf3373 build: update version (v0.0.52) 2024-03-06 02:24:58 +09:00
Shunsuke Shibayama a988e3e52c update deps 2024-03-06 02:24:15 +09:00
Shunsuke Shibayama 6d6aea5ce5 build: update version (v0.0.51) 2023-12-12 12:55:52 +09:00
Shunsuke Shibayama 9c335837e9
Merge pull request #67 from FalkWoldmann/main
Add --locked flag to cargo install
2023-12-10 13:49:55 +09:00
Shunsuke Shibayama 38cd37b75d test: add test cases 2023-12-09 23:58:22 +09:00
Shunsuke Shibayama 021f33b31a build: update version (v0.0.50) 2023-12-09 20:58:58 +09:00
Shunsuke Shibayama 84c72e6ef6 feat: support varargs 2023-12-09 20:58:10 +09:00
FalkWoldmann 7e0d3a7f4a
Add --locked flag to cargo install 2023-11-23 10:18:13 +01:00
Shunsuke Shibayama f396fcb2c5 build: update version (v0.0.49) 2023-11-04 01:04:47 +09:00
Shunsuke Shibayama be21322cef
Merge pull request #61 from mtshiba/parallel
Change checker: `ASTLowerer` -> `PackageBuilder`
2023-11-04 01:03:37 +09:00
Shunsuke Shibayama 12a4c4c45e update deps 2023-11-04 00:59:38 +09:00
Shunsuke Shibayama 2f919b2e6f change checker: ASTLowerer -> PackageBuilder 2023-11-03 17:20:39 +09:00
Shunsuke Shibayama e44f7f4a48 build: update version (v0.0.48) 2023-10-05 02:38:02 +09:00
Shunsuke Shibayama 058e4b97d6 Update convert.rs 2023-10-05 02:02:28 +09:00
Shunsuke Shibayama b9fa9982bc Create publish.sh 2023-09-18 02:01:33 +09:00
Shunsuke Shibayama bb9274ed8f build: update version (v0.0.47) 2023-09-18 01:39:21 +09:00
Shunsuke Shibayama 9e3df04786 Update setup.py 2023-09-18 01:37:08 +09:00
Shunsuke Shibayama bfdafa9965 Update setup.py 2023-09-17 23:29:30 +09:00
Shunsuke Shibayama 71e69d6342 build: update version (v0.0.46) 2023-09-17 23:03:37 +09:00
Shunsuke Shibayama 1a4cb8db60 Update convert.rs 2023-09-11 22:21:37 +09:00
Shunsuke Shibayama a08003d5b8 build: lower minimum version requirements 2023-09-10 17:58:17 +09:00
Shunsuke Shibayama 965418be80 Update release.yml 2023-09-10 15:10:06 +09:00
Shunsuke Shibayama a5435180c0 build: update version (v0.0.45) 2023-09-10 13:28:03 +09:00
Shunsuke Shibayama 3b6d46e3de
Merge pull request #53 from mtshiba/download-deps-v2
feat: auto-copy missing dependencies
2023-09-10 13:26:47 +09:00
Shunsuke Shibayama b69ce84587 Update release.yml 2023-09-10 13:22:46 +09:00
Shunsuke Shibayama 80ab984b6d feat: copy `.erg` 2023-09-10 13:18:15 +09:00
Shunsuke Shibayama 75be1077db chore: add `clean` option 2023-09-10 12:44:47 +09:00
Shunsuke Shibayama 6b807d17e1 build: add `setup.py` 2023-09-10 12:23:59 +09:00
Shunsuke Shibayama 95510f6227 build: update deps 2023-09-10 09:58:37 +09:00
Shunsuke Shibayama a369950784 build: update version (v0.0.43) 2023-09-05 22:06:04 +09:00
Shunsuke Shibayama 700937e6f0 build: update deps & add tests 2023-09-03 03:38:44 +09:00
Shunsuke Shibayama ca262b4046 build: update version (v0.0.42) 2023-09-03 03:08:15 +09:00
Shunsuke Shibayama 27ee8922cf Update Cargo.lock 2023-08-24 15:00:58 +09:00
Shunsuke Shibayama 3d2cc3586f build: update deps 2023-07-26 21:36:16 +09:00
Shunsuke Shibayama d9d355ece0 test: add widening test 2023-07-26 01:45:36 +09:00
Shunsuke Shibayama ecee5b7192 fix: shadowing bug 2023-07-12 14:54:33 +09:00
Shunsuke Shibayama febea7bf9b test: refactor 2023-07-12 13:23:15 +09:00
Shunsuke Shibayama 950efbdd76 Update Cargo.lock 2023-07-08 13:18:51 +09:00
Shunsuke Shibayama b781fdf474 build: update deps & internal changes 2023-07-07 01:25:16 +09:00
Shunsuke Shibayama 71a684f4b6 test: add test cases 2023-07-03 20:16:38 +09:00
Shunsuke Shibayama 40b66fdf9d Update README.md 2023-07-03 01:04:21 +09:00
Shunsuke Shibayama c985ca7fce Update README.md 2023-06-26 14:05:27 +09:00
Shunsuke Shibayama 8f586ba8ea feat: stable support of `Self` type 2023-06-23 20:56:59 +09:00
Shunsuke Shibayama 7c2d53c4a4
Update README.md 2023-06-23 00:56:24 +09:00
Shunsuke Shibayama 4098c90a19 build: update deps 2023-06-23 00:52:37 +09:00
Shunsuke Shibayama 466a3bdc77 build: update version (v0.0.32) 2023-06-10 23:38:15 +09:00
Shunsuke Shibayama 58c1245b72
Merge pull request #37 from youknowone/trial
upgrade rustpython-parser (not going to be finished)
2023-06-10 23:29:53 +09:00
Shunsuke Shibayama 39718e2863 Merge branch 'main' into pr/37 2023-06-09 21:57:22 +09:00
Shunsuke Shibayama b9839553b1 Update convert.rs 2023-06-09 21:54:02 +09:00
Shunsuke Shibayama df54032e54 build: update deps 2023-06-09 21:06:13 +09:00
Shunsuke Shibayama 25336075c1 Update convert.rs 2023-06-08 02:38:40 +09:00
Shunsuke Shibayama d36d4150bd Update analyze.rs 2023-06-08 01:53:00 +09:00
Shunsuke Shibayama 39c27524ec WIP 2023-06-07 17:09:03 +09:00
Shunsuke Shibayama d4c4f08a6f trial 2023-06-07 02:58:01 +09:00
Shunsuke Shibayama e550dd986e Update convert.rs 2023-05-29 06:30:41 +09:00
Jeong YunWon 69d634115a trial 2023-05-29 01:11:43 +09:00
85 changed files with 9172 additions and 5147 deletions

53
.github/ISSUE_TEMPLATE/bug-report.yaml vendored Normal file
View File

@ -0,0 +1,53 @@
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

2
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

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

View File

@ -0,0 +1,19 @@
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,35 +26,71 @@ jobs:
cargo login ${{ secrets.CARGO_TOKEN }}
chmod +x cargo_publish.sh
./cargo_publish.sh --cargo-only
pypi:
make-pypi-artifacts:
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 }}
pip3 install maturin
- name: upload
run: |
maturin publish -u mtshiba -p ${{ secrets.PYPI_PASSWORD }} --target ${{ matrix.target }} --skip-existing
- 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
upload-assets:
needs: create-release
strategy:

View File

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

22
.github/workflows/stale-issues.yml vendored Normal file
View File

@ -0,0 +1,22 @@
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

99
.github/workflows/test.yml vendored Normal file
View File

@ -0,0 +1,99 @@
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,3 +1,5 @@
/target
__pycache__/
test*.py
/site
.venv

View File

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

1501
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -12,20 +12,25 @@ repository.workspace = true
[workspace]
members = [
"crates/py2erg",
"crates/pylyzer_core",
"crates/pylyzer_wasm",
]
[workspace.package]
version = "0.0.41"
version = "0.0.82"
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.19", features = ["py_compat", "els"] }
erg_compiler = { version = "0.6.19", features = ["py_compat", "els"] }
els = { version = "0.1.31", features = ["py_compat"] }
rustpython-parser = "0.1.2"
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_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"] }
@ -34,18 +39,26 @@ rustpython-parser = "0.1.2"
# els = { path = "../erg/crates/els", features = ["py_compat"] }
[features]
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"]
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"]
[dependencies]
erg_compiler = { workspace = true }
pylyzer_core = { version = "0.0.82", path = "./crates/pylyzer_core" }
erg_common = { workspace = true }
els = { workspace = true }
rustpython-parser = { workspace = true }
py2erg = { version = "0.0.41", path = "./crates/py2erg" }
glob = "0.3.2"
indexmap = "2.7.1"
[lib]
path = "src/lib.rs"
[dev-dependencies]
erg_compiler = { workspace = true }
[profile.opt-with-dbg]
inherits = "release"
debug = true

139
README.md
View File

@ -1,5 +1,9 @@
# 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>
@ -10,28 +14,62 @@
## Installation
### cargo (rust package manager)
```bash
cargo install pylyzer
```
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).__
### cargo (Rust package manager)
```bash
curl -L https://github.com/mtshiba/ergup/raw/main/ergup.py | python3
cargo install pylyzer --locked
```
### build from source
```bash
git clone https://github.com/mtshiba/pylyzer.git
cargo install --path . --locked
```
Make sure that `cargo`/`rustc` is up-to-date, as pylyzer may be written with the latest (stable) language features.
### [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 🌟
@ -40,24 +78,18 @@ 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)).
@ -66,12 +98,18 @@ pylyzer as a language server supports various features, such as completion and r
![autoimport](https://raw.githubusercontent.com/mtshiba/pylyzer/main/images/autoimport.gif)
## [VSCode extension](https://github.com/mtshiba/pylyzer/blob/main/extension)
## VSCode extension
## What is the difference from [Ruff](https://github.com/charliermarsh/ruff)?
You can install the VSCode extension from the [Marketplace](https://marketplace.visualstudio.com/items?itemName=pylyzer.pylyzer) or from the command line:
[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.
```sh
code --install-extension pylyzer.pylyzer
```
## What is the difference from [Ruff](https://github.com/astral-sh/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.
## How it works
@ -95,39 +133,76 @@ pylyzer converts Python ASTs to Erg ASTs and passes them to Erg's type checker.
* [x] operator
* [x] function/method
* [x] class
* [ ] `async/await`
* [ ] user-defined abstract class
* [x] type inference
* [x] variable
* [x] operator
* [x] function/method
* [x] class
* [x] builtin modules resolving (partially)
* [x] local scripts resolving
* [ ] local packages resolving
* [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] collection types
* [x] `list`
* [x] `dict`
* [x] `tuple`
* [x] `set`
* [ ] `typing`
* [x] `Union`
* [x] `Optional`
* [x] `Literal`
* [x] `Callable`
* [x] `Any`
* [x] `TypeVar`
* [ ] `TypedDict`
* [ ] type variable (`TypeVar`, `Generic`)
* [ ] `ClassVar`
* [ ] `Generic`
* [ ] `Protocol`
* [ ] `Final`
* [ ] `Annotated`
* [ ] `TypeAlias`
* [ ] type guard (`TypeGuard`)
* [ ] `TypeGuard`
* [x] type parameter syntax
* [x] type narrowing
* [ ] others
* `collections.abc`
* [ ] `collections.abc`
* [x] `Collection`
* [x] `Container`
* [x] `Generator`
* [x] `Iterable`
* [x] `Iterator`
* [x] `Mapping`
* [x] `Sequence`
* [x] `Mapping`, `MutableMapping`
* [x] `Sequence`, `MutableSequence`
* [ ] 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,6 +3,9 @@ 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,13 @@ 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 }
rustpython-ast = { workspace = true }
erg_common = { workspace = true }
erg_compiler = { workspace = true }

View File

@ -1,131 +1,10 @@
use erg_common::log;
use rustpython_parser::ast::{
BooleanOperator, Comparison, ExpressionType, Keyword, Number, StringGroup,
};
use rustpython_parser::ast::located::Expr;
pub fn number_to_string(num: &Number) -> String {
match num {
Number::Integer { value } => value.to_string(),
Number::Float { value } => value.to_string(),
Number::Complex { real, imag } => format!("{real}+{imag}j"),
}
}
pub fn keyword_length(keyword: &Keyword) -> usize {
if let Some(name) = &keyword.name {
name.len() + 1 + length(&keyword.value.node)
} else {
length(&keyword.value.node)
}
}
pub fn string_length(string: &StringGroup) -> usize {
match string {
StringGroup::Constant { value } => value.len(),
StringGroup::Joined { values } => values.iter().map(string_length).sum(),
other => {
log!(err "{other:?}");
0
}
}
}
pub fn comp_to_string(comp: &Comparison) -> String {
match comp {
Comparison::In => "in".to_string(),
Comparison::NotIn => "not in".to_string(),
Comparison::Is => "is".to_string(),
Comparison::IsNot => "is not".to_string(),
Comparison::Less => "<".to_string(),
Comparison::Greater => ">".to_string(),
Comparison::Equal => "==".to_string(),
Comparison::NotEqual => "!=".to_string(),
Comparison::LessOrEqual => "<=".to_string(),
Comparison::GreaterOrEqual => ">=".to_string(),
}
}
pub fn length(expr: &ExpressionType) -> usize {
pub fn accessor_name(expr: Expr) -> Option<String> {
match expr {
ExpressionType::Identifier { name } => name.len(),
ExpressionType::Number { value } => number_to_string(value).len(),
ExpressionType::String { value } => string_length(value),
ExpressionType::Attribute { value, name } => length(&value.node) + name.len() + 1,
ExpressionType::Subscript { a, b } => length(&a.node) + length(&b.node) + 2,
ExpressionType::Tuple { elements }
| ExpressionType::List { elements }
| ExpressionType::Set { elements } => {
if let (Some(first), Some(last)) = (elements.first(), elements.last()) {
2 + last.location.column() - first.location.column()
} else {
2
}
}
ExpressionType::Call {
function,
args,
keywords,
} => {
let args_len = if let (Some(first), Some(last)) = (args.first(), args.last()) {
last.location.column() - first.location.column()
} else {
0
};
let kw_len = if let (Some(first), Some(last)) = (keywords.first(), keywords.last()) {
last.value.location.column() - first.value.location.column()
} else {
0
};
length(&function.node) + args_len + kw_len + 2 // ()
}
ExpressionType::Unop { op: _, a } => 1 + length(&a.node),
ExpressionType::Binop { a, op: _, b } => length(&a.node) + 3 + length(&b.node),
ExpressionType::BoolOp { op, values } => match op {
BooleanOperator::And => values
.iter()
.map(|elem| length(&elem.node))
.fold(0, |acc, x| acc + x + 3),
BooleanOperator::Or => values
.iter()
.map(|elem| length(&elem.node))
.fold(0, |acc, x| acc + x + 2),
},
ExpressionType::Compare { vals, ops } => vals
.iter()
.zip(ops.iter())
.map(|(elem, op)| length(&elem.node) + comp_to_string(op).len())
.fold(0, |acc, x| acc + x + 2),
ExpressionType::IfExpression { test, body, orelse } => {
// x if y else z
length(&test.node) + 4 + length(&body.node) + 6 + length(&orelse.node)
}
ExpressionType::Lambda { args: _, body } => {
// lambda x: y
// TODO:
7 + 1 + length(&body.node)
}
ExpressionType::Await { value } => 5 + length(&value.node),
ExpressionType::Yield { value } => 5 + value.as_ref().map(|x| length(&x.node)).unwrap_or(0),
ExpressionType::NamedExpression { left, right } => {
// x := y
length(&left.node) + 4 + length(&right.node)
}
ExpressionType::Starred { value } => 1 + length(&value.node),
ExpressionType::False => 5,
ExpressionType::True | ExpressionType::None => 4,
ExpressionType::Ellipsis => 8,
other => {
log!(err "{other:?}");
0
}
}
}
pub fn accessor_name(expr: ExpressionType) -> Option<String> {
match expr {
ExpressionType::Identifier { name } => Some(name),
ExpressionType::Attribute { value, name } => {
accessor_name(value.node).map(|value| format!("{value}.{name}"))
Expr::Name(name) => Some(name.id.to_string()),
Expr::Attribute(attr) => {
accessor_name(*attr.value).map(|value| format!("{value}.{}", attr.attr))
}
_ => None,
}

View File

@ -1,272 +0,0 @@
use rustpython_parser::ast::{
BooleanOperator, Comparison, Comprehension, ComprehensionKind, ExpressionType, Keyword,
Located, Number, Operator, Parameter, Parameters, StringGroup, UnaryOperator, Varargs,
};
fn clone_number(num: &Number) -> Number {
match num {
Number::Integer { value } => Number::Integer {
value: value.clone(),
},
Number::Float { value } => Number::Float { value: *value },
Number::Complex { real, imag } => Number::Complex {
real: *real,
imag: *imag,
},
}
}
fn clone_string_group(group: &StringGroup) -> StringGroup {
match group {
StringGroup::Constant { value } => StringGroup::Constant {
value: value.clone(),
},
StringGroup::FormattedValue {
value,
conversion,
spec,
} => StringGroup::FormattedValue {
value: Box::new(clone_loc_expr(value)),
conversion: *conversion,
spec: spec.as_deref().map(|sp| Box::new(clone_string_group(sp))),
},
StringGroup::Joined { values } => StringGroup::Joined {
values: values.iter().map(clone_string_group).collect::<Vec<_>>(),
},
}
}
fn clone_unary_op(op: &UnaryOperator) -> UnaryOperator {
match op {
UnaryOperator::Not => UnaryOperator::Not,
UnaryOperator::Inv => UnaryOperator::Inv,
UnaryOperator::Pos => UnaryOperator::Pos,
UnaryOperator::Neg => UnaryOperator::Neg,
}
}
fn clone_bin_op(op: &Operator) -> Operator {
match op {
Operator::Add => Operator::Add,
Operator::Sub => Operator::Sub,
Operator::Mult => Operator::Mult,
Operator::MatMult => Operator::MatMult,
Operator::Div => Operator::Div,
Operator::Mod => Operator::Mod,
Operator::Pow => Operator::Pow,
Operator::LShift => Operator::LShift,
Operator::RShift => Operator::RShift,
Operator::BitOr => Operator::BitOr,
Operator::BitXor => Operator::BitXor,
Operator::BitAnd => Operator::BitAnd,
Operator::FloorDiv => Operator::FloorDiv,
}
}
fn clone_comp_op(op: &Comparison) -> Comparison {
match op {
Comparison::Equal => Comparison::Equal,
Comparison::NotEqual => Comparison::NotEqual,
Comparison::Less => Comparison::Less,
Comparison::LessOrEqual => Comparison::LessOrEqual,
Comparison::Greater => Comparison::Greater,
Comparison::GreaterOrEqual => Comparison::GreaterOrEqual,
Comparison::Is => Comparison::Is,
Comparison::IsNot => Comparison::IsNot,
Comparison::In => Comparison::In,
Comparison::NotIn => Comparison::NotIn,
}
}
fn clone_bool_op(op: &BooleanOperator) -> BooleanOperator {
match op {
BooleanOperator::And => BooleanOperator::And,
BooleanOperator::Or => BooleanOperator::Or,
}
}
fn clone_param(param: &Parameter) -> Parameter {
Parameter {
location: param.location,
arg: param.arg.clone(),
annotation: param
.annotation
.as_deref()
.map(|a| Box::new(clone_loc_expr(a))),
}
}
fn clone_varargs(varargs: &Varargs) -> Varargs {
match varargs {
Varargs::None => Varargs::None,
Varargs::Unnamed => Varargs::Unnamed,
Varargs::Named(name) => Varargs::Named(clone_param(name)),
}
}
fn clone_params(params: &Parameters) -> Parameters {
Parameters {
posonlyargs_count: params.posonlyargs_count,
args: params.args.iter().map(clone_param).collect::<Vec<_>>(),
vararg: clone_varargs(&params.vararg),
kwonlyargs: params
.kwonlyargs
.iter()
.map(clone_param)
.collect::<Vec<_>>(),
kw_defaults: params
.kw_defaults
.iter()
.map(|def| def.as_ref().map(clone_loc_expr))
.collect::<Vec<_>>(),
kwarg: clone_varargs(&params.kwarg),
defaults: params
.defaults
.iter()
.map(clone_loc_expr)
.collect::<Vec<_>>(),
}
}
fn clone_kw(keyword: &Keyword) -> Keyword {
Keyword {
name: keyword.name.clone(),
value: clone_loc_expr(&keyword.value),
}
}
fn clone_comprehension_kind(kind: &ComprehensionKind) -> ComprehensionKind {
match kind {
ComprehensionKind::Dict { key, value } => ComprehensionKind::Dict {
key: clone_loc_expr(key),
value: clone_loc_expr(value),
},
ComprehensionKind::List { element } => ComprehensionKind::List {
element: clone_loc_expr(element),
},
ComprehensionKind::Set { element } => ComprehensionKind::Set {
element: clone_loc_expr(element),
},
ComprehensionKind::GeneratorExpression { element } => {
ComprehensionKind::GeneratorExpression {
element: clone_loc_expr(element),
}
}
}
}
pub fn clone_loc_expr(expr: &Located<ExpressionType>) -> Located<ExpressionType> {
Located {
node: clone_expr(&expr.node),
location: expr.location,
}
}
pub fn clone_expr(expr: &ExpressionType) -> ExpressionType {
match expr {
ExpressionType::None => ExpressionType::None,
ExpressionType::Ellipsis => ExpressionType::Ellipsis,
ExpressionType::True => ExpressionType::True,
ExpressionType::False => ExpressionType::False,
ExpressionType::Identifier { name } => ExpressionType::Identifier { name: name.clone() },
ExpressionType::Number { value } => ExpressionType::Number {
value: clone_number(value),
},
ExpressionType::String { value } => ExpressionType::String {
value: clone_string_group(value),
},
ExpressionType::Attribute { value, name } => ExpressionType::Attribute {
value: Box::new(clone_loc_expr(value)),
name: name.clone(),
},
ExpressionType::Subscript { a, b } => ExpressionType::Subscript {
a: Box::new(clone_loc_expr(a)),
b: Box::new(clone_loc_expr(b)),
},
ExpressionType::Slice { elements } => ExpressionType::Slice {
elements: elements.iter().map(clone_loc_expr).collect::<Vec<_>>(),
},
ExpressionType::Bytes { value } => ExpressionType::Bytes {
value: value.clone(),
},
ExpressionType::Call {
function,
args,
keywords,
} => ExpressionType::Call {
function: Box::new(clone_loc_expr(function)),
args: args.iter().map(clone_loc_expr).collect::<Vec<_>>(),
keywords: keywords.iter().map(clone_kw).collect::<Vec<_>>(),
},
ExpressionType::Unop { op, a } => ExpressionType::Unop {
op: clone_unary_op(op),
a: Box::new(clone_loc_expr(a)),
},
ExpressionType::Binop { a, op, b } => ExpressionType::Binop {
a: Box::new(clone_loc_expr(a)),
op: clone_bin_op(op),
b: Box::new(clone_loc_expr(b)),
},
ExpressionType::Compare { vals, ops } => ExpressionType::Compare {
ops: ops.iter().map(clone_comp_op).collect::<Vec<_>>(),
vals: vals.iter().map(clone_loc_expr).collect::<Vec<_>>(),
},
ExpressionType::BoolOp { op, values } => ExpressionType::BoolOp {
op: clone_bool_op(op),
values: values.iter().map(clone_loc_expr).collect::<Vec<_>>(),
},
ExpressionType::Lambda { args, body } => ExpressionType::Lambda {
args: Box::new(clone_params(args)),
body: Box::new(clone_loc_expr(body)),
},
ExpressionType::IfExpression { test, body, orelse } => ExpressionType::IfExpression {
test: Box::new(clone_loc_expr(test)),
body: Box::new(clone_loc_expr(body)),
orelse: Box::new(clone_loc_expr(orelse)),
},
ExpressionType::Dict { elements } => ExpressionType::Dict {
elements: elements
.iter()
.map(|(key, value)| (key.as_ref().map(clone_loc_expr), clone_loc_expr(value)))
.collect::<Vec<_>>(),
},
ExpressionType::Set { elements } => ExpressionType::Set {
elements: elements.iter().map(clone_loc_expr).collect::<Vec<_>>(),
},
ExpressionType::List { elements } => ExpressionType::List {
elements: elements.iter().map(clone_loc_expr).collect::<Vec<_>>(),
},
ExpressionType::Tuple { elements } => ExpressionType::Tuple {
elements: elements.iter().map(clone_loc_expr).collect::<Vec<_>>(),
},
ExpressionType::Yield { value } => ExpressionType::Yield {
value: value.as_ref().map(|val| Box::new(clone_loc_expr(val))),
},
ExpressionType::YieldFrom { value } => ExpressionType::YieldFrom {
value: Box::new(clone_loc_expr(value)),
},
ExpressionType::Await { value } => ExpressionType::Await {
value: Box::new(clone_loc_expr(value)),
},
ExpressionType::NamedExpression { left, right } => ExpressionType::NamedExpression {
left: Box::new(clone_loc_expr(left)),
right: Box::new(clone_loc_expr(right)),
},
ExpressionType::Starred { value } => ExpressionType::Starred {
value: Box::new(clone_loc_expr(value)),
},
ExpressionType::Comprehension { kind, generators } => ExpressionType::Comprehension {
kind: Box::new(clone_comprehension_kind(kind)),
generators: generators
.iter()
.map(|gen| Comprehension {
location: gen.location,
target: clone_loc_expr(&gen.target),
iter: clone_loc_expr(&gen.iter),
ifs: gen.ifs.iter().map(clone_loc_expr).collect::<Vec<_>>(),
is_async: gen.is_async,
})
.collect::<Vec<_>>(),
},
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,14 @@
use std::fs::File;
use std::fs::{create_dir_all, File};
use std::io::{BufWriter, Write};
use std::path::Path;
use erg_common::io::Input;
use erg_common::log;
use erg_common::pathutil::{mod_name, NormalizedPathBuf};
use erg_common::set::Set;
use erg_common::traits::LimitedDisplay;
use erg_compiler::context::register::{CheckStatus, PylyzerStatus};
use erg_compiler::hir::{Expr, HIR};
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::ty::value::{GenTypeObj, TypeObj};
use erg_compiler::ty::{HasType, Type};
@ -13,41 +16,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(input: &Input, status: CheckStatus) -> Self {
pub fn new(path: &NormalizedPathBuf, status: CheckStatus) -> std::io::Result<Self> {
let (timestamp, hash) = {
let py_file_path = input.path();
let metadata = std::fs::metadata(py_file_path).unwrap();
let metadata = std::fs::metadata(path)?;
let dummy_hash = metadata.len();
(metadata.modified().unwrap(), dummy_hash)
(metadata.modified()?, dummy_hash)
};
let status = PylyzerStatus {
status,
file: input.path().into(),
file: path.to_path_buf(),
timestamp,
hash,
};
let code = format!("{status}\n");
Self {
filename: input.filename().replace(".py", ".d.er"),
Ok(Self {
filename: path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.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.into_iter() {
pub fn gen_decl_er(mut self, hir: &HIR) -> DeclFile {
for chunk in hir.module.iter() {
self.gen_chunk_decl(chunk);
}
log!("code:\n{}", self.code);
@ -57,7 +60,35 @@ impl DeclFileGenerator {
}
}
fn gen_chunk_decl(&mut self, chunk: Expr) {
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) {
match chunk {
Expr::Def(def) => {
let mut name = def
@ -65,19 +96,29 @@ impl DeclFileGenerator {
.ident()
.inspect()
.replace('\0', "")
.replace('%', "___");
.replace(['%', '*'], "___");
let ref_t = def.sig.ident().ref_t();
let typ = ref_t.replace_failure().to_string_unabbreviated();
let typ = escape_type(typ);
self.prepare_using_type(ref_t);
let typ = self.escape_type(ref_t.replace_failure().to_string_unabbreviated());
// Erg can automatically import nested modules
// `import http.client` => `http = pyimport "http"`
let decl = if ref_t.is_py_module() {
let decl = if ref_t.is_py_module() && ref_t.typarams()[0].is_str_value() {
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 {}",
"{}.{name} = pyimport \"{mod_name}\"\n{imported}",
self.namespace,
ref_t.typarams()[0]
)
} else {
imported
}
} else {
format!("{}.{name}: {typ}", self.namespace)
};
@ -89,15 +130,21 @@ 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 {
let sup = class.sup.as_ref().typ().to_string_unabbreviated();
let sup = escape_type(sup);
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);
let decl = format!(".{class_name} <: {sup}\n");
self.code += &decl;
}
@ -107,7 +154,8 @@ impl DeclFileGenerator {
}) = def.obj.base_or_sup()
{
for (attr, t) in rec.iter() {
let typ = escape_type(t.to_string_unabbreviated());
self.prepare_using_type(t);
let typ = self.escape_type(t.replace_failure().to_string_unabbreviated());
let decl = format!("{}.{}: {typ}\n", self.namespace, attr.symbol);
self.code += &decl;
}
@ -118,52 +166,66 @@ impl DeclFileGenerator {
}) = def.obj.additional()
{
for (attr, t) in rec.iter() {
let typ = escape_type(t.to_string_unabbreviated());
self.prepare_using_type(t);
let typ = self.escape_type(t.replace_failure().to_string_unabbreviated());
let decl = format!("{}.{}: {typ}\n", self.namespace, attr.symbol);
self.code += &decl;
}
}
for attr in def.methods.into_iter() {
for attr in ClassDef::get_all_methods(&def.methods_list) {
self.gen_chunk_decl(attr);
}
self.namespace = stash;
}
Expr::Dummy(dummy) => {
for chunk in dummy.into_iter() {
for chunk in dummy.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');
}
}
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();
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);
}
let filename = input.filename();
let mut path = pycache_dir.join(filename);
path.set_extension("d.er");
let path = cache_dir.join(file.filename);
if !path.exists() {
let _f = File::create(path).unwrap();
File::create(&path)?;
}
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_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();
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);
}
}
}

View File

@ -1,5 +1,4 @@
pub mod ast_util;
mod clone;
mod convert;
mod error;
mod gen_decl;

View File

@ -0,0 +1,26 @@
[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

@ -0,0 +1,352 @@
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::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::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::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 rustpython_ast::{Fold, ModModule};
use rustpython_parser::{Parse, ParseErrorType};
use crate::handle_err;
#[derive(Debug, Default)]
pub struct SimplePythonParser {
cfg: ErgConfig,
}
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 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);
if errors.is_empty() {
Ok(ParseArtifact::new(erg_ast, warns.into()))
} else {
Err(IncompleteParseArtifact::new(
Some(erg_ast),
errors.into(),
warns.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)
}
}
impl Runnable for PythonAnalyzer {
type Err = CompileError;
type Errs = CompileErrors;
const NAME: &'static str = "Python Analyzer";
#[inline]
fn cfg(&self) -> &ErgConfig {
&self.cfg
}
#[inline]
fn cfg_mut(&mut self) -> &mut ErgConfig {
&mut self.cfg
}
fn finish(&mut self) {
self.checker.finish();
}
fn initialize(&mut self) {
self.checker.initialize();
}
fn clear(&mut self) {
self.checker.clear();
}
fn eval(&mut self, src: String) -> Result<String, Self::Errs> {
self.checker.eval(src)
}
fn exec(&mut self) -> Result<ExitStatus, Self::Errs> {
self.checker.exec()
}
}
impl Buildable for PythonAnalyzer {
fn inherit(cfg: ErgConfig, shared: SharedCompilerResource) -> Self {
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),
}
}
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()
}
fn get_context(&self) -> Option<&ModuleContext> {
self.checker.get_context()
}
}
impl BuildRunnable for PythonAnalyzer {}
impl PythonAnalyzer {
pub fn new(cfg: ErgConfig) -> Self {
New::new(cfg)
}
#[allow(clippy::result_large_err)]
fn check(
&mut self,
erg_ast: AST,
mut errors: CompileErrors,
mut warns: CompileErrors,
mode: &str,
) -> Result<CompleteArtifact, IncompleteArtifact> {
match self.checker.build_from_ast(erg_ast, mode) {
Ok(mut artifact) => {
artifact.warns.extend(warns);
artifact.warns =
handle_err::filter_errors(self.get_context().unwrap(), artifact.warns);
if errors.is_empty() {
Ok(artifact)
} else {
Err(IncompleteArtifact::new(
Some(artifact.object),
errors,
artifact.warns,
))
}
}
Err(iart) => {
errors.extend(iart.errors);
let errors = handle_err::filter_errors(self.get_context().unwrap(), errors);
warns.extend(iart.warns);
let warns = handle_err::filter_errors(self.get_context().unwrap(), 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() {
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}");
match self.analyze(py_code, "exec") {
Ok(artifact) => {
if !artifact.warns.is_empty() {
println!(
"{YELLOW}Found {} warnings{RESET}: {}",
artifact.warns.len(),
self.cfg.input.filename()
);
artifact.warns.write_all_stderr();
}
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);
println!("A declaration file has been generated to __pycache__ directory.");
}
0
}
Err(artifact) => {
if !artifact.warns.is_empty() {
println!(
"{YELLOW}Found {} warnings{RESET}: {}",
artifact.warns.len(),
self.cfg.input.filename()
);
artifact.warns.write_all_stderr();
}
let code = if artifact.errors.is_empty() {
println!("{GREEN}All checks OK{RESET}: {}", self.cfg.input.filename());
0
} else {
println!(
"{RED}Found {} errors{RESET}: {}",
artifact.errors.len(),
self.cfg.input.filename()
);
artifact.errors.write_all_stderr();
1
};
// 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);
println!("A declaration file has been generated to __pycache__ directory.");
}
code
}
}
}
}

View File

@ -0,0 +1,100 @@
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

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

View File

@ -0,0 +1,19 @@
[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

@ -0,0 +1,13 @@
# 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();
```

262
crates/pylyzer_wasm/lib.rs Normal file
View File

@ -0,0 +1,262 @@
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()
}
}
}
}

75
docs/options/options.md Normal file
View File

@ -0,0 +1,75 @@
# 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.

10
docs/options/pyproject.md Normal file
View File

@ -0,0 +1,10 @@
# `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"
```

View File

@ -1,28 +0,0 @@
# 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", "rome.rome"]
"recommendations": ["amodio.tsl-problem-matcher", "biomejs.biome"]
}

View File

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

View File

@ -36,4 +36,12 @@ 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 |

31
extension/biome.json Normal file
View File

@ -0,0 +1,31 @@
{
"$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

@ -3,14 +3,11 @@
"displayName": "pylyzer",
"description": "A fast Python static code analyzer & language server for VSCode",
"publisher": "pylyzer",
"version": "0.1.7",
"version": "0.1.11",
"engines": {
"vscode": "^1.70.0"
},
"categories": [
"Programming Languages",
"Linters"
],
"categories": ["Programming Languages", "Linters"],
"repository": {
"type": "git",
"url": "https://github.com/mtshiba/pylyzer.git"
@ -18,6 +15,8 @@
"icon": "images/pylyzer-logo.png",
"main": "./dist/extension.js",
"activationEvents": [
"workspaceContains:pyproject.toml",
"workspaceContains:*/pyproject.toml",
"onLanguage:python"
],
"contributes": {
@ -31,13 +30,8 @@
"languages": [
{
"id": "python",
"aliases": [
"Python",
"python"
],
"extensions": [
".py"
]
"aliases": ["Python", "python"],
"extensions": [".py", ".pyi"]
}
],
"configuration": {
@ -54,10 +48,50 @@
"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"
}
}
}
@ -74,16 +108,16 @@
"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 ."
"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",
@ -91,13 +125,12 @@
"@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"
"*": "biome format --write"
}
}

View File

@ -1,11 +0,0 @@
{
"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 { LanguageClient, Location, Position } from 'vscode-languageclient/node';
import { Uri, commands } from 'vscode';
import type { LanguageClient, Location, Position } from "vscode-languageclient/node";
export async function showReferences(
client: LanguageClient | undefined,
uri: string,
position: Position,
locations: Location[]
locations: Location[],
) {
if (client) {
await commands.executeCommand(
"editor.action.showReferences",
Uri.parse(uri),
client.protocol2CodeConverter.asPosition(position),
locations.map(client.protocol2CodeConverter.asLocation)
locations.map(client.protocol2CodeConverter.asLocation),
);
}
}

View File

@ -1,9 +1,6 @@
import { ExtensionContext, commands, window, workspace } from "vscode";
import {
LanguageClient,
LanguageClientOptions,
ServerOptions,
} from "vscode-languageclient/node";
import { type ExtensionContext, commands, window, workspace } from "vscode";
import { LanguageClient, type LanguageClientOptions, type ServerOptions } from "vscode-languageclient/node";
import fs from "node:fs";
import { showReferences } from "./commands";
let client: LanguageClient | undefined;
@ -11,16 +8,25 @@ let client: LanguageClient | undefined;
async function startLanguageClient(context: ExtensionContext) {
try {
const executablePath = (() => {
const executablePath = workspace
.getConfiguration("pylyzer")
.get<string>("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", "");
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", true);
const enableSemanticTokens = workspace.getConfiguration("pylyzer").get<boolean>("semanticTokens", false);
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"];
@ -41,10 +47,34 @@ 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");
@ -84,13 +114,11 @@ 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 "path";
import * as path from "node:path";
import { runTests } from "@vscode/test-electron";

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 KiB

After

Width:  |  Height:  |  Size: 415 KiB

1
mkdocs_build.sh Executable file
View File

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

11
publish.sh Executable file
View File

@ -0,0 +1,11 @@
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,15 +1,2 @@
[build-system]
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",
]
requires = ["setuptools", "setuptools-rust", "wheel", "tomli"]

92
setup.py Normal file
View File

@ -0,0 +1,92 @@
# 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

@ -1,228 +0,0 @@
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, Runnable, Stream};
use erg_common::Str;
use erg_compiler::artifact::{BuildRunnable, Buildable, CompleteArtifact, IncompleteArtifact};
use erg_compiler::context::register::CheckStatus;
use erg_compiler::context::ModuleContext;
use erg_compiler::erg_parser::ast::AST;
use erg_compiler::erg_parser::error::{
CompleteArtifact as PCompleteArtifact, IncompleteArtifact as PIncompleteArtifact, ParseErrors,
};
use erg_compiler::erg_parser::parse::Parsable;
use erg_compiler::error::{CompileError, CompileErrors};
use erg_compiler::lower::ASTLowerer;
use erg_compiler::module::SharedCompilerResource;
use py2erg::{dump_decl_er, reserve_decl_er, ShadowingMode};
use rustpython_parser::parser;
use crate::handle_err;
pub struct SimplePythonParser {}
impl Parsable for SimplePythonParser {
fn parse(code: String) -> Result<PCompleteArtifact, PIncompleteArtifact> {
let py_program = parser::parse_program(&code).map_err(|_err| ParseErrors::empty())?;
let shadowing = if cfg!(feature = "debug") {
ShadowingMode::Visible
} else {
ShadowingMode::Invisible
};
let converter = py2erg::ASTConverter::new(ErgConfig::default(), shadowing);
let art = converter.convert_program(py_program);
if art.errors.is_empty() {
Ok(PCompleteArtifact::new(
art.object.unwrap(),
art.warns.into(),
))
} else {
Err(PIncompleteArtifact::new(
art.object,
art.errors.into(),
art.warns.into(),
))
}
}
}
#[derive(Debug, Default)]
pub struct PythonAnalyzer {
pub cfg: ErgConfig,
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
}
#[inline]
fn cfg_mut(&mut self) -> &mut ErgConfig {
&mut self.cfg
}
fn finish(&mut self) {
self.checker.finish();
}
fn initialize(&mut self) {
self.checker.initialize();
}
fn clear(&mut self) {
self.checker.clear();
}
fn eval(&mut self, src: String) -> Result<String, Self::Errs> {
self.checker.eval(src)
}
fn exec(&mut self) -> Result<ExitStatus, Self::Errs> {
self.checker.exec()
}
}
impl Buildable for PythonAnalyzer {
fn inherit(cfg: ErgConfig, shared: SharedCompilerResource) -> Self {
let mod_name = Str::rc(&cfg.input.file_stem());
Self {
cfg: cfg.copy(),
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 pop_context(&mut self) -> Option<ModuleContext> {
self.checker.pop_mod_ctx()
}
fn get_context(&self) -> Option<&ModuleContext> {
Some(self.checker.get_mod_ctx())
}
}
impl BuildRunnable for PythonAnalyzer {}
impl PythonAnalyzer {
pub fn new(cfg: ErgConfig) -> Self {
Runnable::new(cfg)
}
pub fn analyze(
&mut self,
py_code: String,
mode: &str,
) -> Result<CompleteArtifact, IncompleteArtifact> {
let filename = self.cfg.input.filename();
let py_program = parser::parse_program(&py_code).map_err(|err| {
let core = ErrorCore::new(
vec![],
err.to_string(),
0,
ErrorKind::SyntaxError,
erg_common::error::Location::Line(err.location.row() as u32),
);
let err = CompileError::new(core, self.cfg.input.clone(), "".into());
IncompleteArtifact::new(None, CompileErrors::from(err), CompileErrors::empty())
})?;
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.checker.get_mod_ctx(), artifact.warns);
if errors.is_empty() {
Ok(artifact)
} else {
Err(IncompleteArtifact::new(
Some(artifact.object),
errors,
artifact.warns,
))
}
}
Err(iart) => {
errors.extend(iart.errors);
let errors = handle_err::filter_errors(self.checker.get_mod_ctx(), errors);
warns.extend(iart.warns);
let warns = handle_err::filter_errors(self.checker.get_mod_ctx(), warns);
Err(IncompleteArtifact::new(iart.object, errors, warns))
}
}
}
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}");
match self.analyze(py_code, "exec") {
Ok(artifact) => {
if !artifact.warns.is_empty() {
println!(
"{YELLOW}Found {} warnings{RESET}: {}",
artifact.warns.len(),
self.cfg.input.filename()
);
artifact.warns.write_all_stderr();
}
println!("{GREEN}All checks OK{RESET}: {}", self.cfg.input.filename());
if self.cfg.dist_dir.is_some() {
dump_decl_er(
self.cfg.input.clone(),
artifact.object,
CheckStatus::Succeed,
);
println!("A declaration file has been generated to __pycache__ directory.");
}
std::process::exit(0);
}
Err(artifact) => {
if !artifact.warns.is_empty() {
println!(
"{YELLOW}Found {} warnings{RESET}: {}",
artifact.warns.len(),
self.cfg.input.filename()
);
artifact.warns.write_all_stderr();
}
let code = if artifact.errors.is_empty() {
println!("{GREEN}All checks OK{RESET}: {}", self.cfg.input.filename());
0
} else {
println!(
"{RED}Found {} errors{RESET}: {}",
artifact.errors.len(),
self.cfg.input.filename()
);
artifact.errors.write_all_stderr();
1
};
// Even if type checking fails, some APIs are still valid, so generate a file
if self.cfg.dist_dir.is_some() {
dump_decl_er(
self.cfg.input.clone(),
artifact.object.unwrap(),
CheckStatus::Failed,
);
println!("A declaration file has been generated to __pycache__ directory.");
}
std::process::exit(code);
}
}
}
}

View File

@ -1,10 +1,36 @@
use std::env;
use std::path::PathBuf;
use std::path::{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!(
@ -21,7 +47,10 @@ OPTIONS
--version/-V
--verbose 0|1|2
--server Language Serverを起動
--code/-c cmd ",
--clear-cache
--code/-c cmd
--dump-decl
--disable feat ",
"simplified_chinese" =>
"\
@ -36,7 +65,10 @@ OPTIONS
--version/-V
--verbose 0|1|2
--server Language Server
--code/-c cmd ",
--clear-cache
--code/-c cmd
--dump-decl
--disable feat ",
"traditional_chinese" =>
"\
@ -51,7 +83,10 @@ OPTIONS
--version/-V
--verbose 0|1|2
--server Language Server
--code/-c cmd ",
--clear-cache
--code/-c cmd
--dump-decl
--disable feat ",
"english" =>
"\
@ -66,7 +101,10 @@ OPTIONS
--version/-V show version
--verbose 0|1|2 verbosity level
--server start the Language Server
--code/-c cmd program passed in as string",
--clear-cache clear cache
--code/-c cmd program passed in as string
--dump-decl output type declaration file
--disable feat disable specified features",
)
}
@ -74,12 +112,18 @@ OPTIONS
pub(crate) fn parse_args() -> ErgConfig {
let mut args = env::args();
args.next(); // "pylyzer"
let mut cfg = ErgConfig::default();
let mut cfg = ErgConfig {
effect_check: false,
ownership_check: false,
respect_pyi: true,
..ErgConfig::default()
};
let mut runtime_args: Vec<&'static str> = Vec::new();
while let Some(arg) = args.next() {
match &arg[..] {
"--" => {
for arg in args {
cfg.runtime_args.push(Box::leak(arg.into_boxed_str()));
runtime_args.push(Box::leak(arg.into_boxed_str()));
}
break;
}
@ -104,10 +148,34 @@ 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!(
"\
@ -127,12 +195,75 @@ For more information try `pylyzer --help`"
);
if let Some("--") = args.next().as_ref().map(|s| &s[..]) {
for arg in args {
cfg.runtime_args.push(Box::leak(arg.into_boxed_str()));
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
}

89
src/copy.rs Normal file
View File

@ -0,0 +1,89 @@
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);
}
}
}

View File

@ -1,45 +0,0 @@
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),
}
}

View File

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

View File

@ -1,22 +1,49 @@
mod analyze;
mod config;
mod handle_err;
mod copy;
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 mut lang_server = Server::<PythonAnalyzer, SimplePythonParser>::new(cfg);
lang_server.run().unwrap_or_else(|_| {
std::process::exit(1);
});
let lang_server = Server::<PythonAnalyzer, SimplePythonParser>::new(cfg, None);
lang_server.run();
} else {
let mut code = 0;
let files = files_to_be_checked();
if files.is_empty() {
let mut analyzer = PythonAnalyzer::new(cfg);
analyzer.run();
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);
}
}

19
tests/abc.py Normal file
View File

@ -0,0 +1,19 @@
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,3 +8,17 @@ 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,3 +7,12 @@ 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,3 +1,5 @@
from typing import Self, List
class Empty: pass
emp = Empty()
@ -15,6 +17,10 @@ class C:
return C(self.x + other.x, self.y + other.y)
def method(self):
return self.x
def id(self) -> Self:
return self
def id2(self) -> "C":
return self
c = C(1, 2)
assert c.x == 1
@ -27,6 +33,7 @@ assert d.x == "a" # ERR
a = c.method() # OK
_: int = a + 1
b = C("a").method() # ERR
assert c.id() == c
class D:
c: int
@ -44,6 +51,8 @@ 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)
@ -52,3 +61,79 @@ 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

51
tests/collection.py Normal file
View File

@ -0,0 +1,51 @@
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

View File

@ -1,26 +0,0 @@
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)

11
tests/decl.py Normal file
View File

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

18
tests/dict.py Normal file
View File

@ -0,0 +1,18 @@
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

18
tests/err/class.py Normal file
View File

@ -0,0 +1,18 @@
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

11
tests/err/property.py Normal file
View File

@ -0,0 +1,11 @@
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

8
tests/err/type_spec.py Normal file
View File

@ -0,0 +1,8 @@
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
from .bar import i, Bar, Baz, Qux
from . import bar
from . import baz

View File

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

View File

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

View File

@ -15,3 +15,8 @@ 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,6 +1,8 @@
import export
import foo
from foo import bar
from . import foo
from foo import bar, Bar
from foo.bar import Baz
from foo import baz
import random
from random import randint as rdi
@ -8,6 +10,7 @@ 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)
@ -27,6 +30,13 @@ 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("*"))
@ -39,3 +49,7 @@ assert dt.datetime.max == max_date
Resp = http.client.HTTPResponse
assert export.http.client.HTTPResponse == Resp
_ = bar.Baz
_ = sin(acos(exp(0))) # OK

19
tests/list.py Normal file
View File

@ -0,0 +1,19 @@
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

5
tests/literal.py Normal file
View File

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

View File

@ -7,3 +7,14 @@ 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,17 +1,51 @@
def imaginary(x):
x.imag
return x.imag
def imaginary2(x):
return imaginary(x)
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):
obj.method(x)
return obj.method(x)
def call_method2(obj, x):
return call_method(obj, x)
def call_foo(x):
return x.foo("foo") # OK
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`

11
tests/property.py Normal file
View File

@ -0,0 +1,11 @@
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

25
tests/pyi.py Normal file
View File

@ -0,0 +1,25 @@
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

13
tests/pyi.pyi Normal file
View File

@ -0,0 +1,13 @@
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,3 +1,5 @@
from typing import Literal
i: int = 0
i: str = "a" # OK
@ -12,3 +14,49 @@ 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,9 +10,8 @@ def add2(x: int, y: int) -> str: # ERR
print(add2(1, 2))
# ERR
for i in [1, 2, 3]:
j = i + "aa"
j = i + "aa" # ERR
print(j)
a: int # OK
@ -31,15 +30,12 @@ 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
i, j = 1, 2
assert i == 1
@ -48,3 +44,6 @@ 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,15 +1,17 @@
use std::path::PathBuf;
use std::path::{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::PythonAnalyzer;
use pylyzer_core::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);
@ -17,95 +19,186 @@ pub fn exec_analyzer(file_path: &'static str) -> Result<CompleteArtifact, Incomp
analyzer.analyze(py_code, "exec")
}
fn _expect(file_path: &'static str, warns: usize, errors: usize) {
fn _expect(file_path: &'static str, warns: usize, errors: usize) -> Result<(), String> {
println!("Testing {file_path} ...");
match exec_analyzer(file_path) {
Ok(artifact) => {
assert_eq!(artifact.warns.len(), warns);
assert_eq!(errors, 0);
if artifact.warns.len() != warns {
eprintln!("warns: {}", artifact.warns);
return Err(format!(
"Expected {warns} warnings, found {}",
artifact.warns.len()
));
}
if errors != 0 {
return Err(format!("Expected {errors} errors, found 0"));
}
Ok(())
}
Err(artifact) => {
assert_eq!(artifact.warns.len(), warns);
assert_eq!(artifact.errors.len(), errors);
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()
));
}
Ok(())
}
}
}
pub fn expect(file_path: &'static str, warns: usize, errors: usize) {
exec_new_thread(
move || {
_expect(file_path, warns, errors);
},
file_path,
);
pub fn expect(file_path: &'static str, warns: usize, errors: usize) -> Result<(), String> {
exec_new_thread(move || _expect(file_path, warns, errors), file_path)
}
#[test]
fn exec_test() {
expect("tests/test.py", 0, 11);
fn exec_abc() -> Result<(), String> {
expect("tests/abc.py", 0, 0)
}
#[test]
fn exec_import() {
expect("tests/import.py", 1, 2);
fn exec_test() -> Result<(), String> {
expect("tests/test.py", 0, 11)
}
#[test]
fn exec_export() {
expect("tests/export.py", 0, 0);
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_func() {
expect("tests/func.py", 0, 1);
fn exec_dict() -> Result<(), String> {
expect("tests/dict.py", 0, 2)
}
#[test]
fn exec_class() {
expect("tests/class.py", 0, 4);
fn exec_export() -> Result<(), String> {
expect("tests/export.py", 0, 0)
}
#[test]
fn exec_errors() {
expect("tests/errors.py", 0, 3);
fn exec_func() -> Result<(), String> {
expect("tests/func.py", 0, 1)
}
#[test]
fn exec_warns() {
expect("tests/warns.py", 2, 0);
fn exec_class() -> Result<(), String> {
expect("tests/class.py", 0, 8)
}
#[test]
fn exec_typespec() {
expect("tests/typespec.py", 0, 7);
fn exec_class_err() -> Result<(), String> {
expect("tests/err/class.py", 0, 3)
}
#[test]
fn exec_projection() {
expect("tests/projection.py", 0, 4);
fn exec_errors() -> Result<(), String> {
expect("tests/errors.py", 0, 3)
}
#[test]
fn exec_narrowing() {
expect("tests/narrowing.py", 0, 1);
fn exec_warns() -> Result<(), String> {
expect("tests/warns.py", 2, 0)
}
#[test]
fn exec_casting() {
expect("tests/casting.py", 1, 1);
fn exec_typespec() -> Result<(), String> {
expect("tests/typespec.py", 0, 16)
}
#[test]
fn exec_collections() {
expect("tests/collections.py", 0, 4);
fn exec_projection() -> Result<(), String> {
expect("tests/projection.py", 0, 5)
}
#[test]
fn exec_call() {
expect("tests/call.py", 0, 3);
fn exec_property() -> Result<(), String> {
expect("tests/property.py", 0, 0)
}
#[test]
fn exec_shadowing() {
expect("tests/shadowing.py", 0, 3);
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)
}
#[test]
fn exec_narrowing() -> Result<(), String> {
expect("tests/narrowing.py", 0, 1)
}
#[test]
fn exec_casting() -> Result<(), String> {
expect("tests/casting.py", 4, 1)
}
#[test]
fn exec_collection() -> Result<(), String> {
expect("tests/collection.py", 0, 5)
}
#[test]
fn exec_call() -> Result<(), String> {
expect("tests/call.py", 0, 6)
}
#[test]
fn exec_decl() -> Result<(), String> {
expect("tests/decl.py", 0, 1)
}
#[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)
}
#[test]
fn exec_widening() -> Result<(), String> {
expect("tests/widening.py", 0, 1)
}

View File

@ -1,5 +1,6 @@
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
@ -9,6 +10,23 @@ 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
@ -36,3 +54,21 @@ 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

32
tests/typevar.py Normal file
View File

@ -0,0 +1,32 @@
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

13
tests/union.py Normal file
View File

@ -0,0 +1,13 @@
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

10
tests/widening.py Normal file
View File

@ -0,0 +1,10 @@
b = False
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