zshshellcheck 0.2.1__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.
- zshshellcheck-0.2.1/PKG-INFO +119 -0
- zshshellcheck-0.2.1/README.md +88 -0
- zshshellcheck-0.2.1/pyproject.toml +141 -0
- zshshellcheck-0.2.1/setup.cfg +4 -0
- zshshellcheck-0.2.1/src/zshcheck/__init__.py +3 -0
- zshshellcheck-0.2.1/src/zshcheck/analyzer.py +269 -0
- zshshellcheck-0.2.1/src/zshcheck/checks/__init__.py +1 -0
- zshshellcheck-0.2.1/src/zshcheck/checks/base.py +225 -0
- zshshellcheck-0.2.1/src/zshcheck/checks/commands.py +179 -0
- zshshellcheck-0.2.1/src/zshcheck/checks/quoting.py +165 -0
- zshshellcheck-0.2.1/src/zshcheck/checks/style.py +163 -0
- zshshellcheck-0.2.1/src/zshcheck/checks/variables.py +168 -0
- zshshellcheck-0.2.1/src/zshcheck/cli.py +321 -0
- zshshellcheck-0.2.1/src/zshcheck/diagnostics.py +177 -0
- zshshellcheck-0.2.1/src/zshcheck/parser.py +239 -0
- zshshellcheck-0.2.1/src/zshshellcheck.egg-info/PKG-INFO +119 -0
- zshshellcheck-0.2.1/src/zshshellcheck.egg-info/SOURCES.txt +25 -0
- zshshellcheck-0.2.1/src/zshshellcheck.egg-info/dependency_links.txt +1 -0
- zshshellcheck-0.2.1/src/zshshellcheck.egg-info/entry_points.txt +2 -0
- zshshellcheck-0.2.1/src/zshshellcheck.egg-info/requires.txt +12 -0
- zshshellcheck-0.2.1/src/zshshellcheck.egg-info/top_level.txt +1 -0
- zshshellcheck-0.2.1/tests/test_analyzer.py +141 -0
- zshshellcheck-0.2.1/tests/test_checks.py +198 -0
- zshshellcheck-0.2.1/tests/test_cli.py +142 -0
- zshshellcheck-0.2.1/tests/test_diagnostics.py +200 -0
- zshshellcheck-0.2.1/tests/test_integration.py +114 -0
- zshshellcheck-0.2.1/tests/test_parser.py +155 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zshshellcheck
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: A static analysis tool for zsh shell scripts
|
|
5
|
+
Author-email: Rodrigo <rodregoc@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://rodrigo.is-a.dev/zshellcheck/
|
|
8
|
+
Project-URL: Repository, https://github.com/rodrigocnascimento/zshshellcheck
|
|
9
|
+
Project-URL: Issues, https://github.com/rodrigocnascimento/zshshellcheck/issues
|
|
10
|
+
Keywords: zsh,shell,lint,static-analysis,script
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
17
|
+
Classifier: Topic :: Software Development :: Testing
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: tree-sitter>=0.20.0
|
|
21
|
+
Requires-Dist: tree-sitter-zsh>=0.60.0
|
|
22
|
+
Requires-Dist: click>=8.0.0
|
|
23
|
+
Requires-Dist: rich>=13.0.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-xdist>=3.0.0; extra == "dev"
|
|
28
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
29
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
30
|
+
Requires-Dist: pre-commit>=3.0.0; extra == "dev"
|
|
31
|
+
|
|
32
|
+
<h1 align="center">ZshCheck</h1>
|
|
33
|
+
|
|
34
|
+
<p align="center">
|
|
35
|
+
<img src="https://raw.githubusercontent.com/rodrigocnascimento/zshellcheck/main/documentation/assets/header-image.png" alt="ZshCheck - Static Analysis for Zsh" width="600"/>
|
|
36
|
+
</p>
|
|
37
|
+
|
|
38
|
+
Uma ferramenta de análise estática para scripts zsh, construída com Python 3.12+.
|
|
39
|
+
|
|
40
|
+
## Funcionalidades
|
|
41
|
+
|
|
42
|
+
- **Parser Tree-sitter**: Análise AST precisa usando gramática tree-sitter-zsh
|
|
43
|
+
- **Checks Plugin-based**: Sistema de verificação extensível com padrão registry
|
|
44
|
+
- **CLI Rico**: Saída formatada com tabela, JSON e formatos compactos
|
|
45
|
+
- **Múltiplos Formatos de Saída**: Table, JSON e compacto
|
|
46
|
+
- **Auto-Fix**: Corrige automaticamente problemas detectáveis (--fix flag)
|
|
47
|
+
- **Suporte a Unicode**: Aceita emojis e caracteres não-ASCII em comentários
|
|
48
|
+
|
|
49
|
+
## Instalação
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install zshshellcheck
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Início Rápido
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Analisar um script
|
|
59
|
+
zshcheck script.zsh
|
|
60
|
+
|
|
61
|
+
# Analisar múltiplos arquivos
|
|
62
|
+
zshcheck *.zsh
|
|
63
|
+
|
|
64
|
+
# Listar checks disponíveis
|
|
65
|
+
zshcheck --list-checks
|
|
66
|
+
|
|
67
|
+
# Filtrar por código de check
|
|
68
|
+
zshcheck --include ZC1001 --include ZC2001
|
|
69
|
+
|
|
70
|
+
# Saída em JSON
|
|
71
|
+
zshcheck --format json script.zsh
|
|
72
|
+
|
|
73
|
+
# Auto-fixar problemas (pergunta para cada fix)
|
|
74
|
+
zshcheck script.zsh --fix
|
|
75
|
+
|
|
76
|
+
# Ou usando a flag curta
|
|
77
|
+
zshcheck script.zsh -F
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Documentação
|
|
81
|
+
|
|
82
|
+
A documentação completa está disponível em: [https://developer.github.io/zshshellcheck/](https://developer.github.io/zshshellcheck/)
|
|
83
|
+
|
|
84
|
+
## Limitações Conhecidas
|
|
85
|
+
|
|
86
|
+
### Emojis e Unicode em scripts zsh
|
|
87
|
+
|
|
88
|
+
O parser `tree-sitter-zsh` pode apresentar problemas com caracteres unicode em identificadores (nomes de variáveis, funções). Esses caracteres agora são detectados como INFO (ZC9004) em vez de erro, para que você possa decidir se quer ajustar.
|
|
89
|
+
|
|
90
|
+
**Exemplo de warning**:
|
|
91
|
+
```
|
|
92
|
+
INFO ZC9004 Non-ASCII characters detected (2 found). Found: 'ã' 'é'
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Isso aparece como INFO (não blockea a análise) e indica quais caracteres foram encontrados.
|
|
96
|
+
|
|
97
|
+
### Sintaxe zsh avançada
|
|
98
|
+
|
|
99
|
+
Algumas sintaxes zsh avançadas (como parâmetros de glob qualifiers `${(k)functions}`) podem não ser suportadas pelo parser. Recomendamos usar a flag acima para scripts que contenham dessas expressões.
|
|
100
|
+
|
|
101
|
+
## Desenvolvimento
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
# Executar testes
|
|
105
|
+
pytest -v
|
|
106
|
+
|
|
107
|
+
# Executar linting
|
|
108
|
+
ruff check .
|
|
109
|
+
|
|
110
|
+
# Executar type checking
|
|
111
|
+
mypy .
|
|
112
|
+
|
|
113
|
+
# Verificação completa
|
|
114
|
+
ruff check . && mypy . && pytest
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Licença
|
|
118
|
+
|
|
119
|
+
MIT
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<h1 align="center">ZshCheck</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="https://raw.githubusercontent.com/rodrigocnascimento/zshellcheck/main/documentation/assets/header-image.png" alt="ZshCheck - Static Analysis for Zsh" width="600"/>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
Uma ferramenta de análise estática para scripts zsh, construída com Python 3.12+.
|
|
8
|
+
|
|
9
|
+
## Funcionalidades
|
|
10
|
+
|
|
11
|
+
- **Parser Tree-sitter**: Análise AST precisa usando gramática tree-sitter-zsh
|
|
12
|
+
- **Checks Plugin-based**: Sistema de verificação extensível com padrão registry
|
|
13
|
+
- **CLI Rico**: Saída formatada com tabela, JSON e formatos compactos
|
|
14
|
+
- **Múltiplos Formatos de Saída**: Table, JSON e compacto
|
|
15
|
+
- **Auto-Fix**: Corrige automaticamente problemas detectáveis (--fix flag)
|
|
16
|
+
- **Suporte a Unicode**: Aceita emojis e caracteres não-ASCII em comentários
|
|
17
|
+
|
|
18
|
+
## Instalação
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install zshshellcheck
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Início Rápido
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Analisar um script
|
|
28
|
+
zshcheck script.zsh
|
|
29
|
+
|
|
30
|
+
# Analisar múltiplos arquivos
|
|
31
|
+
zshcheck *.zsh
|
|
32
|
+
|
|
33
|
+
# Listar checks disponíveis
|
|
34
|
+
zshcheck --list-checks
|
|
35
|
+
|
|
36
|
+
# Filtrar por código de check
|
|
37
|
+
zshcheck --include ZC1001 --include ZC2001
|
|
38
|
+
|
|
39
|
+
# Saída em JSON
|
|
40
|
+
zshcheck --format json script.zsh
|
|
41
|
+
|
|
42
|
+
# Auto-fixar problemas (pergunta para cada fix)
|
|
43
|
+
zshcheck script.zsh --fix
|
|
44
|
+
|
|
45
|
+
# Ou usando a flag curta
|
|
46
|
+
zshcheck script.zsh -F
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Documentação
|
|
50
|
+
|
|
51
|
+
A documentação completa está disponível em: [https://developer.github.io/zshshellcheck/](https://developer.github.io/zshshellcheck/)
|
|
52
|
+
|
|
53
|
+
## Limitações Conhecidas
|
|
54
|
+
|
|
55
|
+
### Emojis e Unicode em scripts zsh
|
|
56
|
+
|
|
57
|
+
O parser `tree-sitter-zsh` pode apresentar problemas com caracteres unicode em identificadores (nomes de variáveis, funções). Esses caracteres agora são detectados como INFO (ZC9004) em vez de erro, para que você possa decidir se quer ajustar.
|
|
58
|
+
|
|
59
|
+
**Exemplo de warning**:
|
|
60
|
+
```
|
|
61
|
+
INFO ZC9004 Non-ASCII characters detected (2 found). Found: 'ã' 'é'
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Isso aparece como INFO (não blockea a análise) e indica quais caracteres foram encontrados.
|
|
65
|
+
|
|
66
|
+
### Sintaxe zsh avançada
|
|
67
|
+
|
|
68
|
+
Algumas sintaxes zsh avançadas (como parâmetros de glob qualifiers `${(k)functions}`) podem não ser suportadas pelo parser. Recomendamos usar a flag acima para scripts que contenham dessas expressões.
|
|
69
|
+
|
|
70
|
+
## Desenvolvimento
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# Executar testes
|
|
74
|
+
pytest -v
|
|
75
|
+
|
|
76
|
+
# Executar linting
|
|
77
|
+
ruff check .
|
|
78
|
+
|
|
79
|
+
# Executar type checking
|
|
80
|
+
mypy .
|
|
81
|
+
|
|
82
|
+
# Verificação completa
|
|
83
|
+
ruff check . && mypy . && pytest
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Licença
|
|
87
|
+
|
|
88
|
+
MIT
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "zshshellcheck"
|
|
7
|
+
version = "0.2.1"
|
|
8
|
+
description = "A static analysis tool for zsh shell scripts"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
authors = [
|
|
12
|
+
{name = "Rodrigo", email = "rodregoc@gmail.com"}
|
|
13
|
+
]
|
|
14
|
+
requires-python = ">=3.12"
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
22
|
+
"Topic :: Software Development :: Testing",
|
|
23
|
+
]
|
|
24
|
+
keywords = ["zsh", "shell", "lint", "static-analysis", "script"]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"tree-sitter>=0.20.0",
|
|
27
|
+
"tree-sitter-zsh>=0.60.0",
|
|
28
|
+
"click>=8.0.0",
|
|
29
|
+
"rich>=13.0.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
dev = [
|
|
34
|
+
"pytest>=7.0.0",
|
|
35
|
+
"pytest-cov>=4.0.0",
|
|
36
|
+
"pytest-xdist>=3.0.0",
|
|
37
|
+
"mypy>=1.0.0",
|
|
38
|
+
"ruff>=0.1.0",
|
|
39
|
+
"pre-commit>=3.0.0",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[project.scripts]
|
|
43
|
+
zshcheck = "zshcheck.cli:main"
|
|
44
|
+
|
|
45
|
+
[project.urls]
|
|
46
|
+
Homepage = "https://rodrigo.is-a.dev/zshellcheck/"
|
|
47
|
+
Repository = "https://github.com/rodrigocnascimento/zshshellcheck"
|
|
48
|
+
Issues = "https://github.com/rodrigocnascimento/zshshellcheck/issues"
|
|
49
|
+
|
|
50
|
+
[tool.setuptools.packages.find]
|
|
51
|
+
where = ["src"]
|
|
52
|
+
|
|
53
|
+
# Ruff configuration
|
|
54
|
+
[tool.ruff]
|
|
55
|
+
target-version = "py312"
|
|
56
|
+
line-length = 100
|
|
57
|
+
select = [
|
|
58
|
+
"E", # pycodestyle errors
|
|
59
|
+
"W", # pycodestyle warnings
|
|
60
|
+
"F", # Pyflakes
|
|
61
|
+
"I", # isort
|
|
62
|
+
"N", # pep8-naming
|
|
63
|
+
"D", # pydocstyle
|
|
64
|
+
"UP", # pyupgrade
|
|
65
|
+
"B", # flake8-bugbear
|
|
66
|
+
"C4", # flake8-comprehensions
|
|
67
|
+
"SIM", # flake8-simplify
|
|
68
|
+
]
|
|
69
|
+
ignore = [
|
|
70
|
+
"D100", # Missing docstring in public module
|
|
71
|
+
"D104", # Missing docstring in public package
|
|
72
|
+
"D107", # Missing docstring in __init__
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
[tool.ruff.pydocstyle]
|
|
76
|
+
convention = "google"
|
|
77
|
+
|
|
78
|
+
[tool.ruff.per-file-ignores]
|
|
79
|
+
"tests/*" = ["D", "S"]
|
|
80
|
+
|
|
81
|
+
# Mypy configuration
|
|
82
|
+
[tool.mypy]
|
|
83
|
+
python_version = "3.12"
|
|
84
|
+
strict = true
|
|
85
|
+
warn_return_any = true
|
|
86
|
+
warn_unused_configs = true
|
|
87
|
+
warn_unused_ignores = true
|
|
88
|
+
warn_redundant_casts = true
|
|
89
|
+
warn_unreachable = true
|
|
90
|
+
disallow_untyped_defs = true
|
|
91
|
+
disallow_incomplete_defs = true
|
|
92
|
+
check_untyped_defs = true
|
|
93
|
+
disallow_untyped_decorators = true
|
|
94
|
+
no_implicit_optional = true
|
|
95
|
+
strict_optional = true
|
|
96
|
+
warn_no_return = true
|
|
97
|
+
show_error_codes = true
|
|
98
|
+
show_column_numbers = true
|
|
99
|
+
|
|
100
|
+
# Pytest configuration
|
|
101
|
+
[tool.pytest.ini_options]
|
|
102
|
+
testpaths = ["tests"]
|
|
103
|
+
python_files = ["test_*.py", "*_test.py"]
|
|
104
|
+
python_classes = ["Test*"]
|
|
105
|
+
python_functions = ["test_*"]
|
|
106
|
+
addopts = [
|
|
107
|
+
"-v",
|
|
108
|
+
"--strict-markers",
|
|
109
|
+
"--tb=short",
|
|
110
|
+
"--cov=zshcheck",
|
|
111
|
+
"--cov-report=term-missing",
|
|
112
|
+
"--cov-report=html:htmlcov",
|
|
113
|
+
"--cov-report=xml:coverage.xml",
|
|
114
|
+
"--cov-fail-under=70",
|
|
115
|
+
]
|
|
116
|
+
markers = [
|
|
117
|
+
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
|
|
118
|
+
"integration: marks tests as integration tests",
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
# Coverage configuration
|
|
122
|
+
[tool.coverage.run]
|
|
123
|
+
source = ["src/zshcheck"]
|
|
124
|
+
branch = true
|
|
125
|
+
|
|
126
|
+
[tool.coverage.report]
|
|
127
|
+
exclude_lines = [
|
|
128
|
+
"pragma: no cover",
|
|
129
|
+
"def __repr__",
|
|
130
|
+
"if self.debug:",
|
|
131
|
+
"if settings.DEBUG",
|
|
132
|
+
"raise AssertionError",
|
|
133
|
+
"raise NotImplementedError",
|
|
134
|
+
"if 0:",
|
|
135
|
+
"if __name__ == .__main__.:",
|
|
136
|
+
"class .*\\bProtocol\\):",
|
|
137
|
+
"@(abc\\.)?abstractmethod",
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
[tool.coverage.html]
|
|
141
|
+
directory = "htmlcov"
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""Analyzer module for zshcheck.
|
|
2
|
+
|
|
3
|
+
This module provides the main analysis engine that orchestrates parsing,
|
|
4
|
+
running checks, and collecting diagnostics.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from tree_sitter import Node
|
|
13
|
+
|
|
14
|
+
from zshcheck.checks.base import AnalysisContext, CheckRegistry, get_registry
|
|
15
|
+
from zshcheck.diagnostics import Diagnostic, Position, Range, Severity
|
|
16
|
+
from zshcheck.parser import ZshParser
|
|
17
|
+
|
|
18
|
+
NON_ASCII_PATTERN = re.compile(r"[^\x00-\x7F]")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _check_non_ascii(source: str) -> list[Diagnostic]:
|
|
22
|
+
"""Check for non-ASCII characters (including emojis) that may cause parsing issues.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
source: The source code to check.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
List of diagnostics for non-ASCII characters found.
|
|
29
|
+
"""
|
|
30
|
+
diagnostics: list[Diagnostic] = []
|
|
31
|
+
matches = list(NON_ASCII_PATTERN.finditer(source))
|
|
32
|
+
|
|
33
|
+
if not matches:
|
|
34
|
+
return diagnostics
|
|
35
|
+
|
|
36
|
+
chars = [m.group() for m in matches[:5]]
|
|
37
|
+
chars_str = " ".join(f"'{c}'" for c in chars)
|
|
38
|
+
|
|
39
|
+
diagnostic = Diagnostic(
|
|
40
|
+
code="ZC9004",
|
|
41
|
+
severity=Severity.INFO,
|
|
42
|
+
message=(
|
|
43
|
+
f"Non-ASCII characters detected ({len(matches)} found). "
|
|
44
|
+
"tree-sitter-zsh grammar may not parse these correctly. "
|
|
45
|
+
f"Found: {chars_str}"
|
|
46
|
+
),
|
|
47
|
+
range=Range(Position(1, 1), Position(1, 1)),
|
|
48
|
+
)
|
|
49
|
+
diagnostics.append(diagnostic)
|
|
50
|
+
return diagnostics
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def apply_fixes(source: str, fixes: list[Diagnostic]) -> str:
|
|
54
|
+
"""Apply fixes to source code.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
source: Original source code.
|
|
58
|
+
fixes: List of diagnostics with fixes to apply.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Source code with fixes applied.
|
|
62
|
+
"""
|
|
63
|
+
if not fixes:
|
|
64
|
+
return source
|
|
65
|
+
|
|
66
|
+
result = source
|
|
67
|
+
ordered_fixes = sorted(
|
|
68
|
+
[f for f in fixes if f.fixable],
|
|
69
|
+
key=lambda d: (d.range.start.line, d.range.start.column),
|
|
70
|
+
reverse=True,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
for diagnostic in ordered_fixes:
|
|
74
|
+
if diagnostic.fix is None:
|
|
75
|
+
continue
|
|
76
|
+
for replacement in diagnostic.fix.replacements:
|
|
77
|
+
start_line = replacement.range.start.line - 1
|
|
78
|
+
start_col = replacement.range.start.column - 1
|
|
79
|
+
end_line = replacement.range.end.line - 1
|
|
80
|
+
end_col = replacement.range.end.column
|
|
81
|
+
|
|
82
|
+
lines = result.splitlines(keepends=True)
|
|
83
|
+
if start_line < 0 or start_line >= len(lines):
|
|
84
|
+
continue
|
|
85
|
+
if end_line < 0 or end_line >= len(lines):
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
if start_line == end_line:
|
|
89
|
+
line = lines[start_line]
|
|
90
|
+
lines[start_line] = line[:start_col] + replacement.text + line[end_col:]
|
|
91
|
+
else:
|
|
92
|
+
first_line = lines[start_line]
|
|
93
|
+
lines[start_line] = first_line[:start_col] + replacement.text + "\n"
|
|
94
|
+
del lines[start_line + 1 : end_line + 1]
|
|
95
|
+
|
|
96
|
+
result = "".join(lines)
|
|
97
|
+
|
|
98
|
+
return result
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class Analyzer:
|
|
102
|
+
"""Main analysis engine for zsh shell scripts."""
|
|
103
|
+
|
|
104
|
+
def __init__(self, registry: CheckRegistry | None = None) -> None:
|
|
105
|
+
"""Initialize the analyzer.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
registry: Check registry to use (creates default if None).
|
|
109
|
+
"""
|
|
110
|
+
self._parser = ZshParser()
|
|
111
|
+
self._registry = registry or get_registry()
|
|
112
|
+
|
|
113
|
+
def analyze_string(
|
|
114
|
+
self,
|
|
115
|
+
source: str,
|
|
116
|
+
filename: str | None = None,
|
|
117
|
+
include: list[str] | None = None,
|
|
118
|
+
exclude: list[str] | None = None,
|
|
119
|
+
) -> list[Diagnostic]:
|
|
120
|
+
"""Analyze a zsh script from a string.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
source: The zsh script source code.
|
|
124
|
+
filename: Optional filename for context.
|
|
125
|
+
include: Optional list of check codes to run.
|
|
126
|
+
exclude: Optional list of check codes to skip.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
List of diagnostics found.
|
|
130
|
+
"""
|
|
131
|
+
parse_result = self._parser.parse(source)
|
|
132
|
+
|
|
133
|
+
# Collect parse errors first
|
|
134
|
+
all_diagnostics: list[Diagnostic] = list(parse_result.diagnostics)
|
|
135
|
+
|
|
136
|
+
# Check for non-ASCII characters
|
|
137
|
+
all_diagnostics.extend(_check_non_ascii(source))
|
|
138
|
+
|
|
139
|
+
if not parse_result.success or parse_result.root_node is None:
|
|
140
|
+
return all_diagnostics
|
|
141
|
+
|
|
142
|
+
# Create analysis context
|
|
143
|
+
context = AnalysisContext(
|
|
144
|
+
source=source,
|
|
145
|
+
filename=filename,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Run all checks on the AST
|
|
149
|
+
check_diagnostics = self._run_checks(
|
|
150
|
+
parse_result.root_node,
|
|
151
|
+
context,
|
|
152
|
+
include=include,
|
|
153
|
+
exclude=exclude,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
all_diagnostics.extend(check_diagnostics)
|
|
157
|
+
|
|
158
|
+
# Sort by line number, then column
|
|
159
|
+
all_diagnostics.sort()
|
|
160
|
+
|
|
161
|
+
return all_diagnostics
|
|
162
|
+
|
|
163
|
+
def analyze_file(
|
|
164
|
+
self,
|
|
165
|
+
path: str | Path,
|
|
166
|
+
include: list[str] | None = None,
|
|
167
|
+
exclude: list[str] | None = None,
|
|
168
|
+
) -> list[Diagnostic]:
|
|
169
|
+
"""Analyze a zsh script from a file.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
path: Path to the zsh script file.
|
|
173
|
+
include: Optional list of check codes to run.
|
|
174
|
+
exclude: Optional list of check codes to skip.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
List of diagnostics found.
|
|
178
|
+
"""
|
|
179
|
+
file_path = Path(path)
|
|
180
|
+
parse_result = self._parser.parse_file(file_path)
|
|
181
|
+
|
|
182
|
+
# Collect parse errors first
|
|
183
|
+
all_diagnostics: list[Diagnostic] = list(parse_result.diagnostics)
|
|
184
|
+
|
|
185
|
+
# Check for non-ASCII characters
|
|
186
|
+
all_diagnostics.extend(_check_non_ascii(parse_result.source))
|
|
187
|
+
|
|
188
|
+
if not parse_result.success or parse_result.root_node is None:
|
|
189
|
+
return all_diagnostics
|
|
190
|
+
|
|
191
|
+
# Create analysis context
|
|
192
|
+
context = AnalysisContext(
|
|
193
|
+
source=parse_result.source,
|
|
194
|
+
filename=str(file_path),
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Run all checks on the AST
|
|
198
|
+
check_diagnostics = self._run_checks(
|
|
199
|
+
parse_result.root_node,
|
|
200
|
+
context,
|
|
201
|
+
include=include,
|
|
202
|
+
exclude=exclude,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
all_diagnostics.extend(check_diagnostics)
|
|
206
|
+
|
|
207
|
+
# Sort by line number, then column
|
|
208
|
+
all_diagnostics.sort()
|
|
209
|
+
|
|
210
|
+
return all_diagnostics
|
|
211
|
+
|
|
212
|
+
def _run_checks(
|
|
213
|
+
self,
|
|
214
|
+
root_node: Node,
|
|
215
|
+
context: AnalysisContext,
|
|
216
|
+
include: list[str] | None = None,
|
|
217
|
+
exclude: list[str] | None = None,
|
|
218
|
+
) -> list[Diagnostic]:
|
|
219
|
+
"""Run all checks against the AST.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
root_node: Root node of the AST.
|
|
223
|
+
context: Analysis context.
|
|
224
|
+
include: Optional list of check codes to run.
|
|
225
|
+
exclude: Optional list of check codes to skip.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
List of diagnostics found.
|
|
229
|
+
"""
|
|
230
|
+
diagnostics: list[Diagnostic] = []
|
|
231
|
+
exclude_set = set(exclude or [])
|
|
232
|
+
|
|
233
|
+
def visit_node(node: Node, depth: int) -> None:
|
|
234
|
+
# Run checks that are not excluded
|
|
235
|
+
for check in self._registry.checks:
|
|
236
|
+
if check.code in exclude_set:
|
|
237
|
+
continue
|
|
238
|
+
if include is not None and check.code not in include:
|
|
239
|
+
continue
|
|
240
|
+
|
|
241
|
+
if diagnostic := check.check(node, context):
|
|
242
|
+
diagnostics.append(diagnostic)
|
|
243
|
+
|
|
244
|
+
# Visit children
|
|
245
|
+
for child in node.children:
|
|
246
|
+
visit_node(child, depth + 1)
|
|
247
|
+
|
|
248
|
+
visit_node(root_node, 0)
|
|
249
|
+
return diagnostics
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def create_default_analyzer() -> Analyzer:
|
|
253
|
+
"""Create an analyzer with all default checks registered."""
|
|
254
|
+
from zshcheck.checks.commands import DeprecatedCommandCheck
|
|
255
|
+
from zshcheck.checks.quoting import UnquotedVariableCheck
|
|
256
|
+
from zshcheck.checks.style import DoubleBracketCheck
|
|
257
|
+
from zshcheck.checks.variables import UnusedVariableCheck
|
|
258
|
+
|
|
259
|
+
registry = CheckRegistry()
|
|
260
|
+
registry.register_all(
|
|
261
|
+
[
|
|
262
|
+
UnquotedVariableCheck(),
|
|
263
|
+
UnusedVariableCheck(),
|
|
264
|
+
DeprecatedCommandCheck(),
|
|
265
|
+
DoubleBracketCheck(),
|
|
266
|
+
]
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
return Analyzer(registry)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Checks package for zshcheck."""
|