java-functional-lsp 0.3.1__tar.gz → 0.3.2__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.
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/.claude-plugin/plugin.json +1 -1
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/PKG-INFO +20 -3
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/README.md +19 -2
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/editors/vscode/package.json +1 -1
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/pyproject.toml +1 -1
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/src/java_functional_lsp/__init__.py +1 -1
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/src/java_functional_lsp/analyzers/base.py +52 -15
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/src/java_functional_lsp/analyzers/exception_checker.py +2 -4
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/src/java_functional_lsp/analyzers/mutation_checker.py +12 -3
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/src/java_functional_lsp/server.py +8 -4
- java_functional_lsp-0.3.2/tests/test_base.py +122 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/tests/test_exception_checker.py +16 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/tests/test_mutation_checker.py +38 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/.github/CODEOWNERS +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/.github/ISSUE_TEMPLATE/bug-report.md +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/.github/ISSUE_TEMPLATE/feature-request.md +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/.github/SECURITY.md +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/.github/dependabot.yml +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/.github/release-drafter.yml +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/.github/workflows/publish.yml +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/.github/workflows/release-drafter.yml +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/.github/workflows/stale.yml +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/.github/workflows/test.yml +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/.github/workflows/update-homebrew.yml +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/.github/workflows/vscode-ext.yml +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/.gitignore +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/CONTRIBUTING.md +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/LICENSE +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/SKILL.md +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/commands/lint-java.md +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/editors/intellij/README.md +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/editors/intellij/lsp4ij-template.json +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/editors/vscode/.vscodeignore +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/editors/vscode/README.md +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/editors/vscode/package-lock.json +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/editors/vscode/src/extension.ts +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/editors/vscode/tsconfig.json +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/hooks/hooks.json +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/hooks/java_linter_reminder.py +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/scripts/ensure-lsp.sh +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/scripts/generate-formula.py +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/src/java_functional_lsp/analyzers/__init__.py +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/src/java_functional_lsp/analyzers/null_checker.py +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/src/java_functional_lsp/analyzers/spring_checker.py +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/src/java_functional_lsp/cli.py +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/src/java_functional_lsp/proxy.py +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/tests/__init__.py +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/tests/conftest.py +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/tests/test_cli.py +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/tests/test_config.py +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/tests/test_null_checker.py +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/tests/test_proxy.py +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/tests/test_spring_checker.py +0 -0
- {java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: java-functional-lsp
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Java LSP server enforcing functional programming best practices — null safety, immutability, no exceptions
|
|
5
5
|
Project-URL: Homepage, https://github.com/aviadshiber/java-functional-lsp
|
|
6
6
|
Project-URL: Repository, https://github.com/aviadshiber/java-functional-lsp
|
|
@@ -63,13 +63,28 @@ pip install java-functional-lsp
|
|
|
63
63
|
|
|
64
64
|
# From source
|
|
65
65
|
pip install git+https://github.com/aviadshiber/java-functional-lsp.git
|
|
66
|
+
|
|
67
|
+
# Optional: install jdtls for full Java language support (completions, hover, go-to-def)
|
|
68
|
+
brew install jdtls
|
|
66
69
|
```
|
|
67
70
|
|
|
71
|
+
**Requirements:**
|
|
72
|
+
- Python 3.10+ (for the LSP server)
|
|
73
|
+
- JDK 21+ (only if using jdtls — jdtls 1.57+ requires JDK 21 as its runtime, but can analyze Java 8+ source code)
|
|
74
|
+
|
|
68
75
|
## IDE Setup
|
|
69
76
|
|
|
70
77
|
### VS Code
|
|
71
78
|
|
|
72
|
-
Install the extension from a `.vsix` file ([download from releases](https://github.com/aviadshiber/java-functional-lsp/releases))
|
|
79
|
+
Install the extension from a `.vsix` file ([download from releases](https://github.com/aviadshiber/java-functional-lsp/releases)):
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Download and install
|
|
83
|
+
gh release download --repo aviadshiber/java-functional-lsp --pattern "*.vsix" --dir /tmp
|
|
84
|
+
code --install-extension /tmp/java-functional-lsp-*.vsix
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Or build from source:
|
|
73
88
|
|
|
74
89
|
```bash
|
|
75
90
|
cd editors/vscode
|
|
@@ -78,7 +93,9 @@ npx vsce package
|
|
|
78
93
|
code --install-extension java-functional-lsp-*.vsix
|
|
79
94
|
```
|
|
80
95
|
|
|
81
|
-
The extension
|
|
96
|
+
The extension is a thin launcher — it just starts the `java-functional-lsp` binary for `.java` files. **Updating rules only requires upgrading the LSP binary** (`brew upgrade java-functional-lsp` or `pip install --upgrade java-functional-lsp`). The VSIX itself rarely needs updating.
|
|
97
|
+
|
|
98
|
+
Configure the binary path in settings if needed (`javaFunctionalLsp.serverPath`). See [editors/vscode/README.md](editors/vscode/README.md) for details.
|
|
82
99
|
|
|
83
100
|
### IntelliJ IDEA
|
|
84
101
|
|
|
@@ -35,13 +35,28 @@ pip install java-functional-lsp
|
|
|
35
35
|
|
|
36
36
|
# From source
|
|
37
37
|
pip install git+https://github.com/aviadshiber/java-functional-lsp.git
|
|
38
|
+
|
|
39
|
+
# Optional: install jdtls for full Java language support (completions, hover, go-to-def)
|
|
40
|
+
brew install jdtls
|
|
38
41
|
```
|
|
39
42
|
|
|
43
|
+
**Requirements:**
|
|
44
|
+
- Python 3.10+ (for the LSP server)
|
|
45
|
+
- JDK 21+ (only if using jdtls — jdtls 1.57+ requires JDK 21 as its runtime, but can analyze Java 8+ source code)
|
|
46
|
+
|
|
40
47
|
## IDE Setup
|
|
41
48
|
|
|
42
49
|
### VS Code
|
|
43
50
|
|
|
44
|
-
Install the extension from a `.vsix` file ([download from releases](https://github.com/aviadshiber/java-functional-lsp/releases))
|
|
51
|
+
Install the extension from a `.vsix` file ([download from releases](https://github.com/aviadshiber/java-functional-lsp/releases)):
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# Download and install
|
|
55
|
+
gh release download --repo aviadshiber/java-functional-lsp --pattern "*.vsix" --dir /tmp
|
|
56
|
+
code --install-extension /tmp/java-functional-lsp-*.vsix
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Or build from source:
|
|
45
60
|
|
|
46
61
|
```bash
|
|
47
62
|
cd editors/vscode
|
|
@@ -50,7 +65,9 @@ npx vsce package
|
|
|
50
65
|
code --install-extension java-functional-lsp-*.vsix
|
|
51
66
|
```
|
|
52
67
|
|
|
53
|
-
The extension
|
|
68
|
+
The extension is a thin launcher — it just starts the `java-functional-lsp` binary for `.java` files. **Updating rules only requires upgrading the LSP binary** (`brew upgrade java-functional-lsp` or `pip install --upgrade java-functional-lsp`). The VSIX itself rarely needs updating.
|
|
69
|
+
|
|
70
|
+
Configure the binary path in settings if needed (`javaFunctionalLsp.serverPath`). See [editors/vscode/README.md](editors/vscode/README.md) for details.
|
|
54
71
|
|
|
55
72
|
### IntelliJ IDEA
|
|
56
73
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "java-functional-lsp",
|
|
3
3
|
"displayName": "Java Functional LSP",
|
|
4
4
|
"description": "Java LSP server enforcing functional programming best practices — null safety, immutability, no exceptions",
|
|
5
|
-
"version": "0.3.
|
|
5
|
+
"version": "0.3.2",
|
|
6
6
|
"publisher": "aviadshiber",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"engines": {
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "java-functional-lsp"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.2"
|
|
8
8
|
description = "Java LSP server enforcing functional programming best practices — null safety, immutability, no exceptions"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
{java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/src/java_functional_lsp/analyzers/base.py
RENAMED
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
from collections.abc import Generator
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from enum import IntEnum
|
|
8
|
-
from typing import Any, Protocol
|
|
8
|
+
from typing import Any, Protocol, cast
|
|
9
9
|
|
|
10
10
|
import tree_sitter_java as tsjava
|
|
11
11
|
from tree_sitter import Language, Node, Parser
|
|
@@ -60,20 +60,57 @@ def get_language() -> Language:
|
|
|
60
60
|
return _language
|
|
61
61
|
|
|
62
62
|
|
|
63
|
-
def find_nodes(
|
|
64
|
-
"""
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
63
|
+
def find_nodes(root: Node, type_name: str) -> Generator[Node, None, None]:
|
|
64
|
+
"""Find all descendant nodes of a given type using TreeCursor for performance."""
|
|
65
|
+
cursor = root.walk()
|
|
66
|
+
visited_children = False
|
|
67
|
+
while True:
|
|
68
|
+
if not visited_children:
|
|
69
|
+
current = cast(Node, cursor.node)
|
|
70
|
+
if current.type == type_name:
|
|
71
|
+
yield current
|
|
72
|
+
if not cursor.goto_first_child():
|
|
73
|
+
visited_children = True
|
|
74
|
+
elif cursor.goto_next_sibling():
|
|
75
|
+
visited_children = False
|
|
76
|
+
elif not cursor.goto_parent():
|
|
77
|
+
break
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def find_nodes_multi(root: Node, type_names: set[str]) -> Generator[Node, None, None]:
|
|
81
|
+
"""Find all descendant nodes matching any of the given types using TreeCursor."""
|
|
82
|
+
cursor = root.walk()
|
|
83
|
+
visited_children = False
|
|
84
|
+
while True:
|
|
85
|
+
if not visited_children:
|
|
86
|
+
current = cast(Node, cursor.node)
|
|
87
|
+
if current.type in type_names:
|
|
88
|
+
yield current
|
|
89
|
+
if not cursor.goto_first_child():
|
|
90
|
+
visited_children = True
|
|
91
|
+
elif cursor.goto_next_sibling():
|
|
92
|
+
visited_children = False
|
|
93
|
+
elif not cursor.goto_parent():
|
|
94
|
+
break
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def collect_nodes_by_type(root: Node, type_names: set[str]) -> dict[str, list[Node]]:
|
|
98
|
+
"""Walk tree once, bucket nodes by type. Avoids multiple full traversals."""
|
|
99
|
+
buckets: dict[str, list[Node]] = {t: [] for t in type_names}
|
|
100
|
+
cursor = root.walk()
|
|
101
|
+
visited_children = False
|
|
102
|
+
while True:
|
|
103
|
+
if not visited_children:
|
|
104
|
+
current = cast(Node, cursor.node)
|
|
105
|
+
if current.type in buckets:
|
|
106
|
+
buckets[current.type].append(current)
|
|
107
|
+
if not cursor.goto_first_child():
|
|
108
|
+
visited_children = True
|
|
109
|
+
elif cursor.goto_next_sibling():
|
|
110
|
+
visited_children = False
|
|
111
|
+
elif not cursor.goto_parent():
|
|
112
|
+
break
|
|
113
|
+
return buckets
|
|
77
114
|
|
|
78
115
|
|
|
79
116
|
def find_ancestor(node: Node, type_name: str) -> Node | None:
|
|
@@ -43,10 +43,8 @@ class ExceptionChecker:
|
|
|
43
43
|
body = node.child_by_field_name("body")
|
|
44
44
|
if body is None:
|
|
45
45
|
continue
|
|
46
|
-
# Check if the block has exactly one statement and it's a throw
|
|
47
|
-
statements = [
|
|
48
|
-
c for c in body.children if c.type not in ("{", "}", "comment", "line_comment", "block_comment")
|
|
49
|
-
]
|
|
46
|
+
# Check if the block has exactly one named statement and it's a throw
|
|
47
|
+
statements = [c for c in body.named_children if c.type not in ("line_comment", "block_comment")]
|
|
50
48
|
if len(statements) == 1 and statements[0].type == "throw_statement":
|
|
51
49
|
diagnostics.append(
|
|
52
50
|
Diagnostic(
|
|
@@ -111,10 +111,19 @@ class MutationChecker:
|
|
|
111
111
|
if name_node.text not in _CHECK_METHODS:
|
|
112
112
|
continue
|
|
113
113
|
|
|
114
|
-
# Check if the body contains .get() on the same object
|
|
114
|
+
# Check if the if-body contains .get() on the same object (AST-based)
|
|
115
115
|
obj_name = obj_node.text
|
|
116
|
-
|
|
117
|
-
if
|
|
116
|
+
consequence = if_node.child_by_field_name("consequence")
|
|
117
|
+
if consequence is None or obj_name is None:
|
|
118
|
+
continue
|
|
119
|
+
found_get = False
|
|
120
|
+
for call in find_nodes(consequence, "method_invocation"):
|
|
121
|
+
call_name = call.child_by_field_name("name")
|
|
122
|
+
call_obj = call.child_by_field_name("object")
|
|
123
|
+
if call_name and call_name.text == b"get" and call_obj and call_obj.text == obj_name:
|
|
124
|
+
found_get = True
|
|
125
|
+
break
|
|
126
|
+
if found_get:
|
|
118
127
|
diagnostics.append(
|
|
119
128
|
Diagnostic(
|
|
120
129
|
line=if_node.start_point[0],
|
|
@@ -46,6 +46,7 @@ class JavaFunctionalLspServer(LanguageServer):
|
|
|
46
46
|
self._parser = get_parser()
|
|
47
47
|
self._config: dict[str, Any] = {}
|
|
48
48
|
self._init_params: dict[str, Any] = {}
|
|
49
|
+
self._trees: dict[str, Any] = {} # URI -> last parsed tree for incremental parsing
|
|
49
50
|
self._proxy = JdtlsProxy(on_diagnostics=self._on_jdtls_diagnostics)
|
|
50
51
|
|
|
51
52
|
def _on_jdtls_diagnostics(self, uri: str, diagnostics: list[Any]) -> None:
|
|
@@ -93,10 +94,13 @@ def _to_lsp_diagnostic(diag: LintDiagnostic) -> lsp.Diagnostic:
|
|
|
93
94
|
)
|
|
94
95
|
|
|
95
96
|
|
|
96
|
-
def _analyze_document(source_text: str) -> list[lsp.Diagnostic]:
|
|
97
|
-
"""Run all custom analyzers on the given source text."""
|
|
97
|
+
def _analyze_document(source_text: str, uri: str = "") -> list[lsp.Diagnostic]:
|
|
98
|
+
"""Run all custom analyzers on the given source text. Uses incremental parsing when possible."""
|
|
98
99
|
source_bytes = source_text.encode("utf-8")
|
|
99
|
-
|
|
100
|
+
old_tree = server._trees.get(uri) if uri else None
|
|
101
|
+
tree = server._parser.parse(source_bytes, old_tree) if old_tree else server._parser.parse(source_bytes)
|
|
102
|
+
if uri:
|
|
103
|
+
server._trees[uri] = tree
|
|
100
104
|
config = server._config
|
|
101
105
|
|
|
102
106
|
all_diagnostics: list[LintDiagnostic] = []
|
|
@@ -143,7 +147,7 @@ def _jdtls_raw_to_lsp_diagnostics(raw_diagnostics: list[Any]) -> list[lsp.Diagno
|
|
|
143
147
|
def _publish_diagnostics(uri: str) -> None:
|
|
144
148
|
"""Merge custom + jdtls diagnostics and publish to client."""
|
|
145
149
|
doc = server.workspace.get_text_document(uri)
|
|
146
|
-
custom_diags = _analyze_document(doc.source)
|
|
150
|
+
custom_diags = _analyze_document(doc.source, uri)
|
|
147
151
|
|
|
148
152
|
# Get cached jdtls diagnostics
|
|
149
153
|
jdtls_diags: list[lsp.Diagnostic] = []
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Tests for base.py tree traversal helper functions."""
|
|
2
|
+
|
|
3
|
+
from java_functional_lsp.analyzers.base import (
|
|
4
|
+
collect_nodes_by_type,
|
|
5
|
+
find_ancestor,
|
|
6
|
+
find_nodes,
|
|
7
|
+
find_nodes_multi,
|
|
8
|
+
get_parser,
|
|
9
|
+
has_ancestor,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _parse(source: str):
|
|
14
|
+
parser = get_parser()
|
|
15
|
+
return parser.parse(source.encode())
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TestFindNodes:
|
|
19
|
+
def test_finds_null_literal(self):
|
|
20
|
+
tree = _parse("class T { void f() { return null; } }")
|
|
21
|
+
nodes = list(find_nodes(tree.root_node, "null_literal"))
|
|
22
|
+
assert len(nodes) == 1
|
|
23
|
+
assert nodes[0].text == b"null"
|
|
24
|
+
|
|
25
|
+
def test_finds_multiple_matches(self):
|
|
26
|
+
tree = _parse("class T { void f() { return null; } void g() { return null; } }")
|
|
27
|
+
nodes = list(find_nodes(tree.root_node, "null_literal"))
|
|
28
|
+
assert len(nodes) == 2
|
|
29
|
+
|
|
30
|
+
def test_finds_nested_nodes(self):
|
|
31
|
+
tree = _parse("""
|
|
32
|
+
class Outer {
|
|
33
|
+
class Inner {
|
|
34
|
+
void f() { return null; }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
""")
|
|
38
|
+
nodes = list(find_nodes(tree.root_node, "null_literal"))
|
|
39
|
+
assert len(nodes) == 1
|
|
40
|
+
|
|
41
|
+
def test_no_match_returns_empty(self):
|
|
42
|
+
tree = _parse("class T { void f() { return 42; } }")
|
|
43
|
+
nodes = list(find_nodes(tree.root_node, "null_literal"))
|
|
44
|
+
assert len(nodes) == 0
|
|
45
|
+
|
|
46
|
+
def test_empty_class(self):
|
|
47
|
+
tree = _parse("class T { }")
|
|
48
|
+
nodes = list(find_nodes(tree.root_node, "method_declaration"))
|
|
49
|
+
assert len(nodes) == 0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class TestFindNodesMulti:
|
|
53
|
+
def test_finds_multiple_types(self):
|
|
54
|
+
tree = _parse("""
|
|
55
|
+
class T {
|
|
56
|
+
void f() {
|
|
57
|
+
for (int i = 0; i < 10; i++) {}
|
|
58
|
+
while (true) {}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
""")
|
|
62
|
+
nodes = list(find_nodes_multi(tree.root_node, {"for_statement", "while_statement"}))
|
|
63
|
+
assert len(nodes) == 2
|
|
64
|
+
|
|
65
|
+
def test_empty_set_returns_nothing(self):
|
|
66
|
+
tree = _parse("class T { void f() { return null; } }")
|
|
67
|
+
nodes = list(find_nodes_multi(tree.root_node, set()))
|
|
68
|
+
assert len(nodes) == 0
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class TestHasAncestor:
|
|
72
|
+
def test_has_method_ancestor(self):
|
|
73
|
+
tree = _parse("class T { void f() { return null; } }")
|
|
74
|
+
null_nodes = list(find_nodes(tree.root_node, "null_literal"))
|
|
75
|
+
assert len(null_nodes) == 1
|
|
76
|
+
assert has_ancestor(null_nodes[0], {"method_declaration"})
|
|
77
|
+
|
|
78
|
+
def test_no_matching_ancestor(self):
|
|
79
|
+
tree = _parse("class T { void f() { return null; } }")
|
|
80
|
+
null_nodes = list(find_nodes(tree.root_node, "null_literal"))
|
|
81
|
+
assert not has_ancestor(null_nodes[0], {"constructor_declaration"})
|
|
82
|
+
|
|
83
|
+
def test_multiple_ancestor_types(self):
|
|
84
|
+
tree = _parse("class T { void f() { return null; } }")
|
|
85
|
+
null_nodes = list(find_nodes(tree.root_node, "null_literal"))
|
|
86
|
+
assert has_ancestor(null_nodes[0], {"method_declaration", "constructor_declaration"})
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class TestFindAncestor:
|
|
90
|
+
def test_finds_nearest_ancestor(self):
|
|
91
|
+
tree = _parse("class T { void f() { return null; } }")
|
|
92
|
+
null_nodes = list(find_nodes(tree.root_node, "null_literal"))
|
|
93
|
+
ancestor = find_ancestor(null_nodes[0], "method_declaration")
|
|
94
|
+
assert ancestor is not None
|
|
95
|
+
assert ancestor.type == "method_declaration"
|
|
96
|
+
|
|
97
|
+
def test_returns_none_when_not_found(self):
|
|
98
|
+
tree = _parse("class T { void f() { return null; } }")
|
|
99
|
+
null_nodes = list(find_nodes(tree.root_node, "null_literal"))
|
|
100
|
+
assert find_ancestor(null_nodes[0], "constructor_declaration") is None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class TestCollectNodesByType:
|
|
104
|
+
def test_collects_multiple_types_single_pass(self):
|
|
105
|
+
tree = _parse("""
|
|
106
|
+
class T {
|
|
107
|
+
void f() {
|
|
108
|
+
return null;
|
|
109
|
+
throw new Exception();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
""")
|
|
113
|
+
buckets = collect_nodes_by_type(tree.root_node, {"null_literal", "throw_statement", "method_declaration"})
|
|
114
|
+
assert len(buckets["null_literal"]) == 1
|
|
115
|
+
assert len(buckets["throw_statement"]) == 1
|
|
116
|
+
assert len(buckets["method_declaration"]) == 1
|
|
117
|
+
|
|
118
|
+
def test_empty_buckets_for_missing_types(self):
|
|
119
|
+
tree = _parse("class T { }")
|
|
120
|
+
buckets = collect_nodes_by_type(tree.root_node, {"null_literal", "throw_statement"})
|
|
121
|
+
assert len(buckets["null_literal"]) == 0
|
|
122
|
+
assert len(buckets["throw_statement"]) == 0
|
|
@@ -33,6 +33,22 @@ class TestCatchRethrow:
|
|
|
33
33
|
codes = [d.code for d in diags]
|
|
34
34
|
assert "catch-rethrow" in codes
|
|
35
35
|
|
|
36
|
+
def test_catch_with_comment_and_throw_still_flagged(self) -> None:
|
|
37
|
+
"""A catch with only a comment + throw is still a rethrow — comments are ignored."""
|
|
38
|
+
source = b"""
|
|
39
|
+
class T {
|
|
40
|
+
void f() {
|
|
41
|
+
try { foo(); }
|
|
42
|
+
catch (Exception e) {
|
|
43
|
+
// log the error
|
|
44
|
+
throw new RuntimeException(e);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
"""
|
|
49
|
+
diags = parse_and_analyze(ExceptionChecker(), source)
|
|
50
|
+
assert any(d.code == "catch-rethrow" for d in diags)
|
|
51
|
+
|
|
36
52
|
def test_ignores_catch_with_logic(self) -> None:
|
|
37
53
|
source = b"""
|
|
38
54
|
class T {
|
|
@@ -94,3 +94,41 @@ class TestImperativeOptionUnwrap:
|
|
|
94
94
|
"""
|
|
95
95
|
diags = parse_and_analyze(MutationChecker(), source)
|
|
96
96
|
assert not any(d.code == "imperative-option-unwrap" for d in diags)
|
|
97
|
+
|
|
98
|
+
def test_ignores_unrelated_get(self) -> None:
|
|
99
|
+
"""Different object's .get() should not trigger the rule."""
|
|
100
|
+
source = b"""
|
|
101
|
+
class T {
|
|
102
|
+
void f() {
|
|
103
|
+
if (opt.isDefined()) { other.get(); }
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
"""
|
|
107
|
+
diags = parse_and_analyze(MutationChecker(), source)
|
|
108
|
+
assert not any(d.code == "imperative-option-unwrap" for d in diags)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class TestConstructorAssignment:
|
|
112
|
+
def test_ignores_this_field_in_constructor(self) -> None:
|
|
113
|
+
source = b"class T { final int x; T(int x) { this.x = x; } }"
|
|
114
|
+
diags = parse_and_analyze(MutationChecker(), source)
|
|
115
|
+
assert not any(d.code == "mutable-variable" for d in diags)
|
|
116
|
+
|
|
117
|
+
def test_ignores_computed_field_in_constructor(self) -> None:
|
|
118
|
+
"""this.x = computeValue() in constructor should not be flagged."""
|
|
119
|
+
source = b"class T { final int x; T() { this.x = compute(); } }"
|
|
120
|
+
diags = parse_and_analyze(MutationChecker(), source)
|
|
121
|
+
assert not any(d.code == "mutable-variable" for d in diags)
|
|
122
|
+
|
|
123
|
+
def test_detects_other_object_field_in_constructor(self) -> None:
|
|
124
|
+
"""other.field = x in a constructor IS a mutation and should be flagged."""
|
|
125
|
+
source = b"class T { T() { other.field = 42; } }"
|
|
126
|
+
diags = parse_and_analyze(MutationChecker(), source)
|
|
127
|
+
assert any(d.code == "mutable-variable" for d in diags)
|
|
128
|
+
|
|
129
|
+
def test_detects_reassignment_in_method(self) -> None:
|
|
130
|
+
"""this.x = ... in a regular method IS a mutation."""
|
|
131
|
+
source = b"class T { int x; void f() { this.x = 42; } }"
|
|
132
|
+
diags = parse_and_analyze(MutationChecker(), source)
|
|
133
|
+
codes = [d.code for d in diags]
|
|
134
|
+
assert "mutable-variable" in codes
|
|
File without changes
|
{java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/.github/ISSUE_TEMPLATE/bug-report.md
RENAMED
|
File without changes
|
{java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/.github/ISSUE_TEMPLATE/feature-request.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/.github/workflows/release-drafter.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/.github/workflows/update-homebrew.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{java_functional_lsp-0.3.1 → java_functional_lsp-0.3.2}/editors/intellij/lsp4ij-template.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|