prettycsharp 1.0.0__py3-none-any.whl
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.
- prettycsharp/__init__.py +9 -0
- prettycsharp/__main__.py +5 -0
- prettycsharp/cli.py +250 -0
- prettycsharp/config.py +263 -0
- prettycsharp/doc.py +537 -0
- prettycsharp/formatter.py +106 -0
- prettycsharp/ignore.py +184 -0
- prettycsharp/parsing.py +53 -0
- prettycsharp/printer/__init__.py +28 -0
- prettycsharp/printer/core.py +540 -0
- prettycsharp/printer/declarations.py +623 -0
- prettycsharp/printer/expressions.py +1107 -0
- prettycsharp/printer/statements.py +603 -0
- prettycsharp/project.py +214 -0
- prettycsharp/py.typed +0 -0
- prettycsharp/rewrites.py +732 -0
- prettycsharp/settings.py +192 -0
- prettycsharp/syntax.py +367 -0
- prettycsharp/verify.py +312 -0
- prettycsharp-1.0.0.dist-info/METADATA +212 -0
- prettycsharp-1.0.0.dist-info/RECORD +24 -0
- prettycsharp-1.0.0.dist-info/WHEEL +4 -0
- prettycsharp-1.0.0.dist-info/entry_points.txt +2 -0
- prettycsharp-1.0.0.dist-info/licenses/LICENSE +21 -0
prettycsharp/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""An opinionated formatter for C#, in the spirit of Prettier."""
|
|
2
|
+
|
|
3
|
+
from prettycsharp.formatter import FormattingError, format_source
|
|
4
|
+
from prettycsharp.parsing import CSharpSyntaxError
|
|
5
|
+
from prettycsharp.settings import Settings, SettingsError
|
|
6
|
+
|
|
7
|
+
__version__ = "1.0.0"
|
|
8
|
+
|
|
9
|
+
__all__ = ["CSharpSyntaxError", "FormattingError", "Settings", "SettingsError", "__version__", "format_source"]
|
prettycsharp/__main__.py
ADDED
prettycsharp/cli.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""The prettycsharp command line.
|
|
2
|
+
|
|
3
|
+
prettycsharp format PATH... rewrite files in place (alias --write)
|
|
4
|
+
prettycsharp check PATH... exit 1 if any would change (alias --check)
|
|
5
|
+
prettycsharp diff PATH... print a unified diff (alias --diff)
|
|
6
|
+
prettycsharp PATH print the formatted file to stdout
|
|
7
|
+
prettycsharp - format stdin to stdout
|
|
8
|
+
|
|
9
|
+
A PATH is a .cs file, a folder, a .csproj, or a .sln or .slnx. Exit status:
|
|
10
|
+
0 when nothing needs to change, 1 when check or diff found a file that
|
|
11
|
+
would change, 2 when a file could not be read, parsed or formatted, or the
|
|
12
|
+
settings are wrong.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import difflib
|
|
19
|
+
import os
|
|
20
|
+
import shutil
|
|
21
|
+
import sys
|
|
22
|
+
import tempfile
|
|
23
|
+
from collections.abc import Iterator, Sequence
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from prettycsharp import __version__, config, ignore, project
|
|
27
|
+
from prettycsharp.config import settings_for
|
|
28
|
+
from prettycsharp.formatter import FormattingError, format_source
|
|
29
|
+
from prettycsharp.ignore import Ignorer
|
|
30
|
+
from prettycsharp.parsing import CSharpSyntaxError
|
|
31
|
+
from prettycsharp.project import compiled_files, lang_version_for, solution_projects
|
|
32
|
+
from prettycsharp.settings import Settings, SettingsError
|
|
33
|
+
|
|
34
|
+
_BOM = chr(0xFEFF)
|
|
35
|
+
_MODES = {"format": "write", "check": "check", "diff": "diff"}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _Options:
|
|
39
|
+
def __init__(self, namespace: argparse.Namespace) -> None:
|
|
40
|
+
self.overrides: dict[str, object] = {}
|
|
41
|
+
for item in namespace.option or []:
|
|
42
|
+
key, sep, value = item.partition("=")
|
|
43
|
+
if not sep or not key.strip():
|
|
44
|
+
raise SettingsError(f"--option {item!r}: use KEY=VALUE")
|
|
45
|
+
self.overrides[key.strip()] = value.strip()
|
|
46
|
+
self.config: Path | None = namespace.config
|
|
47
|
+
self.no_ignore: bool = namespace.no_ignore
|
|
48
|
+
self.ignorer = Ignorer(respect_gitignore=not self.no_ignore, use_ignore_files=not self.no_ignore)
|
|
49
|
+
self.stdin_filepath: Path | None = namespace.stdin_filepath
|
|
50
|
+
|
|
51
|
+
def ignorer_for(self, target: Path) -> Ignorer:
|
|
52
|
+
"""The skipping rules for a target, honouring its respect_gitignore setting."""
|
|
53
|
+
if self.no_ignore:
|
|
54
|
+
return self.ignorer
|
|
55
|
+
probe = target / "probe.cs" if target.is_dir() else target
|
|
56
|
+
settings = settings_for(probe, self.overrides, self.config)
|
|
57
|
+
return Ignorer(respect_gitignore=settings.respect_gitignore, use_ignore_files=True)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
61
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
62
|
+
# Settings, project and ignore files are cached for one run, not across runs.
|
|
63
|
+
config.clear_caches()
|
|
64
|
+
project.clear_caches()
|
|
65
|
+
ignore.clear_caches()
|
|
66
|
+
mode = "stdout"
|
|
67
|
+
if args and args[0] in _MODES:
|
|
68
|
+
mode = _MODES[args.pop(0)]
|
|
69
|
+
|
|
70
|
+
parser = argparse.ArgumentParser(prog="prettycsharp", description="Format C# source files.")
|
|
71
|
+
parser.add_argument("paths", nargs="+", help=".cs files, folders, .csproj or .sln files, or - for stdin")
|
|
72
|
+
parser.add_argument("--version", action="version", version=f"prettycsharp {__version__}")
|
|
73
|
+
flags = parser.add_mutually_exclusive_group()
|
|
74
|
+
flags.add_argument("--write", action="store_const", const="write", dest="mode", help="same as format")
|
|
75
|
+
flags.add_argument("--check", action="store_const", const="check", dest="mode", help="same as check")
|
|
76
|
+
flags.add_argument("--diff", action="store_const", const="diff", dest="mode", help="same as diff")
|
|
77
|
+
parser.add_argument("--config", type=Path, help="use this settings file instead of searching for one")
|
|
78
|
+
parser.add_argument(
|
|
79
|
+
"-o", "--option", action="append", metavar="KEY=VALUE", help="set one setting, over every settings file"
|
|
80
|
+
)
|
|
81
|
+
parser.add_argument("--no-ignore", action="store_true", help="do not skip files .gitignore or .prettycsharpignore lists")
|
|
82
|
+
parser.add_argument("--stdin-filepath", type=Path, help="the path stdin stands for, to find its settings")
|
|
83
|
+
namespace = parser.parse_args(args)
|
|
84
|
+
if namespace.mode is not None:
|
|
85
|
+
if mode != "stdout" and namespace.mode != mode:
|
|
86
|
+
parser.error("give a subcommand or a flag, not two different ones")
|
|
87
|
+
mode = namespace.mode
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
options = _Options(namespace)
|
|
91
|
+
if namespace.config is not None and not namespace.config.is_file():
|
|
92
|
+
raise SettingsError(f"--config {namespace.config}: no such file")
|
|
93
|
+
except SettingsError as error:
|
|
94
|
+
print(f"prettycsharp: {error}", file=sys.stderr)
|
|
95
|
+
return 2
|
|
96
|
+
|
|
97
|
+
if namespace.paths == ["-"]:
|
|
98
|
+
return _run_stdin(options)
|
|
99
|
+
if "-" in namespace.paths:
|
|
100
|
+
parser.error("- (stdin) cannot be combined with other paths")
|
|
101
|
+
|
|
102
|
+
files, status = _collect(namespace.paths, options)
|
|
103
|
+
if mode == "stdout" and len(files) != 1:
|
|
104
|
+
if status:
|
|
105
|
+
return status
|
|
106
|
+
parser.error("printing to stdout takes exactly one file; use format, check or diff")
|
|
107
|
+
for path in files:
|
|
108
|
+
status = max(status, _run(path, mode, options))
|
|
109
|
+
return status
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _collect(targets: Sequence[str], options: _Options) -> tuple[list[Path], int]:
|
|
113
|
+
files: list[Path] = []
|
|
114
|
+
seen: set[Path] = set()
|
|
115
|
+
status = 0
|
|
116
|
+
for target in targets:
|
|
117
|
+
path = Path(target)
|
|
118
|
+
if not path.exists():
|
|
119
|
+
print(f"{path}: no such file or folder", file=sys.stderr)
|
|
120
|
+
status = 2
|
|
121
|
+
continue
|
|
122
|
+
try:
|
|
123
|
+
ignorer = options.ignorer_for(path)
|
|
124
|
+
except SettingsError as error:
|
|
125
|
+
print(f"prettycsharp: {error}", file=sys.stderr)
|
|
126
|
+
status = 2
|
|
127
|
+
continue
|
|
128
|
+
for found in _expand(path, ignorer):
|
|
129
|
+
key = found.resolve()
|
|
130
|
+
if key not in seen:
|
|
131
|
+
seen.add(key)
|
|
132
|
+
files.append(found)
|
|
133
|
+
return files, status
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _expand(path: Path, ignorer: Ignorer) -> Iterator[Path]:
|
|
137
|
+
suffix = path.suffix.lower()
|
|
138
|
+
if path.is_dir():
|
|
139
|
+
for folder, subfolders, names in os.walk(path):
|
|
140
|
+
here = Path(folder)
|
|
141
|
+
subfolders[:] = sorted(s for s in subfolders if not ignorer.skip_folder(here / s))
|
|
142
|
+
for name in sorted(names):
|
|
143
|
+
candidate = here / name
|
|
144
|
+
if name.lower().endswith(".cs") and not ignorer.skip_file(candidate):
|
|
145
|
+
yield candidate
|
|
146
|
+
elif suffix == ".csproj":
|
|
147
|
+
for found in compiled_files(path):
|
|
148
|
+
if not ignorer.skip_file(found):
|
|
149
|
+
yield found
|
|
150
|
+
elif suffix in (".sln", ".slnx"):
|
|
151
|
+
for project in solution_projects(path):
|
|
152
|
+
if project.is_file():
|
|
153
|
+
yield from _expand(project, ignorer)
|
|
154
|
+
elif not ignorer.skip_file(path):
|
|
155
|
+
yield path
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _settings(path: Path, options: _Options) -> Settings:
|
|
159
|
+
return settings_for(path, options.overrides, options.config)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _format_text(text: str, path: Path, settings: Settings) -> str:
|
|
163
|
+
"""Format text read from ``path``, keeping or changing its byte order mark."""
|
|
164
|
+
bom = _BOM if text.startswith(_BOM) else ""
|
|
165
|
+
lang = lang_version_for(path) if settings.lang_version == "auto" else None
|
|
166
|
+
formatted = format_source(text[len(bom) :], settings, lang_version=lang)
|
|
167
|
+
if settings.charset == "utf-8-bom":
|
|
168
|
+
bom = _BOM
|
|
169
|
+
elif settings.charset == "utf-8":
|
|
170
|
+
bom = ""
|
|
171
|
+
return bom + formatted
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _run_stdin(options: _Options) -> int:
|
|
175
|
+
path = options.stdin_filepath or Path("stdin.cs")
|
|
176
|
+
text = sys.stdin.read()
|
|
177
|
+
if options.stdin_filepath is not None and options.ignorer.skip_file(path):
|
|
178
|
+
sys.stdout.write(text)
|
|
179
|
+
return 0
|
|
180
|
+
try:
|
|
181
|
+
formatted = _format_text(text, path, _settings(path, options))
|
|
182
|
+
except (CSharpSyntaxError, FormattingError, SettingsError) as error:
|
|
183
|
+
print(f"{path}: {error}; not formatted", file=sys.stderr)
|
|
184
|
+
return 2
|
|
185
|
+
sys.stdout.write(formatted.lstrip(_BOM))
|
|
186
|
+
return 0
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _run(path: Path, mode: str, options: _Options) -> int:
|
|
190
|
+
try:
|
|
191
|
+
settings = _settings(path, options)
|
|
192
|
+
except SettingsError as error:
|
|
193
|
+
print(f"prettycsharp: {error}", file=sys.stderr)
|
|
194
|
+
return 2
|
|
195
|
+
try:
|
|
196
|
+
# newline="" keeps the file's own line endings.
|
|
197
|
+
with path.open(encoding="utf-8", newline="") as handle:
|
|
198
|
+
original = handle.read()
|
|
199
|
+
except (OSError, UnicodeDecodeError) as error:
|
|
200
|
+
print(f"{path}: cannot read: {error}", file=sys.stderr)
|
|
201
|
+
return 2
|
|
202
|
+
|
|
203
|
+
try:
|
|
204
|
+
formatted = _format_text(original, path, settings)
|
|
205
|
+
except CSharpSyntaxError as error:
|
|
206
|
+
print(f"{path}: {error}; not formatted", file=sys.stderr)
|
|
207
|
+
return 2
|
|
208
|
+
except FormattingError as error:
|
|
209
|
+
print(f"{path}: not formatted: {error}", file=sys.stderr)
|
|
210
|
+
return 2
|
|
211
|
+
|
|
212
|
+
if mode == "stdout":
|
|
213
|
+
sys.stdout.write(formatted.lstrip(_BOM))
|
|
214
|
+
return 0
|
|
215
|
+
if formatted == original:
|
|
216
|
+
return 0
|
|
217
|
+
if mode == "write":
|
|
218
|
+
try:
|
|
219
|
+
_replace(path, formatted)
|
|
220
|
+
except OSError as error:
|
|
221
|
+
print(f"{path}: cannot write: {error}", file=sys.stderr)
|
|
222
|
+
return 2
|
|
223
|
+
print(f"formatted {path}")
|
|
224
|
+
return 0
|
|
225
|
+
if mode == "diff":
|
|
226
|
+
sys.stdout.writelines(
|
|
227
|
+
difflib.unified_diff(
|
|
228
|
+
original.splitlines(keepends=True),
|
|
229
|
+
formatted.splitlines(keepends=True),
|
|
230
|
+
fromfile=str(path),
|
|
231
|
+
tofile=str(path),
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
else:
|
|
235
|
+
print(f"would format {path}")
|
|
236
|
+
return 1
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _replace(path: Path, text: str) -> None:
|
|
240
|
+
# Write beside the file, then swap it in, so a failed write never leaves
|
|
241
|
+
# the original half-written. The new file keeps the original's mode.
|
|
242
|
+
handle, temporary = tempfile.mkstemp(dir=path.parent, prefix=f".{path.name}.", suffix=".tmp")
|
|
243
|
+
try:
|
|
244
|
+
with os.fdopen(handle, "w", encoding="utf-8", newline="") as stream:
|
|
245
|
+
stream.write(text)
|
|
246
|
+
shutil.copymode(path, temporary)
|
|
247
|
+
os.replace(temporary, path)
|
|
248
|
+
except BaseException:
|
|
249
|
+
Path(temporary).unlink(missing_ok=True)
|
|
250
|
+
raise
|
prettycsharp/config.py
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Finding the settings for a source file.
|
|
2
|
+
|
|
3
|
+
Settings come from, highest precedence first: the command line, the
|
|
4
|
+
nearest ``.prettycsharp.toml`` or ``pyproject.toml`` with a
|
|
5
|
+
``[tool.prettycsharp]`` table, the standard keys of ``.editorconfig``, and
|
|
6
|
+
the defaults in ``Settings``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
import sys
|
|
13
|
+
from collections.abc import Mapping
|
|
14
|
+
from functools import cache
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from prettycsharp.settings import Settings, SettingsError
|
|
18
|
+
|
|
19
|
+
if sys.version_info >= (3, 11):
|
|
20
|
+
import tomllib
|
|
21
|
+
else: # pragma: no cover - exercised on Python 3.10 only
|
|
22
|
+
import tomli as tomllib
|
|
23
|
+
|
|
24
|
+
CONFIG_NAME = ".prettycsharp.toml"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def settings_for(path: Path, overrides: Mapping[str, object] | None = None, config_file: Path | None = None) -> Settings:
|
|
28
|
+
"""The settings for the source file at ``path``."""
|
|
29
|
+
directory = path.resolve().parent
|
|
30
|
+
settings = Settings()
|
|
31
|
+
settings = settings.with_values(editorconfig_values(path.resolve()), ".editorconfig")
|
|
32
|
+
if config_file is not None:
|
|
33
|
+
values, source = read_config(config_file.resolve())
|
|
34
|
+
else:
|
|
35
|
+
found = find_config(directory)
|
|
36
|
+
values, source = ({}, "") if found is None else read_config(found)
|
|
37
|
+
if values:
|
|
38
|
+
settings = settings.with_values(values, source)
|
|
39
|
+
if overrides:
|
|
40
|
+
settings = settings.with_values(overrides, "command line")
|
|
41
|
+
return settings
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@cache
|
|
45
|
+
def find_config(directory: Path) -> Path | None:
|
|
46
|
+
"""The nearest .prettycsharp.toml, or pyproject.toml with our table, at or above ``directory``."""
|
|
47
|
+
for folder in (directory, *directory.parents):
|
|
48
|
+
candidate = folder / CONFIG_NAME
|
|
49
|
+
if candidate.is_file():
|
|
50
|
+
return candidate
|
|
51
|
+
pyproject = folder / "pyproject.toml"
|
|
52
|
+
if pyproject.is_file() and "prettycsharp" in _tool_tables(_load_toml(pyproject)):
|
|
53
|
+
return pyproject
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _tool_tables(data: dict[str, object]) -> dict[str, object]:
|
|
58
|
+
tool = data.get("tool", {})
|
|
59
|
+
return tool if isinstance(tool, dict) else {}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@cache
|
|
63
|
+
def read_config(path: Path) -> tuple[dict[str, object], str]:
|
|
64
|
+
data = _load_toml(path)
|
|
65
|
+
table: object = _tool_tables(data).get("prettycsharp", {}) if path.name == "pyproject.toml" else data
|
|
66
|
+
if not isinstance(table, dict):
|
|
67
|
+
raise SettingsError(f"{path}: [tool.prettycsharp] must be a table")
|
|
68
|
+
return dict(table), str(path)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _load_toml(path: Path) -> dict[str, object]:
|
|
72
|
+
try:
|
|
73
|
+
with path.open("rb") as handle:
|
|
74
|
+
return tomllib.load(handle)
|
|
75
|
+
except tomllib.TOMLDecodeError as error:
|
|
76
|
+
raise SettingsError(f"{path}: {error}") from None
|
|
77
|
+
except OSError as error:
|
|
78
|
+
raise SettingsError(f"{path}: cannot read: {error}") from None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# .editorconfig ------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def editorconfig_values(path: Path) -> dict[str, object]:
|
|
85
|
+
"""Our settings from the .editorconfig files that apply to ``path``."""
|
|
86
|
+
files: list[Path] = []
|
|
87
|
+
for folder in path.parents:
|
|
88
|
+
candidate = folder / ".editorconfig"
|
|
89
|
+
if candidate.is_file():
|
|
90
|
+
files.append(candidate)
|
|
91
|
+
if _parse_editorconfig(candidate)[0]:
|
|
92
|
+
break
|
|
93
|
+
raw: dict[str, str] = {}
|
|
94
|
+
# The file nearest the source wins, so apply the farthest first.
|
|
95
|
+
for config in reversed(files):
|
|
96
|
+
_, sections = _parse_editorconfig(config)
|
|
97
|
+
relative = path.relative_to(config.parent).as_posix()
|
|
98
|
+
for pattern, values in sections:
|
|
99
|
+
if _section_matches(pattern, relative):
|
|
100
|
+
raw.update(values)
|
|
101
|
+
return _map_editorconfig(raw)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@cache
|
|
105
|
+
def _parse_editorconfig(path: Path) -> tuple[bool, tuple[tuple[str, dict[str, str]], ...]]:
|
|
106
|
+
root = False
|
|
107
|
+
sections: list[tuple[str, dict[str, str]]] = []
|
|
108
|
+
try:
|
|
109
|
+
lines = path.read_text(encoding="utf-8-sig").splitlines()
|
|
110
|
+
except (OSError, UnicodeDecodeError):
|
|
111
|
+
return False, ()
|
|
112
|
+
for line in lines:
|
|
113
|
+
text = line.strip()
|
|
114
|
+
if not text or text[0] in "#;":
|
|
115
|
+
continue
|
|
116
|
+
if text.startswith("[") and text.endswith("]"):
|
|
117
|
+
sections.append((text[1:-1], {}))
|
|
118
|
+
continue
|
|
119
|
+
if "=" not in text:
|
|
120
|
+
continue
|
|
121
|
+
key, value = (part.strip() for part in text.split("=", 1))
|
|
122
|
+
key = key.lower()
|
|
123
|
+
if not sections:
|
|
124
|
+
if key == "root":
|
|
125
|
+
root = value.lower() == "true"
|
|
126
|
+
continue
|
|
127
|
+
sections[-1][1][key] = value
|
|
128
|
+
return root, tuple(sections)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _section_matches(pattern: str, relative: str) -> bool:
|
|
132
|
+
if "/" not in pattern:
|
|
133
|
+
pattern = "**/" + pattern
|
|
134
|
+
elif pattern.startswith("/"):
|
|
135
|
+
pattern = pattern[1:]
|
|
136
|
+
return re.fullmatch(_editorconfig_regex(pattern), relative) is not None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@cache
|
|
140
|
+
def _editorconfig_regex(pattern: str) -> str:
|
|
141
|
+
out: list[str] = []
|
|
142
|
+
i = 0
|
|
143
|
+
depth = 0
|
|
144
|
+
while i < len(pattern):
|
|
145
|
+
c = pattern[i]
|
|
146
|
+
if c == "\\" and i + 1 < len(pattern):
|
|
147
|
+
out.append(re.escape(pattern[i + 1]))
|
|
148
|
+
i += 2
|
|
149
|
+
continue
|
|
150
|
+
if c == "*":
|
|
151
|
+
if pattern[i : i + 3] == "**/":
|
|
152
|
+
out.append("(?:.*/)?")
|
|
153
|
+
i += 3
|
|
154
|
+
continue
|
|
155
|
+
if pattern[i : i + 2] == "**":
|
|
156
|
+
out.append(".*")
|
|
157
|
+
i += 2
|
|
158
|
+
continue
|
|
159
|
+
out.append("[^/]*")
|
|
160
|
+
elif c == "?":
|
|
161
|
+
out.append("[^/]")
|
|
162
|
+
elif c == "[":
|
|
163
|
+
end = pattern.find("]", i + 1)
|
|
164
|
+
if end < 0:
|
|
165
|
+
out.append(re.escape(c))
|
|
166
|
+
else:
|
|
167
|
+
body = pattern[i + 1 : end]
|
|
168
|
+
negate = body.startswith("!")
|
|
169
|
+
body = body[1:] if negate else body
|
|
170
|
+
out.append("[" + ("^" if negate else "") + body.replace("\\", "\\\\") + "]")
|
|
171
|
+
i = end + 1
|
|
172
|
+
continue
|
|
173
|
+
elif c == "{":
|
|
174
|
+
end = pattern.find("}", i)
|
|
175
|
+
body = pattern[i + 1 : end] if end > 0 else ""
|
|
176
|
+
numbers = re.fullmatch(r"(-?\d+)\.\.(-?\d+)", body)
|
|
177
|
+
if end > 0 and numbers:
|
|
178
|
+
low, high = sorted(int(n) for n in numbers.groups())
|
|
179
|
+
out.append("(?:" + "|".join(str(n) for n in range(low, high + 1)) + ")")
|
|
180
|
+
i = end + 1
|
|
181
|
+
continue
|
|
182
|
+
if end > 0 and "," in body:
|
|
183
|
+
out.append("(?:")
|
|
184
|
+
depth += 1
|
|
185
|
+
else:
|
|
186
|
+
out.append(re.escape(c))
|
|
187
|
+
elif c == "," and depth:
|
|
188
|
+
out.append("|")
|
|
189
|
+
elif c == "}" and depth:
|
|
190
|
+
out.append(")")
|
|
191
|
+
depth -= 1
|
|
192
|
+
else:
|
|
193
|
+
out.append(re.escape(c))
|
|
194
|
+
i += 1
|
|
195
|
+
return "".join(out)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _map_editorconfig(raw: Mapping[str, str]) -> dict[str, object]:
|
|
199
|
+
values: dict[str, object] = {}
|
|
200
|
+
|
|
201
|
+
def text(key: str) -> str | None:
|
|
202
|
+
value = raw.get(key)
|
|
203
|
+
if value is None:
|
|
204
|
+
return None
|
|
205
|
+
# Roslyn options can carry a severity: true:warning.
|
|
206
|
+
return value.split(":")[0].strip().lower()
|
|
207
|
+
|
|
208
|
+
style = text("indent_style")
|
|
209
|
+
if style in ("space", "tab"):
|
|
210
|
+
values["indent_style"] = style
|
|
211
|
+
size = text("indent_size")
|
|
212
|
+
if size is not None and size.isdigit() and int(size) > 0:
|
|
213
|
+
values["indent_size"] = int(size)
|
|
214
|
+
elif size == "tab":
|
|
215
|
+
width = text("tab_width")
|
|
216
|
+
if width is not None and width.isdigit() and int(width) > 0:
|
|
217
|
+
values["indent_size"] = int(width)
|
|
218
|
+
length = text("max_line_length")
|
|
219
|
+
if length is not None and length.isdigit():
|
|
220
|
+
values["max_line_length"] = int(length)
|
|
221
|
+
elif length == "off":
|
|
222
|
+
values["max_line_length"] = 0
|
|
223
|
+
eol = text("end_of_line")
|
|
224
|
+
if eol in ("lf", "crlf", "cr"):
|
|
225
|
+
values["end_of_line"] = eol
|
|
226
|
+
charset = text("charset")
|
|
227
|
+
if charset in ("utf-8", "utf-8-bom"):
|
|
228
|
+
values["charset"] = charset
|
|
229
|
+
for key in ("insert_final_newline", "trim_trailing_whitespace"):
|
|
230
|
+
flag = text(key)
|
|
231
|
+
if flag in ("true", "false"):
|
|
232
|
+
values[key] = flag == "true"
|
|
233
|
+
brace = text("csharp_new_line_before_open_brace")
|
|
234
|
+
if brace == "all":
|
|
235
|
+
values["brace_style"] = "allman"
|
|
236
|
+
elif brace == "none":
|
|
237
|
+
values["brace_style"] = "kr"
|
|
238
|
+
if any(text(f"csharp_new_line_before_{k}") == "false" for k in ("else", "catch", "finally")):
|
|
239
|
+
values["continuation_keywords"] = "same_line"
|
|
240
|
+
if text("dotnet_sort_system_directives_first") == "true":
|
|
241
|
+
values["using_order"] = "system_first"
|
|
242
|
+
namespaces = text("csharp_style_namespace_declarations")
|
|
243
|
+
if namespaces == "file_scoped":
|
|
244
|
+
values["namespace_style"] = "file_scoped"
|
|
245
|
+
elif namespaces == "block_scoped":
|
|
246
|
+
values["namespace_style"] = "block"
|
|
247
|
+
braces = text("csharp_prefer_braces")
|
|
248
|
+
if braces == "true":
|
|
249
|
+
values["braces"] = "always"
|
|
250
|
+
elif braces == "when_multiline":
|
|
251
|
+
values["braces"] = "single_line_only"
|
|
252
|
+
elif braces == "false":
|
|
253
|
+
values["braces"] = "preserve"
|
|
254
|
+
order = raw.get("csharp_preferred_modifier_order")
|
|
255
|
+
if order:
|
|
256
|
+
values["modifier_order"] = [m.strip() for m in order.split(":")[0].split(",") if m.strip()]
|
|
257
|
+
return values
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def clear_caches() -> None:
|
|
261
|
+
find_config.cache_clear()
|
|
262
|
+
read_config.cache_clear()
|
|
263
|
+
_parse_editorconfig.cache_clear()
|