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 "yaml": ["yml", "yaml"],
13 "rust": ["rs"],
14 "go": ["go"],
15 "jsonc": ["jsonc", "json"],
16 # Bash uses `#` line comments; zsh and ksh share the same comment syntax and
17 # are scanned with the bash grammar. Fish is intentionally excluded: it is
18 # not POSIX-compatible and no tree-sitter-fish distribution is published on
19 # PyPI, so it cannot be wired in here. Track fish separately if a PyPI
20 # grammar becomes available.
21 "bash": ["sh", "bash", "zsh", "ksh"],
22}
23
24
25class CommentType(str, Enum): # noqa: UP042 # StrEnum changes str(member), which reaches CLI warnings and error messages
26 python = "python"
27 cpp = "cpp"
28 cs = "cs"
29 yaml = "yaml"
[docs] 30 # @Support Rust style comments, IMPL_RUST_1, impl, [FE_RUST]
31 rust = "rust"
[docs] 32 # @Support Go style comments, IMPL_GO_1, impl, [FE_GO]
33 go = "go"
[docs] 34 # @Support JSONC style comments, IMPL_JSONC_1, impl, [FE_JSONC]
35 jsonc = "jsonc"
[docs] 36 # @Support Bash style comments, IMPL_BASH_1, impl, [FE_BASH]
37 bash = "bash"
38
39
40class SourceDiscoverSectionConfigType(TypedDict, total=False):
41 """Define typing for loading configuration from TOML files"""
42
43 src_dir: Required[str]
44 exclude: list[str]
45 include: list[str]
46 gitignore: bool
47 follow_links: bool
48 comment_type: CommentType
49
50
51class SourceDiscoverConfigType(TypedDict, total=False):
52 """Define typing for its API configuration"""
53
54 src_dir: Required[Path]
55 exclude: list[str]
56 include: list[str]
57 gitignore: bool
58 follow_links: bool
59 comment_type: CommentType
60
61
62@dataclass
63class SourceDiscoverConfig:
64 @classmethod
65 def field_names(cls) -> set[str]:
66 return {item.name for item in fields(cls)}
67
68 src_dir: Path = field(
69 default_factory=lambda: Path("./"), metadata={"schema": {"type": "string"}}
70 )
71 """The root of the source directory."""
72
73 exclude: list[str] = field(
74 default_factory=list,
75 metadata={"schema": {"type": "array", "items": {"type": "string"}}},
76 )
77 """The glob pattern to exclude files."""
78
79 include: list[str] = field(
80 default_factory=list,
81 metadata={"schema": {"type": "array", "items": {"type": "string"}}},
82 )
83 """The glob pattern to include files."""
84
85 gitignore: bool = field(default=True, metadata={"schema": {"type": "boolean"}})
86 """Whether to respect .gitignore to exclude files."""
87
88 follow_links: bool = field(default=False, metadata={"schema": {"type": "boolean"}})
89 """Whether to follow symbolic links during file discovery."""
90
91 comment_type: str = field(
92 default="cpp",
93 metadata={
94 "schema": {
95 "type": "string",
96 "enum": sorted(COMMENT_FILETYPE),
97 }
98 },
99 )
100 """The file types to discover."""
101
102 @classmethod
103 def get_schema(cls, name: str) -> dict[str, Any] | None:
104 _field = next(_field for _field in fields(cls) if _field.name is name)
105 if _field.metadata and "schema" in _field.metadata:
106 return cast(dict[str, Any], _field.metadata["schema"])
107 return None
108
109 def check_schema(self) -> list[str]:
110 errors = []
111 for _field_name in self.field_names():
112 schema = self.get_schema(_field_name)
113 value = getattr(self, _field_name)
114 if isinstance(value, Path): # adapt to json schema restriction
115 value = str(value)
116 try:
117 validate(
118 instance=value,
119 schema=schema, # ty: ignore[invalid-argument-type]
120 ) # validate has no type specified
121 except ValidationError as e:
122 errors.append(
123 f"Schema validation error in field '{_field_name}': {e.message}"
124 )
125 return errors