1from dataclasses import dataclass, field, fields
2from enum import Enum
3from pathlib import Path
4from typing import Any, Required, TypedDict, cast
5
6from jsonschema import ValidationError, validate
7
8COMMENT_FILETYPE = {
9 "cpp": ["c", "ci", "cpp", "cc", "cxx", "h", "hpp", "hxx", "hh", "ihl"],
10 "python": ["py"],
11 "cs": ["cs"],
12 # ".mts"/".cts" are TypeScript's own ESM/CJS module variants. ".js"/".jsx"/
13 # ".mjs"/".cjs" are JavaScript. All of these share the "ts" comment type:
14 # comment syntax is identical across the family, and the analyse stage
15 # picks the actual tree-sitter grammar per file from the suffix (the plain
16 # TypeScript grammar for ".ts"/".mts"/".cts", the TSX grammar for
17 # everything else — see utils.ts_grammar_key), so no separate
18 # comment_type value is needed here.
19 "ts": ["ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs"],
20 "yaml": ["yml", "yaml"],
21 "rust": ["rs"],
22 "go": ["go"],
23 "jsonc": ["jsonc", "json"],
24 # Bash uses `#` line comments; zsh and ksh share the same comment syntax and
25 # are scanned with the bash grammar. Fish is intentionally excluded: it is
26 # not POSIX-compatible and no tree-sitter-fish distribution is published on
27 # PyPI, so it cannot be wired in here. Track fish separately if a PyPI
28 # grammar becomes available.
29 "bash": ["sh", "bash", "zsh", "ksh"],
30 "vhdl": ["vhd", "vhdl"],
31}
32
33
34# Default ``exclude`` glob patterns applied to ``ts`` (TypeScript/JavaScript
35# family) projects when their configuration does not set ``exclude``
36# explicitly.
37#
38# ``src_dir`` defaults to ``"./"`` and the ``ts`` comment type claims ``.js``/
39# ``.jsx``/``.mjs``/``.cjs`` in addition to TypeScript's own extensions, so a
40# checked-in ``tsc``/bundler output directory (``dist/``, ``build/``, ...) is
41# otherwise scanned as source alongside the ``.ts`` it was generated from,
42# producing duplicate need ids for the same marker. These directory names are
43# common generated-output or dependency locations across the JS/TS ecosystem,
44# so excluding them by default avoids that duplication for most projects out
45# of the box.
46#
47# ``**/lib/**`` is deliberately NOT in this list: it is ambiguous even within
48# the JS/TS ecosystem, where many packages use ``lib/`` for hand-written
49# source rather than as a ``tsc`` ``outDir``. Projects whose ``outDir`` is
50# ``lib`` should add ``"**/lib/**"`` to their own ``exclude`` explicitly.
51#
52# This default is applied only for ``comment_type == "ts"``. Every other
53# ``comment_type`` (``cpp``, ``python``, ``rust``, ``go``, ``yaml``, ``jsonc``,
54# ``bash``, ``cs``, ...) defaults ``exclude`` to ``[]`` — unchanged from
55# before this default existed. ``cpp`` projects in particular very commonly
56# keep hand-written library source under ``lib/``, so a directory-name-based
57# default that isn't scoped to the language family would silently drop
58# markers there.
59#
60# Setting ``exclude`` explicitly in a project's configuration replaces this
61# default outright (it is not merged) — including setting it to ``[]`` to
62# scan everything.
63TS_DEFAULT_EXCLUDE = [
64 "**/node_modules/**",
65 "**/dist/**",
66 "**/build/**",
67 "**/out/**",
68 "**/coverage/**",
69]
70
71
72class CommentType(str, Enum): # noqa: UP042 # StrEnum changes str(member), which reaches CLI warnings and error messages
73 python = "python"
74 cpp = "cpp"
75 cs = "cs"
[docs] 76 # @Support TypeScript style comments, IMPL_TS_1, impl, [FE_TS]
77 ts = "ts"
78 yaml = "yaml"
[docs] 79 # @Support Rust style comments, IMPL_RUST_1, impl, [FE_RUST]
80 rust = "rust"
[docs] 81 # @Support Go style comments, IMPL_GO_1, impl, [FE_GO]
82 go = "go"
[docs] 83 # @Support JSONC style comments, IMPL_JSONC_1, impl, [FE_JSONC]
84 jsonc = "jsonc"
[docs] 85 # @Support Bash style comments, IMPL_BASH_1, impl, [FE_BASH]
86 bash = "bash"
[docs] 87 # @Support VHDL style comments, IMPL_VHDL_1, impl, [FE_VHDL]
88 vhdl = "vhdl"
89
90
91def default_exclude_for_comment_type(comment_type: str) -> list[str]:
92 """Resolve the default ``exclude`` patterns for a given ``comment_type``.
93
94 Only the ``ts`` (TypeScript/JavaScript) family gets a non-empty default —
95 see ``TS_DEFAULT_EXCLUDE`` for why. Every other ``comment_type`` defaults
96 to ``[]``, i.e. no default exclusion at all, matching the behavior before
97 a default was ever introduced.
98 """
99 if comment_type == CommentType.ts:
100 return list(TS_DEFAULT_EXCLUDE)
101 return []
102
103
104class SourceDiscoverSectionConfigType(TypedDict, total=False):
105 """Define typing for loading configuration from TOML files"""
106
107 src_dir: Required[str]
108 exclude: list[str]
109 include: list[str]
110 gitignore: bool
111 follow_links: bool
112 comment_type: CommentType
113
114
115class SourceDiscoverConfigType(TypedDict, total=False):
116 """Define typing for its API configuration"""
117
118 src_dir: Required[Path]
119 exclude: list[str]
120 include: list[str]
121 gitignore: bool
122 follow_links: bool
123 comment_type: CommentType
124
125
126@dataclass
127class SourceDiscoverConfig:
128 @classmethod
129 def field_names(cls) -> set[str]:
130 return {item.name for item in fields(cls)}
131
132 src_dir: Path = field(
133 default_factory=lambda: Path("./"), metadata={"schema": {"type": "string"}}
134 )
135 """The root of the source directory."""
136
137 exclude: list[str] | None = field(
138 default=None,
139 metadata={"schema": {"type": "array", "items": {"type": "string"}}},
140 )
141 """The glob pattern to exclude files.
142
143 Leave unset (``None``) to get the ``comment_type``-derived default
144 resolved in ``__post_init__`` (see ``default_exclude_for_comment_type``):
145 ``TS_DEFAULT_EXCLUDE`` for ``comment_type == "ts"``, ``[]`` for every
146 other ``comment_type``. Set this explicitly — including to ``[]`` — to
147 replace that default outright; it is never merged with it.
148 """
149
150 include: list[str] = field(
151 default_factory=list,
152 metadata={"schema": {"type": "array", "items": {"type": "string"}}},
153 )
154 """The glob pattern to include files."""
155
156 gitignore: bool = field(default=True, metadata={"schema": {"type": "boolean"}})
157 """Whether to respect .gitignore to exclude files."""
158
159 follow_links: bool = field(default=False, metadata={"schema": {"type": "boolean"}})
160 """Whether to follow symbolic links during file discovery."""
161
162 comment_type: str = field(
163 default="cpp",
164 metadata={
165 "schema": {
166 "type": "string",
167 "enum": sorted(COMMENT_FILETYPE),
168 }
169 },
170 )
171 """The file types to discover."""
172
173 def __post_init__(self) -> None:
174 # ``None`` means "the user didn't set exclude" (a dataclass field
175 # default can't otherwise be told apart from an explicit ``[]``) —
176 # resolve it to the comment_type-derived default. An explicit value,
177 # including ``[]``, is left untouched.
178 if self.exclude is None:
179 self.exclude = default_exclude_for_comment_type(self.comment_type)
180
181 @classmethod
182 def get_schema(cls, name: str) -> dict[str, Any] | None:
183 _field = next(_field for _field in fields(cls) if _field.name is name)
184 if _field.metadata and "schema" in _field.metadata:
185 return cast(dict[str, Any], _field.metadata["schema"])
186 return None
187
188 def check_schema(self) -> list[str]:
189 errors = []
190 for _field_name in self.field_names():
191 schema = self.get_schema(_field_name)
192 if schema is None:
193 # no schema to check against; `validate` would raise TypeError
194 continue
195 value = getattr(self, _field_name)
196 if isinstance(value, Path): # adapt to json schema restriction
197 value = str(value)
198 try:
199 validate(
200 instance=value,
201 schema=schema,
202 ) # validate has no type specified
203 except ValidationError as e:
204 errors.append(
205 f"Schema validation error in field '{_field_name}': {e.message}"
206 )
207 return errors