import-linter-ls 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- import_linter_ls-0.1.0/LICENSE +21 -0
- import_linter_ls-0.1.0/PKG-INFO +75 -0
- import_linter_ls-0.1.0/README.md +55 -0
- import_linter_ls-0.1.0/pyproject.toml +108 -0
- import_linter_ls-0.1.0/pyproject.toml.orig +97 -0
- import_linter_ls-0.1.0/src/import_linter_ls/__init__.py +5 -0
- import_linter_ls-0.1.0/src/import_linter_ls/__main__.py +13 -0
- import_linter_ls-0.1.0/src/import_linter_ls/config_files.py +24 -0
- import_linter_ls-0.1.0/src/import_linter_ls/py.typed +0 -0
- import_linter_ls-0.1.0/src/import_linter_ls/server.py +90 -0
- import_linter_ls-0.1.0/src/import_linter_ls/violations.py +74 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 daniprol
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: import-linter-ls
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A language server for import-linter: see contract errors and import violations in your editor
|
|
5
|
+
Keywords: import-linter,language-server,lsp,architecture
|
|
6
|
+
Author: daniprol
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
12
|
+
Classifier: Typing :: Typed
|
|
13
|
+
Requires-Dist: import-linter==2.16
|
|
14
|
+
Requires-Dist: pygls>=2.0,<3
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Project-URL: Documentation, https://daniprol.github.io/import-linter-ls/
|
|
17
|
+
Project-URL: Repository, https://github.com/daniprol/import-linter-ls
|
|
18
|
+
Project-URL: Issues, https://github.com/daniprol/import-linter-ls/issues
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# import-linter-ls
|
|
22
|
+
|
|
23
|
+
A language server for [import-linter](https://import-linter.readthedocs.io/), with a VS Code extension.
|
|
24
|
+
See your architecture contract errors and import violations in the editor, not only in CI.
|
|
25
|
+
|
|
26
|
+
> **Status: early.** The server starts and recognises import-linter config files. Diagnostics, hover and completion
|
|
27
|
+
> are coming. See the [roadmap](docs/roadmap.md).
|
|
28
|
+
|
|
29
|
+
## Use it in VS Code
|
|
30
|
+
|
|
31
|
+
1. Install the server in your project:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
uv add --dev import-linter-ls # or: pip install import-linter-ls
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
2. Install the **Import Linter** extension. Until it is on the Marketplace, download the `.vsix` from the
|
|
38
|
+
[latest release](https://github.com/daniprol/import-linter-ls/releases/latest) and run
|
|
39
|
+
**Extensions: Install from VSIX...**.
|
|
40
|
+
3. Select your project's Python environment (**Python: Select Interpreter**) and open your project.
|
|
41
|
+
|
|
42
|
+
It works with `.importlinter`, `setup.cfg` and `pyproject.toml` configs. Check **Output > Import Linter** for logs.
|
|
43
|
+
|
|
44
|
+
| Setting | Purpose |
|
|
45
|
+
|---|---|
|
|
46
|
+
| `importLinterLs.interpreter` | Python that has `import-linter-ls`. Empty: use the one selected in VS Code. |
|
|
47
|
+
| `importLinterLs.trace.server` | `off`, `messages` or `verbose`: log the editor/server messages. |
|
|
48
|
+
|
|
49
|
+
Command: **Import Linter: Restart Language Server**.
|
|
50
|
+
|
|
51
|
+
## Use it in another editor
|
|
52
|
+
|
|
53
|
+
Any editor with an LSP client can run the server over stdio with `import-linter-ls`
|
|
54
|
+
(or `python -m import_linter_ls`).
|
|
55
|
+
|
|
56
|
+
## Develop
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
uv sync && pnpm install
|
|
60
|
+
uv run prek install # git hooks
|
|
61
|
+
uv run pytest # unit + integration tests
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
To try the extension, open this folder in VS Code and press `F5`, then open `examples/layered_app`.
|
|
65
|
+
More in [CONTRIBUTING.md](CONTRIBUTING.md) and [AGENTS.md](AGENTS.md).
|
|
66
|
+
|
|
67
|
+
## Layout
|
|
68
|
+
|
|
69
|
+
| Path | What |
|
|
70
|
+
|---|---|
|
|
71
|
+
| `src/import_linter_ls/` | The language server (Python, [pygls](https://pygls.readthedocs.io/)). |
|
|
72
|
+
| `vscode-extension/` | The VS Code extension (TypeScript), a thin client. |
|
|
73
|
+
| `examples/layered_app/` | Example app with one broken contract, in TOML and INI. |
|
|
74
|
+
| `tests/` | Unit and integration tests. |
|
|
75
|
+
| `docs/` | Documentation site ([zensical](https://zensical.org/)). |
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# import-linter-ls
|
|
2
|
+
|
|
3
|
+
A language server for [import-linter](https://import-linter.readthedocs.io/), with a VS Code extension.
|
|
4
|
+
See your architecture contract errors and import violations in the editor, not only in CI.
|
|
5
|
+
|
|
6
|
+
> **Status: early.** The server starts and recognises import-linter config files. Diagnostics, hover and completion
|
|
7
|
+
> are coming. See the [roadmap](docs/roadmap.md).
|
|
8
|
+
|
|
9
|
+
## Use it in VS Code
|
|
10
|
+
|
|
11
|
+
1. Install the server in your project:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
uv add --dev import-linter-ls # or: pip install import-linter-ls
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
2. Install the **Import Linter** extension. Until it is on the Marketplace, download the `.vsix` from the
|
|
18
|
+
[latest release](https://github.com/daniprol/import-linter-ls/releases/latest) and run
|
|
19
|
+
**Extensions: Install from VSIX...**.
|
|
20
|
+
3. Select your project's Python environment (**Python: Select Interpreter**) and open your project.
|
|
21
|
+
|
|
22
|
+
It works with `.importlinter`, `setup.cfg` and `pyproject.toml` configs. Check **Output > Import Linter** for logs.
|
|
23
|
+
|
|
24
|
+
| Setting | Purpose |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `importLinterLs.interpreter` | Python that has `import-linter-ls`. Empty: use the one selected in VS Code. |
|
|
27
|
+
| `importLinterLs.trace.server` | `off`, `messages` or `verbose`: log the editor/server messages. |
|
|
28
|
+
|
|
29
|
+
Command: **Import Linter: Restart Language Server**.
|
|
30
|
+
|
|
31
|
+
## Use it in another editor
|
|
32
|
+
|
|
33
|
+
Any editor with an LSP client can run the server over stdio with `import-linter-ls`
|
|
34
|
+
(or `python -m import_linter_ls`).
|
|
35
|
+
|
|
36
|
+
## Develop
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
uv sync && pnpm install
|
|
40
|
+
uv run prek install # git hooks
|
|
41
|
+
uv run pytest # unit + integration tests
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
To try the extension, open this folder in VS Code and press `F5`, then open `examples/layered_app`.
|
|
45
|
+
More in [CONTRIBUTING.md](CONTRIBUTING.md) and [AGENTS.md](AGENTS.md).
|
|
46
|
+
|
|
47
|
+
## Layout
|
|
48
|
+
|
|
49
|
+
| Path | What |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `src/import_linter_ls/` | The language server (Python, [pygls](https://pygls.readthedocs.io/)). |
|
|
52
|
+
| `vscode-extension/` | The VS Code extension (TypeScript), a thin client. |
|
|
53
|
+
| `examples/layered_app/` | Example app with one broken contract, in TOML and INI. |
|
|
54
|
+
| `tests/` | Unit and integration tests. |
|
|
55
|
+
| `docs/` | Documentation site ([zensical](https://zensical.org/)). |
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "import-linter-ls"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A language server for import-linter: see contract errors and import violations in your editor"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
keywords = [
|
|
9
|
+
"import-linter",
|
|
10
|
+
"language-server",
|
|
11
|
+
"lsp",
|
|
12
|
+
"architecture",
|
|
13
|
+
]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
18
|
+
"Typing :: Typed",
|
|
19
|
+
]
|
|
20
|
+
requires-python = ">=3.11"
|
|
21
|
+
dependencies = [
|
|
22
|
+
"import-linter==2.16",
|
|
23
|
+
"pygls>=2.0,<3",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[[project.authors]]
|
|
27
|
+
name = "daniprol"
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
import-linter-ls = "import_linter_ls.__main__:main"
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Documentation = "https://daniprol.github.io/import-linter-ls/"
|
|
34
|
+
Repository = "https://github.com/daniprol/import-linter-ls"
|
|
35
|
+
Issues = "https://github.com/daniprol/import-linter-ls/issues"
|
|
36
|
+
|
|
37
|
+
[build-system]
|
|
38
|
+
requires = ["uv_build>=0.9,<0.12"]
|
|
39
|
+
build-backend = "uv_build"
|
|
40
|
+
|
|
41
|
+
[tool.uv.workspace]
|
|
42
|
+
members = ["examples/layered_app"]
|
|
43
|
+
|
|
44
|
+
[tool.uv.sources.layered-app]
|
|
45
|
+
workspace = true
|
|
46
|
+
|
|
47
|
+
[tool.ruff]
|
|
48
|
+
line-length = 120
|
|
49
|
+
target-version = "py311"
|
|
50
|
+
src = [
|
|
51
|
+
"src",
|
|
52
|
+
"tests",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
[tool.ruff.lint]
|
|
56
|
+
select = [
|
|
57
|
+
"E",
|
|
58
|
+
"W",
|
|
59
|
+
"F",
|
|
60
|
+
"I",
|
|
61
|
+
"N",
|
|
62
|
+
"UP",
|
|
63
|
+
"B",
|
|
64
|
+
"A",
|
|
65
|
+
"C4",
|
|
66
|
+
"SIM",
|
|
67
|
+
"RET",
|
|
68
|
+
"PIE",
|
|
69
|
+
"PTH",
|
|
70
|
+
"PT",
|
|
71
|
+
"ASYNC",
|
|
72
|
+
"RUF",
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
[tool.ruff.format]
|
|
76
|
+
docstring-code-format = true
|
|
77
|
+
|
|
78
|
+
[tool.ty.rules]
|
|
79
|
+
missing-type-argument = "error"
|
|
80
|
+
|
|
81
|
+
[tool.pytest.ini_options]
|
|
82
|
+
testpaths = ["tests"]
|
|
83
|
+
asyncio_mode = "auto"
|
|
84
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
85
|
+
addopts = [
|
|
86
|
+
"--strict-markers",
|
|
87
|
+
"--strict-config",
|
|
88
|
+
]
|
|
89
|
+
markers = [
|
|
90
|
+
"unit: fast tests of one function, no server",
|
|
91
|
+
"integration: starts the real language server and talks LSP to it",
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
[tool.coverage.run]
|
|
95
|
+
source = ["import_linter_ls"]
|
|
96
|
+
branch = true
|
|
97
|
+
|
|
98
|
+
[dependency-groups]
|
|
99
|
+
dev = [
|
|
100
|
+
"layered-app",
|
|
101
|
+
"prek>=0.2",
|
|
102
|
+
"pytest>=8",
|
|
103
|
+
"pytest-cov>=6",
|
|
104
|
+
"pytest-lsp>=1.0",
|
|
105
|
+
"ruff>=0.14",
|
|
106
|
+
"ty>=0.0.1",
|
|
107
|
+
]
|
|
108
|
+
docs = ["zensical>=0.0.20"]
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "import-linter-ls"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A language server for import-linter: see contract errors and import violations in your editor"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [{ name = "daniprol" }]
|
|
9
|
+
keywords = ["import-linter", "language-server", "lsp", "architecture"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 3 - Alpha",
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
14
|
+
"Typing :: Typed",
|
|
15
|
+
]
|
|
16
|
+
# Same minimum as import-linter itself.
|
|
17
|
+
requires-python = ">=3.11"
|
|
18
|
+
dependencies = [
|
|
19
|
+
"import-linter==2.16",
|
|
20
|
+
"pygls>=2.0,<3",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
import-linter-ls = "import_linter_ls.__main__:main"
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Documentation = "https://daniprol.github.io/import-linter-ls/"
|
|
28
|
+
Repository = "https://github.com/daniprol/import-linter-ls"
|
|
29
|
+
Issues = "https://github.com/daniprol/import-linter-ls/issues"
|
|
30
|
+
|
|
31
|
+
[build-system]
|
|
32
|
+
requires = ["uv_build>=0.9,<0.12"]
|
|
33
|
+
build-backend = "uv_build"
|
|
34
|
+
|
|
35
|
+
# The example app is a workspace member so `uv sync` installs it too.
|
|
36
|
+
[tool.uv.workspace]
|
|
37
|
+
members = ["examples/layered_app"]
|
|
38
|
+
|
|
39
|
+
[dependency-groups]
|
|
40
|
+
dev = [
|
|
41
|
+
"layered-app",
|
|
42
|
+
"prek>=0.2",
|
|
43
|
+
"pytest>=8",
|
|
44
|
+
"pytest-cov>=6",
|
|
45
|
+
"pytest-lsp>=1.0",
|
|
46
|
+
"ruff>=0.14",
|
|
47
|
+
"ty>=0.0.1",
|
|
48
|
+
]
|
|
49
|
+
docs = ["zensical>=0.0.20"] # uv run --only-group docs zensical serve
|
|
50
|
+
|
|
51
|
+
[tool.uv.sources]
|
|
52
|
+
layered-app = { workspace = true }
|
|
53
|
+
|
|
54
|
+
[tool.ruff]
|
|
55
|
+
line-length = 120
|
|
56
|
+
target-version = "py311"
|
|
57
|
+
src = ["src", "tests"]
|
|
58
|
+
|
|
59
|
+
[tool.ruff.lint]
|
|
60
|
+
select = [
|
|
61
|
+
"E", "W", # pycodestyle
|
|
62
|
+
"F", # pyflakes
|
|
63
|
+
"I", # import sorting
|
|
64
|
+
"N", # naming
|
|
65
|
+
"UP", # modern syntax for Python 3.11+
|
|
66
|
+
"B", # likely bugs
|
|
67
|
+
"A", # shadowing builtins
|
|
68
|
+
"C4", # comprehensions
|
|
69
|
+
"SIM", # simplifications
|
|
70
|
+
"RET", # return statements
|
|
71
|
+
"PIE", # misc. clean-ups
|
|
72
|
+
"PTH", # pathlib over os.path
|
|
73
|
+
"PT", # pytest style
|
|
74
|
+
"ASYNC", # asyncio pitfalls
|
|
75
|
+
"RUF", # ruff-specific
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
[tool.ruff.format]
|
|
79
|
+
docstring-code-format = true
|
|
80
|
+
|
|
81
|
+
[tool.ty.rules]
|
|
82
|
+
missing-type-argument = "error" # `list[str]`, not `list`: a bare generic hides mistakes
|
|
83
|
+
|
|
84
|
+
[tool.pytest.ini_options]
|
|
85
|
+
testpaths = ["tests"]
|
|
86
|
+
asyncio_mode = "auto"
|
|
87
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
88
|
+
# `unit` tests are fast and need no server. `integration` tests start the real server and talk LSP to it.
|
|
89
|
+
addopts = ["--strict-markers", "--strict-config"]
|
|
90
|
+
markers = [
|
|
91
|
+
"unit: fast tests of one function, no server",
|
|
92
|
+
"integration: starts the real language server and talks LSP to it",
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
[tool.coverage.run]
|
|
96
|
+
source = ["import_linter_ls"]
|
|
97
|
+
branch = true
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Entry point: `python -m import_linter_ls` or the `import-linter-ls` command."""
|
|
2
|
+
|
|
3
|
+
from import_linter_ls.server import server
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main() -> None:
|
|
7
|
+
# Editors start the server as a child process and talk to it over stdin and stdout,
|
|
8
|
+
# so never print to stdout: it would corrupt the protocol messages. Use `log` from server.py instead.
|
|
9
|
+
server.start_io()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
if __name__ == "__main__":
|
|
13
|
+
main()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Finding out which files are import-linter config files."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
# import-linter reads its config from these files (see its docs, "Usage").
|
|
6
|
+
# `.importlinter` and `setup.cfg` use the INI format, `pyproject.toml` uses TOML.
|
|
7
|
+
INI_CONFIG_FILE_NAMES = frozenset({".importlinter", "setup.cfg"})
|
|
8
|
+
TOML_CONFIG_FILE_NAMES = frozenset({"pyproject.toml"})
|
|
9
|
+
|
|
10
|
+
# Some projects pass their own file with `lint-imports --config my_file`. We cannot know those names,
|
|
11
|
+
# so we only recognise the three default ones for now.
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def config_format(path: Path) -> str | None:
|
|
15
|
+
"""Return "ini" or "toml" if `path` can hold import-linter config, otherwise None.
|
|
16
|
+
|
|
17
|
+
Only the file name is checked. A `pyproject.toml` or `setup.cfg` is only a *candidate*: it
|
|
18
|
+
holds import-linter config only if it has an `importlinter` section.
|
|
19
|
+
"""
|
|
20
|
+
if path.name in INI_CONFIG_FILE_NAMES:
|
|
21
|
+
return "ini"
|
|
22
|
+
if path.name in TOML_CONFIG_FILE_NAMES:
|
|
23
|
+
return "toml"
|
|
24
|
+
return None
|
|
File without changes
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""The language server: connects pygls to our features.
|
|
2
|
+
|
|
3
|
+
This file only wires things together. The actual logic lives in small modules next to it.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from lsprotocol import types
|
|
10
|
+
from pygls.lsp.server import LanguageServer
|
|
11
|
+
from pygls.uris import to_fs_path
|
|
12
|
+
|
|
13
|
+
from import_linter_ls import __version__
|
|
14
|
+
from import_linter_ls.violations import Violation, find_project_dir, parse_report, run_import_linter
|
|
15
|
+
|
|
16
|
+
# A language server is a program that an editor starts and talks to with JSON messages (the Language
|
|
17
|
+
# Server Protocol, LSP). The editor sends events ("the user opened this file") and we answer or
|
|
18
|
+
# send back results ("these lines have problems"). pygls handles the JSON and the protocol for us.
|
|
19
|
+
server = LanguageServer("import-linter-ls", __version__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def log(message: str) -> None:
|
|
23
|
+
"""Send a message to the editor's log (VS Code shows it in the extension's output channel)."""
|
|
24
|
+
server.window_log_message(types.LogMessageParams(type=types.MessageType.Info, message=message))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@server.feature(types.INITIALIZED)
|
|
28
|
+
def on_initialized(_params: types.InitializedParams) -> None:
|
|
29
|
+
"""The editor finished the handshake: from now on it may send us events."""
|
|
30
|
+
log(f"import-linter-ls {__version__} is ready")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Files that currently show our diagnostics. We must send an empty list for a file once it is fixed.
|
|
34
|
+
files_with_diagnostics: set[str] = set()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@server.feature(types.TEXT_DOCUMENT_DID_OPEN)
|
|
38
|
+
async def on_document_opened(params: types.DidOpenTextDocumentParams) -> None:
|
|
39
|
+
"""The user opened a file: check the project, so the editor shows problems right away."""
|
|
40
|
+
await check_project(params.text_document.uri)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@server.feature(types.TEXT_DOCUMENT_DID_SAVE)
|
|
44
|
+
async def on_document_saved(params: types.DidSaveTextDocumentParams) -> None:
|
|
45
|
+
"""The user saved a file: check the project again."""
|
|
46
|
+
await check_project(params.text_document.uri)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def check_project(document_uri: str) -> None:
|
|
50
|
+
"""Run import-linter for the project that owns the document and publish what it finds."""
|
|
51
|
+
file_path = to_fs_path(document_uri)
|
|
52
|
+
if file_path is None: # not a file on disk, for example an unsaved "untitled:" document
|
|
53
|
+
return
|
|
54
|
+
project_dir = find_project_dir(Path(file_path))
|
|
55
|
+
if project_dir is None: # no import-linter config nearby, nothing to check
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
log(f"Running import-linter in {project_dir}")
|
|
59
|
+
# import-linter takes a few seconds on big projects. Run it in a thread so the server can keep answering.
|
|
60
|
+
report = await asyncio.to_thread(run_import_linter, project_dir)
|
|
61
|
+
publish_diagnostics(parse_report(report, project_dir))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def publish_diagnostics(violations: list[Violation]) -> None:
|
|
65
|
+
"""Send the violations to the editor. A "diagnostic" is a problem on a range of a file (a squiggly line)."""
|
|
66
|
+
diagnostics_by_uri: dict[str, list[types.Diagnostic]] = {uri: [] for uri in files_with_diagnostics}
|
|
67
|
+
for violation in violations:
|
|
68
|
+
uri = violation.file.as_uri()
|
|
69
|
+
diagnostics_by_uri.setdefault(uri, []).append(to_diagnostic(violation))
|
|
70
|
+
|
|
71
|
+
for uri, diagnostics in diagnostics_by_uri.items():
|
|
72
|
+
server.text_document_publish_diagnostics(types.PublishDiagnosticsParams(uri=uri, diagnostics=diagnostics))
|
|
73
|
+
files_with_diagnostics.clear()
|
|
74
|
+
files_with_diagnostics.update(uri for uri, diagnostics in diagnostics_by_uri.items() if diagnostics)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def to_diagnostic(violation: Violation) -> types.Diagnostic:
|
|
78
|
+
"""Underline the whole line of the import."""
|
|
79
|
+
# LSP positions start at 0 for both line and character, while import-linter's line numbers start at 1.
|
|
80
|
+
line_index = violation.line - 1
|
|
81
|
+
line_text = violation.file.read_text(errors="replace").splitlines()[line_index]
|
|
82
|
+
return types.Diagnostic(
|
|
83
|
+
range=types.Range(
|
|
84
|
+
start=types.Position(line=line_index, character=0),
|
|
85
|
+
end=types.Position(line=line_index, character=len(line_text)),
|
|
86
|
+
),
|
|
87
|
+
message=f"{violation.importer} imports {violation.imported}, which breaks an import-linter contract",
|
|
88
|
+
severity=types.DiagnosticSeverity.Error,
|
|
89
|
+
source="import-linter",
|
|
90
|
+
)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Run import-linter on a project and turn its report into a list of violations.
|
|
2
|
+
|
|
3
|
+
This is a throwaway first version: it runs `lint-imports` and reads its text output. It will be
|
|
4
|
+
replaced by a module that talks to import-linter directly (see docs/roadmap.md).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from import_linter_ls.config_files import config_format
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class Violation:
|
|
18
|
+
"""One import that breaks a contract. `line` starts at 1, like in an editor's status bar."""
|
|
19
|
+
|
|
20
|
+
file: Path
|
|
21
|
+
line: int
|
|
22
|
+
importer: str
|
|
23
|
+
imported: str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def find_project_dir(file: Path) -> Path | None:
|
|
27
|
+
"""Walk up from `file` to the first folder with an import-linter config file."""
|
|
28
|
+
for folder in file.parents:
|
|
29
|
+
for candidate in folder.iterdir():
|
|
30
|
+
if config_format(candidate) and "importlinter" in candidate.read_text(errors="replace"):
|
|
31
|
+
return folder
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def run_import_linter(project_dir: Path) -> str:
|
|
36
|
+
"""Run `lint-imports` in `project_dir` and return its report (the output is the same for pass and fail)."""
|
|
37
|
+
# Start the `lint-imports` command through Python so that it uses the same environment as the
|
|
38
|
+
# server, on every operating system. `--no-cache` makes sure that a saved file is analysed again.
|
|
39
|
+
code = "from importlinter.cli import lint_imports_command; lint_imports_command()"
|
|
40
|
+
result = subprocess.run(
|
|
41
|
+
[sys.executable, "-c", code, "--no-cache"],
|
|
42
|
+
cwd=project_dir,
|
|
43
|
+
capture_output=True,
|
|
44
|
+
text=True,
|
|
45
|
+
check=False, # exit code 1 only means "there are broken contracts"
|
|
46
|
+
)
|
|
47
|
+
return result.stdout
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# A broken import is reported like this: `- layered_app.low.storage -> layered_app.high.report (l.4)`.
|
|
51
|
+
# When one module imports the same thing twice there are several line numbers: `(l.4, l.9)`.
|
|
52
|
+
VIOLATION_LINE = re.compile(r"(?P<importer>[\w.]+) -> (?P<imported>[\w.]+) \((?P<lines>l\.\d+(?:, l\.\d+)*)\)")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def parse_report(report: str, project_dir: Path) -> list[Violation]:
|
|
56
|
+
"""Find the broken imports in the text printed by `lint-imports`."""
|
|
57
|
+
violations = []
|
|
58
|
+
for match in VIOLATION_LINE.finditer(report):
|
|
59
|
+
file = module_to_file(match["importer"], project_dir)
|
|
60
|
+
if file is None:
|
|
61
|
+
continue
|
|
62
|
+
for line_number in re.findall(r"\d+", match["lines"]):
|
|
63
|
+
violations.append(Violation(file, int(line_number), match["importer"], match["imported"]))
|
|
64
|
+
return violations
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def module_to_file(module: str, project_dir: Path) -> Path | None:
|
|
68
|
+
"""Find the source file of a module like `layered_app.low.storage`, in the project or in its `src` folder."""
|
|
69
|
+
relative = Path(*module.split("."))
|
|
70
|
+
for source_root in (project_dir, project_dir / "src"):
|
|
71
|
+
for candidate in (source_root / relative.with_suffix(".py"), source_root / relative / "__init__.py"):
|
|
72
|
+
if candidate.is_file():
|
|
73
|
+
return candidate
|
|
74
|
+
return None
|