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