From 110f0a556f21ed007c59fcc183c377775cbbd5fe Mon Sep 17 00:00:00 2001 From: SiriusStarr <2049163+SiriusStarr@users.noreply.github.com> Date: Wed, 8 Jul 2026 11:43:39 -0700 Subject: [PATCH 01/23] Add initial pass on config parsing --- jdlint.py | 447 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 445 insertions(+), 2 deletions(-) diff --git a/jdlint.py b/jdlint.py index 18937a3..b9bd692 100755 --- a/jdlint.py +++ b/jdlint.py @@ -9,10 +9,453 @@ import json import os import re -import sys +import typing from dataclasses import dataclass from pathlib import Path, PurePath -from typing import Any, Callable, Literal, TypeVar +from typing import TYPE_CHECKING, Any, Literal, TypeVar + +if TYPE_CHECKING: + from collections.abc import Callable +import tomllib + + +class ConfigError(Exception): + """An error in the jdlint config.""" + + def __init__(self, key: str, message: str) -> None: + """Create a config error, given the key it occurs at and a message.""" + super().__init__(f"Error in config at key: {key}. {message}") + + +class ConfigKeyError(ConfigError): + """An unexpected key in the jdlint config.""" + + def __init__(self, key: str, valid: list[str]) -> None: + """Create a key error, given the extra key and a list of valid keys.""" + super().__init__( + key, + f"Not an expected key. Valid keys are: [{', '.join(valid)}]", + ) + + +class ConfigTypeError(ConfigError): + """A value with the wrong type in the jdlint config.""" + + def __init__(self, key: str, expected: str, got: str) -> None: + """Create a type error, given the key it occurs at, the expected type, and the actual type.""" + super().__init__(key, f"Wrong type. Expected: {expected} Got: {got}") + + +class ConfigValueError(ConfigError): + """A bad value in the jdlint config.""" + + def __init__(self, key, issue, got): + """Create a value error, given the key it occurs at, the issue with the value, and the actual value.""" + super().__init__(key, f"Bad value. Got: {got} Issue: {issue}") + + +class ConfigConflictError(ConfigError): + """A conflict in the jdlint config.""" + + def __init__(self, key, issue): + """Create a conflict error, given the key it occurs at and the issue.""" + super().__init__(key, f"Conflict in config. Issue: {issue}") + + +class ConfigSystemRoot: + """A root (base folder) of a JD system to check for correctness, e.g. ~/Documents.""" + + def __init__(self, at: str, from_file: dict) -> None: + """Create a valid configuration given a loaded section of a config file.""" + # Acquire and set defaults + self.name = from_file.pop("name") + self.path = Path(from_file.pop("path")) + self.ignore = from_file.pop("ignore", []) + + if not isinstance(self.name, str): + raise ConfigTypeError( + f"{at}.name", + "str", + type(self.name).__name__, + ) + + # Validate path is good + if not self.path.is_dir(): + ConfigValueError( + f"{at}.path", + "Root path isn't a folder that exists!", + self.path, + ) + if not isinstance(self.ignore, list): + raise ConfigTypeError( + f"{at}.ignore", + "list", + type(self.ignore).__name__, + ) + for r in self.ignore: + if not isinstance(r, str): + raise ConfigTypeError(f"{at}.ignore", "str", type(r).__name__) + + # Ensure no extra fields + for key in from_file: + raise ConfigKeyError(f"{at}.{key}", list(self.__dict__.keys())) + + +class ConfigSystemJDex: + """Valid configuration for the JDex of a system.""" + + def __init__(self, at: str, from_file: dict) -> None: + """Create a valid configuration given a loaded section of a config file.""" + # Acquire and set defaults + self.path = Path(from_file.pop("path")) + + # Validate path is good + if not self.path.is_dir(): + ConfigValueError( + f"{at}.path", + "JDex path isn't a folder that exists!", + self.path, + ) + + self.children = [ + ConfigJDexTier( + f"{at}.children[{i}]", + [], + v, + ) + for i, v in enumerate(from_file.pop("children", [])) + ] + self.notes = [ + ConfigJDexNotes( + f"{at}.notes[{i}]", + [], + v, + ) + for i, v in enumerate(from_file.pop("notes", [])) + ] + if self.notes and self.children: + raise ConfigConflictError( + at, + "Only one of notes and children may be specified.", + ) + + # Ensure no extra fields + for key in from_file: + raise ConfigKeyError(f"{at}.{key}", list(self.__dict__.keys())) + + +class ConfigLinter: + """Valid configuration for the linter.""" + + def __init__(self, from_file: dict) -> None: + """Create a valid configuration given a loaded linter section of a config file.""" + # Acquire and set defaults + self.disable_rules = from_file.pop("disable_rules", []) + self.json_output = from_file.pop("json_output", False) + self.ignore = from_file.pop("ignore", []) + + # Validate + if not isinstance(self.disable_rules, list): + raise ConfigTypeError( + "linter.disable_rules", + "list", + type(self.disable_rules).__name__, + ) + for r in self.disable_rules: + if r in [e.type for e in typing.get_args(ErrorType)]: + continue + raise ConfigValueError("linter.disable_rules", "not a valid rule name", r) + + if not isinstance(self.json_output, bool): + raise ConfigTypeError( + "linter.json_output", + "bool", + type(self.json_output).__name__, + ) + + if not isinstance(self.ignore, list): + raise ConfigTypeError( + "linter.ignore", + "list", + type(self.ignore).__name__, + ) + for r in self.ignore: + if not isinstance(r, str): + raise ConfigTypeError("linter.ignore", "str", type(r).__name__) + + # Ensure no extra fields + for key in from_file: + raise ConfigKeyError(f"linter.{key}", list(self.__dict__.keys())) + + +class ConfigSystem: + """Valid configuration for the JD system.""" + + def __init__(self, from_file: dict) -> None: + """Create a valid configuration given a loaded system section of a config file.""" + self.roots = [ + ConfigSystemRoot( + f"system.roots[{i}]", + v, + ) + for i, v in enumerate(from_file.pop("roots", [])) + ] + + if "jdex" in from_file: + self.jdex = ConfigSystemJDex("system.jdex", from_file.pop("jdex")) + else: + self.jdex = None + + self.children = [ + ConfigSystemTier( + f"system.children[{i}]", + [], + v, + ) + for i, v in enumerate(from_file.pop("children", [])) + ] + # Ensure no extra fields + for key in from_file: + raise ConfigKeyError(f"system.{key}", list(self.__dict__.keys())) + + +class ConfigJDexNotes: + """Configuration for how JDex notes are formatted.""" + + def __init__( + self, + at: str, + parent_segments: list[str], + from_file: dict, + ) -> None: + """Create a valid note format given a loaded section of a config file.""" + # Acquire and set defaults + self.name = from_file.pop("name") + + # Validate + if not isinstance(self.name, str): + raise ConfigTypeError( + f"{at}.name", + "str", + type(self.name).__name__, + ) + if not isinstance(from_file["format"], str): + raise ConfigTypeError( + f"{at}.format", + "str", + type(from_file["format"]).__name__, + ) + # Compile Format + self.format = ConfigFormat( + f"{at}.format", + parent_segments, + from_file.pop("format"), + ) + + # Ensure no extra fields + for key in from_file: + raise ConfigKeyError(f"{at}.{key}", list(self.__dict__.keys())) + + +class ConfigFolderTier: + """A tier (hierarchical level) of a JD system, e.g. a Category, whether in the JDex or the system itself.""" + + def __init__( + self, + child_class: Callable, + at: str, + parent_segments: list[str], + from_file: dict, + ) -> None: + """Create a valid tier given a loaded section of a config file.""" + # Acquire and set defaults + self.name = from_file.pop("name") + self.allow_arbitrary_contents = from_file.pop("allow_arbitrary_contents", False) + + # Validate + if not isinstance(self.name, str): + raise ConfigTypeError( + f"{at}.name", + "str", + type(self.name).__name__, + ) + if not isinstance(from_file["format"], str): + raise ConfigTypeError( + f"{at}.format", + "str", + type(from_file["format"]).__name__, + ) + if not isinstance(self.allow_arbitrary_contents, bool): + raise ConfigTypeError( + f"{at}.allow_arbitrary_contents", + "bool", + type(self.allow_arbitrary_contents).__name__, + ) + + # Compile Format & Children + self.format = ConfigFormat( + f"{at}.format", + parent_segments, + from_file.pop("format"), + ) + self.children = [ + child_class( + f"{at}.children[{i}]", + self.format.known_segments, + v, + ) + for i, v in enumerate(from_file.pop("children", [])) + ] + if self.children and self.allow_arbitrary_contents: + raise ConfigConflictError( + at, + "If children are specified, allow_arbitrary_contents must be false.", + ) + + +class ConfigSystemTier(ConfigFolderTier): + """A tier (hierarchical level) of a JD system, e.g. a Category, in the system (not the JDex).""" + + def __init__(self, at: str, parent_segments: list[str], from_file: dict) -> None: + """Create a valid tier given a loaded section of a config file.""" + # Acquire and set defaults + self.jdex_note = from_file.pop("jdex_note", None) + self.can_be_file = from_file.pop("can_be_file", False) + + # Call the folder tier stuff + super().__init__(ConfigSystemTier, at, parent_segments, from_file) + + # Validate + if self.jdex_note is not None and not isinstance(self.jdex_note, str): + raise ConfigTypeError( + f"{at}.jdex_note", + "str", + type(self.jdex_note).__name__, + ) + if not isinstance(self.can_be_file, bool): + raise ConfigTypeError( + f"{at}.can_be_file", + "bool", + type(self.can_be_file).__name__, + ) + + if self.children and (self.can_be_file): + raise ConfigConflictError( + at, + "If children are specified, can_be_file must be false.", + ) + # Ensure no extra fields + for key in from_file: + raise ConfigKeyError(f"{at}.{key}", list(self.__dict__.keys())) + + +class ConfigJDexTier(ConfigFolderTier): + """A tier (hierarchical level) of a JD system, e.g. a Category, in the JDex.""" + + def __init__(self, at: str, parent_segments: list[str], from_file: dict) -> None: + """Create a valid tier given a loaded section of a config file.""" + # Call the folder tier stuff + super().__init__(ConfigJDexTier, at, parent_segments, from_file) + + self.notes = [ + ConfigJDexNotes( + f"{at}.notes[{i}]", + self.format.known_segments, + v, + ) + for i, v in enumerate(from_file.pop("notes", [])) + ] + + if not self.notes and not self.children: + raise ConfigConflictError( + at, + "A JDex tier must specify either notes or children.", + ) + + if self.notes and self.children: + raise ConfigConflictError( + at, + "Only one of notes and children may be specified.", + ) + + # Ensure no extra fields + for key in from_file: + raise ConfigKeyError(f"{at}.{key}", list(self.__dict__.keys())) + + +class ConfigFormat: + """A format for a file or folder.""" + + # A valid variable segment of a format + variable_segment_re = re.compile(r"(=|\*|[#]+)([A-Za-z]+)") + + def __init__(self, at: str, parent_segments: list[str], from_file: str) -> None: + """Create a valid format given a string from a config file.""" + if from_file.count("/") % 2 != 0: + raise ConfigValueError( + at, + "Malfored format; there must be an even number of / characters. You have an extra one/are missing one.", + from_file, + ) + if from_file == "": + raise ConfigValueError( + at, + "Malfored format; must not be empty.", + from_file, + ) + regex = [] + new_segments = [] + for i, v in enumerate(from_file.split("/")): + if i % 2 == 0: + # Literal segment + regex.append(lambda _, v=v: re.escape(v)) + else: + # Variable segment + match = ConfigFormat.variable_segment_re.fullmatch(v) + if not match: + raise ConfigValueError( + at, + "Malfored format; variable segment must consist of =, *, or one or more # followed by an alphabetic identifier.", + v, + ) + if match.group(1) == "=": + if match.group(2) not in parent_segments: + raise ConfigValueError( + at, + "Malfored format; variable segment referenced an identifier not bound in a parent.", + v, + ) + p = match.group(2) + regex.append(lambda d, p=p: re.escape(d[p])) + else: + if match.group(2) in parent_segments: + raise ConfigValueError( + at, + "Malfored format; variable segment tried to rebind an identifier already bound in a parent.", + v, + ) + identifier = match.group(2) + new_segments.append(match.group(2)) + if match.group(1) == "*": + regex.append( + lambda _, identifier=identifier: f"(?P<{identifier}>.+)", + ) + else: + # Must be a ## type variable + regex.append( + lambda _, identifier=identifier, match_len=len(match.group(1)): ( + f"(?P<{identifier}>[0-9]{{{match_len}}})" + ), + ) + + self.known_segments = parent_segments + new_segments + self.build_regex = lambda d: "".join([f(d) for f in regex]) + + +class Config: + def __init__(self, from_file): + self.linter = ConfigLinter(from_file.get("linter", {})) + self.system = ConfigSystem(from_file["system"]) @dataclass(frozen=True) From 8bdaf333cbac9d51c7bf0c37366a9752c0e52670 Mon Sep 17 00:00:00 2001 From: SiriusStarr <2049163+SiriusStarr@users.noreply.github.com> Date: Thu, 9 Jul 2026 15:33:44 -0700 Subject: [PATCH 02/23] Begin adding tests --- jdlint.py | 1241 +++++++++-------- run_tests.py | 93 +- .../files/1/1A/1A.md} | 0 .../files/1/1A/File} | 0 .../files/1/1A/Folder}/.placeholder | 0 .../files/1/A1/File} | 0 .../files/1/A1/Folder}/.placeholder | 0 .../files/1/A1/X.md} | 0 .../files/1/File} | 0 .../files/1/Folder}/.placeholder | 0 .../jdlint.toml | 39 + .../result.json | 35 + .../files/.obsidian}/.placeholder | 0 .../files/1/1A/1A.md} | 0 .../files/1/1B} | 0 .../files/3} | 0 .../jdlint.toml | 32 + .../result.json | 15 + .../area_different_from_jdex_flat/result.json | 18 - .../01.03 Another ID/File Inside ID | 0 .../01 System Stuff/01.00 An ID.md | 0 .../01 System Stuff/01.02 A Name.md | 0 .../01 System Stuff/01.03 Another ID.md | 0 .../result.json | 18 - .../01.03 Another ID/File Inside ID | 0 .../10-19 Oops/11 Cat/11.12 ID/.placeholder | 0 .../area_not_in_jdex/jdex/00.00 System.md | 0 .../jdex/01.00 System Stuff.md | 0 .../area_not_in_jdex/jdex/01.02 A Name.md | 0 .../area_not_in_jdex/jdex/01.03 Another ID.md | 0 .../area_not_in_jdex/jdex/11.00 Cat.md | 0 .../area_not_in_jdex/jdex/11.12 ID.md | 0 tests/with_jdex/area_not_in_jdex/result.json | 17 - .../01 System Stuf/01.02 A Name/.placeholder | 0 .../Directory Inside Id/.placeholder | 0 .../01.03 Another ID/File Inside ID | 0 .../jdex/00.00 System.md | 0 .../jdex/01.00 System Stuff.md | 0 .../jdex/01.02 A Name.md | 0 .../jdex/01.03 Another ID.md | 0 .../result.json | 20 - .../01 System Stuf/01.00 An ID/Other File | 0 .../01 System Stuf/01.02 A Name/.placeholder | 0 .../Directory Inside Id/.placeholder | 0 .../01.03 Another ID/File Inside ID | 0 .../01 System Stuff/01.00 An ID.md | 0 .../01 System Stuff/01.02 A Name.md | 0 .../01 System Stuff/01.03 Another ID.md | 0 .../result.json | 20 - .../01 System Stuff/01.02 An ID/.placeholder | 0 .../Directory Inside Id/.placeholder | 0 .../01.03 Another ID/File Inside ID | 0 .../category_not_in_jdex/jdex/00.00 System.md | 0 .../category_not_in_jdex/jdex/01.02 An ID.md | 0 .../jdex/01.03 Another ID.md | 0 .../category_not_in_jdex/result.json | 19 - tests/with_jdex/flat_alt_zeros/altzeros | 0 .../Directory Inside Id/.placeholder | 0 .../01.03 Area Standard Zero/File Inside ID | 0 .../10.02 An ID/.placeholder | 0 .../jdex/00.00 System Area Management.md | 0 .../jdex/01.00 Life Admin Area Management.md | 0 .../jdex/01.03 Area Standard Zero.md | 0 .../jdex/10.00 Me, Myself, and I.md | 0 .../flat_alt_zeros/jdex/10.02 An ID.md | 0 tests/with_jdex/flat_alt_zeros/result.json | 4 - .../flat_alt_zeros_duplicate_header/altzeros | 0 .../Directory Inside Id/.placeholder | 0 .../01.03 Area Standard Zero/File Inside ID | 0 .../10.02 An ID/.placeholder | 0 .../jdex/00.00 System Area Management.md | 0 .../jdex/01.00 Life Admin Area Management.md | 0 .../jdex/01.03 Area Standard Zero.md | 0 .../jdex/10. Life Admin.md | 0 .../jdex/10. Life Adminn.md | 0 .../jdex/10.00 Me, Myself, and I.md | 0 .../jdex/10.02 An ID.md | 0 .../result.json | 21 - .../flat_alt_zeros_with_headers/altzeros | 0 .../Directory Inside Id/.placeholder | 0 .../01.03 Area Standard Zero/File Inside ID | 0 .../10.02 An ID/.placeholder | 0 .../jdex/00.00 System Area Management.md | 0 .../jdex/01.00 Life Admin Area Management.md | 0 .../jdex/01.03 Area Standard Zero.md | 0 .../jdex/10. Life Admin.md | 0 .../jdex/10.00 Me, Myself, and I.md | 0 .../jdex/10.02 An ID.md | 0 .../flat_alt_zeros_with_headers/result.json | 4 - .../01 System Stuff/01.02 A Name/.placeholder | 0 .../Directory Inside Id/.placeholder | 0 .../01.03 Another ID/File Inside ID | 0 .../jdex/00.00 System Index.md | 0 .../jdex/01.00 System Stuff Index.md | 0 .../jdex/01.02 A Name.md | 0 .../jdex/01.03 Another ID.md | 0 .../flat_with_index_suffix/result.json | 4 - .../01 System Stuff/01.02 A Name/.placeholder | 0 .../Directory Inside Id/.placeholder | 0 .../01.03 Another ID/File Inside ID | 0 .../jdex/00.00 System Area Management.md | 0 .../01.00 System Stuff Category Management.md | 0 .../jdex/01.02 A Name.md | 0 .../jdex/01.03 Another ID.md | 0 .../flat_with_management_suffixes/result.json | 4 - .../01 System Stuff/01.02 A Naem/.placeholder | 0 .../Directory Inside Id/.placeholder | 0 .../01.03 Another ID/File Inside ID | 0 .../01 System Stuff/01.04 An ID/Other File | 0 .../jdex/00.00 System.md | 0 .../jdex/01.00 System Stuff.md | 0 .../jdex/01.02 A Name.md | 0 .../jdex/01.03 Another ID.md | 0 .../jdex/01.04 An ID.md | 0 .../id_different_from_jdex/result.json | 21 - .../01.02 Missing ID/.placeholder | 0 .../Directory Inside Id/.placeholder | 0 .../01.03 Another ID/File Inside ID | 0 .../id_not_in_jdex/jdex/00.00 System.md | 0 .../id_not_in_jdex/jdex/01.00 System Stuff.md | 0 .../id_not_in_jdex/jdex/01.03 Another ID.md | 0 tests/with_jdex/id_not_in_jdex/result.json | 20 - .../altzeros | 0 .../Directory Inside Id/.placeholder | 0 .../01.03 Area Standard Zero/File Inside ID | 0 .../10.02 An ID/.placeholder | 0 .../jdex/00.00 System Area Management.md | 0 .../jdex/01.00 Life Admin Area Management.md | 0 .../jdex/01.03 Area Standard Zero.md | 0 .../jdex/10. Life Adminn.md | 0 .../jdex/10.00 Me, Myself, and I.md | 0 .../jdex/10.02 An ID.md | 0 .../result.json | 18 - .../jdex_area_header_without_area/altzeros | 0 .../Directory Inside Id/.placeholder | 0 .../01.03 Area Standard Zero/File Inside ID | 0 .../10.02 An ID/.placeholder | 0 .../jdex/00.00 System Area Management.md | 0 .../jdex/01.00 Life Admin Area Management.md | 0 .../jdex/01.03 Area Standard Zero.md | 0 .../jdex/10. Life Admin.md | 0 .../jdex/10.00 Me, Myself, and I.md | 0 .../jdex/10.02 An ID.md | 0 .../jdex/20. Digital Stuff.md | 0 .../jdex_area_header_without_area/result.json | 17 - .../Directory Inside Id/.placeholder | 0 .../01.00 An ID/File Inside ID | 0 .../01 System Stuff/01.00 An ID.md | 0 .../00-09 System/11 Whoops/11.01 Inbox.md | 0 .../jdex_category_in_wrong_area/result.json | 20 - .../Directory Inside Id/.placeholder | 0 .../01 A Category/01.00 An ID/File Inside ID | 0 .../jdex/00.00 A Reuse.md | 0 .../jdex/00.00 An Area.md | 0 .../jdex/01.00 A Category.md | 0 .../jdex/01.01 An ID.md | 0 .../jdex/02.00 An Id.md | 0 .../jdex_duplicate_area_flat/result.json | 37 - .../jdex_duplicate_area_header/altzeros | 0 .../Directory Inside Id/.placeholder | 0 .../10 A Category/10.02 An Id/File Inside ID | 0 .../jdex/01.00 An Area.md | 0 .../jdex/10. A Reuse.md | 0 .../jdex/10. An Area.md | 0 .../jdex/10.00 A Category.md | 0 .../jdex/10.02 An Id.md | 0 .../jdex_duplicate_area_header/result.json | 21 - .../Directory Inside Id/.placeholder | 0 .../01 A Category/01.00 An ID/File Inside ID | 0 .../02 Another Category/02.00 An Id | 0 .../00-09 An Area/01 A Category/01.00 An ID | 0 .../jdex_duplicate_area_nested/result.json | 21 - .../Directory Inside Id/.placeholder | 0 .../01 A Category/01.00 An ID/File Inside ID | 0 .../jdex/00.00 An Area.md | 0 .../jdex/01.00 A Category.md | 0 .../jdex/01.00 A Reuse.md | 0 .../jdex/01.01 An ID.md | 0 .../jdex/02.00 An Id.md | 0 .../jdex_duplicate_category_flat/result.json | 37 - .../Directory Inside Id/.placeholder | 0 .../01 A Category/01.00 An ID/File Inside ID | 0 .../00-09 An Area/01 A Category/01.00 An ID | 0 .../00-09 An Area/01 A Reuse/01.02 Another ID | 0 .../result.json | 25 - .../Directory Inside Id/.placeholder | 0 .../01 A Category/01.00 An ID/File Inside ID | 0 .../jdex/00.00 An Area.md | 0 .../jdex/01.00 A Category.md | 0 .../jdex/01.01 A Reuse.md | 0 .../jdex/01.01 An ID.md | 0 .../jdex_duplicate_id_flat/result.json | 21 - .../Directory Inside Id/.placeholder | 0 .../01 A Category/01.00 An ID/File Inside ID | 0 .../01 A Category/01.01 A Reuse.md | 0 .../01 A Category/01.01 An ID.md | 0 .../jdex_duplicate_id_nested/result.json | 27 - .../01 System Stuff/01.02 A Name/.placeholder | 0 .../Directory Inside Id/.placeholder | 0 .../01.03 Another ID/File Inside ID | 0 .../01 System Stuff/01.02 A Name.md | 0 .../01 System Stuff/01.03 Another ID.md | 0 .../jdex/00-09 System/Nor here | 0 .../jdex_file_outside_category/jdex/Not here | 0 .../jdex_file_outside_category/result.json | 29 - .../Directory Inside Id/.placeholder | 0 .../01.00 An ID/File Inside ID | 0 .../01 System Stuff/01.00 An ID.md | 0 .../01 System Stuff/02.00 Whoops.md | 0 .../jdex_id_in_wrong_category/result.json | 21 - .../01 System Stuff/01.02 A Naem/.placeholder | 0 .../Directory Inside Id/.placeholder | 0 .../01.03 Another ID/File Inside ID | 0 .../01 System Stuff/01.02 A Name.md | 0 .../01 System Stuff/01.03 Another ID.md | 0 tests/with_jdex/jdex_in_folders/result.json | 21 - .../01 System Stuff/01.02 A Name/.placeholder | 0 .../Directory Inside Id/.placeholder | 0 .../01.03 Another ID/File Inside ID | 0 .../01 System Stuff/01.02 A Name.md | 0 .../01 System Stuff/01.03 Another ID.md | 0 .../jdex/10-18 Malformed Numbers/.placeholder | 0 .../jdex/No Numbers/.placeholder | 0 .../jdex_invalid_area_name/result.json | 27 - .../01 System Stuff/01.02 A Name/.placeholder | 0 .../Directory Inside Id/.placeholder | 0 .../01.03 Another ID/File Inside ID | 0 .../01 System Stuff/01.02 A Name.md | 0 .../01 System Stuff/01.03 Another ID.md | 0 .../2 Malformed Numbers/.placeholder | 0 .../jdex/00-09 System/No Numbers/.placeholder | 0 .../jdex_invalid_category_name/result.json | 31 - .../01 System Stuff/01.02 A Name/.placeholder | 0 .../Directory Inside Id/.placeholder | 0 .../01.03 Another ID/File Inside ID | 0 .../01 System Stuff/01.04 An ID/Other File | 0 .../jdex/00.00 System.md | 0 .../jdex/01.00 System Stuff.md | 0 .../jdex/01.02 A Name.md | 0 .../jdex/01.03 Another ID.md | 0 .../jdex/01.04 An ID.md | 0 .../jdex/01.5 Malformed Numbers.md | 0 .../jdex/No Numbers.md | 0 .../jdex_invalid_id_name_flat/result.json | 27 - .../01 System Stuff/01.02 A Name/.placeholder | 0 .../Directory Inside Id/.placeholder | 0 .../01.03 Another ID/File Inside ID | 0 .../01 System Stuff/01.02 A Name.md | 0 .../01 System Stuff/01.03 Another ID.md | 0 .../01 System Stuff/01.4 Malformed Numbers.md | 0 .../00-09 System/01 System Stuff/No ID.md | 0 .../jdex_invalid_id_name_nested/result.json | 33 - .../01 System Stuff/01.02 A Naem/.placeholder | 0 .../Directory Inside Id/.placeholder | 0 .../01.03 Another ID/File Inside ID | 0 tests/with_jdex/single_file_jdex/jdex | 12 - tests/with_jdex/single_file_jdex/result.json | 21 - .../Directory Inside Id/.placeholder | 0 .../01.00 An ID/File Inside ID | 0 .../11 Whoops/11.01 Inbox/.placeholder | 0 .../category_in_wrong_area/result.json | 20 - .../Directory Inside Id/.placeholder | 0 .../02.00 An ID/File Inside ID | 0 .../Directory Inside Id/.placeholder | 0 .../01 A Category/01.00 An ID/File Inside ID | 0 tests/without_jdex/duplicate_area/result.json | 21 - .../Directory Inside Id/.placeholder | 0 .../01 A Category/01.00 An ID/File Inside ID | 0 .../Directory Inside Id/.placeholder | 0 .../01.02 Another ID/File Inside ID | 0 .../duplicate_category/result.json | 25 - .../01 System Stuff/01.00 A Reuse/Other File | 0 .../Directory Inside Id/.placeholder | 0 .../01.00 An ID/File Inside ID | 0 tests/without_jdex/duplicate_id/result.json | 27 - .../Directory Inside Id/.placeholder | 0 .../01.00 An ID/File Inside ID | 0 .../01 System Stuff/File Outside Id | 0 .../files/00-09 System/File Outside Category | 0 .../file_outside_id/files/File Outside Area | 0 .../without_jdex/file_outside_id/result.json | 43 - .../Directory Inside Id/.placeholder | 0 .../01.00 An ID/File Inside ID | 0 .../01 System Stuff/11.01 Whoops/.placeholder | 0 .../id_in_wrong_category/result.json | 21 - .../without_jdex/ignore_files/files/.ignoreme | 0 .../00-09 System/01 System Stuff/.ignoreme2 | 0 .../Directory Inside Id/.placeholder | 0 .../01.00 An ID/File Inside ID | 0 .../00-09 System/01 System Stuff/ignoreme | 0 .../ignore_files/files/00-09 System/ignoreme | 0 tests/without_jdex/ignore_files/ignore | 2 - tests/without_jdex/ignore_files/result.json | 4 - .../Directory Inside Id/.placeholder | 0 .../01.00 An ID/File Inside ID | 0 .../files/10-18 Malformed/.placeholder | 0 .../files/No ID/.placeholder | 0 .../invalid_area_name/result.json | 27 - .../Directory Inside Id/.placeholder | 0 .../01.00 An ID/File Inside ID | 0 .../00-09 System/2 Malformed/.placeholder | 0 .../files/00-09 System/No ID/.placeholder | 0 .../invalid_category_name/result.json | 31 - .../Directory Inside Id/.placeholder | 0 .../01.00 An ID/File Inside ID | 0 .../01.1 Malformed/.placeholder | 0 .../01 System Stuff/No ID/.placeholder | 0 .../without_jdex/invalid_id_name/result.json | 33 - .../Directory Inside Id/.placeholder | 0 .../01.00 An ID/File Inside ID | 0 .../01 System Stuff/01.01 Inbox/Not This | 0 .../01.01 Inbox/Or This/.placeholder | 0 tests/without_jdex/nonempty_inbox/result.json | 20 - 313 files changed, 842 insertions(+), 1543 deletions(-) rename tests/{with_jdex/area_different_from_jdex_flat/files/00-09 Systme/01 System Stuff/01.02 A Name/.placeholder => JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/1A/1A.md} (100%) rename tests/{with_jdex/area_different_from_jdex_flat/files/00-09 Systme/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder => JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/1A/File} (100%) rename tests/{with_jdex/area_different_from_jdex_nested/files/00-09 Systme/01 System Stuff/01.02 A Name => JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/1A/Folder}/.placeholder (100%) rename tests/{with_jdex/area_different_from_jdex_flat/files/00-09 Systme/01 System Stuff/01.03 Another ID/File Inside ID => JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/A1/File} (100%) rename tests/{with_jdex/area_different_from_jdex_nested/files/00-09 Systme/01 System Stuff/01.03 Another ID/Directory Inside Id => JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/A1/Folder}/.placeholder (100%) rename tests/{with_jdex/area_different_from_jdex_flat/jdex/00.00 System.md => JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/A1/X.md} (100%) rename tests/{with_jdex/area_different_from_jdex_flat/jdex/01.00 System Stuff.md => JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/File} (100%) rename tests/{with_jdex/area_not_in_jdex/files/00-09 System/01 System Stuff/01.02 A Name => JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/Folder}/.placeholder (100%) create mode 100644 tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml create mode 100644 tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/result.json rename tests/{with_jdex/area_not_in_jdex/files/00-09 System/01 System Stuff/01.03 Another ID/Directory Inside Id => JDEX_FILE_WHERE_FOLDER_EXPECTED/files/.obsidian}/.placeholder (100%) rename tests/{with_jdex/area_different_from_jdex_flat/jdex/01.02 A Name.md => JDEX_FILE_WHERE_FOLDER_EXPECTED/files/1/1A/1A.md} (100%) rename tests/{with_jdex/area_different_from_jdex_flat/jdex/01.03 Another ID.md => JDEX_FILE_WHERE_FOLDER_EXPECTED/files/1/1B} (100%) rename tests/{with_jdex/area_different_from_jdex_nested/files/00-09 Systme/01 System Stuff/01.00 An ID/Other File => JDEX_FILE_WHERE_FOLDER_EXPECTED/files/3} (100%) create mode 100644 tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/jdlint.toml create mode 100644 tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/result.json delete mode 100644 tests/with_jdex/area_different_from_jdex_flat/result.json delete mode 100644 tests/with_jdex/area_different_from_jdex_nested/files/00-09 Systme/01 System Stuff/01.03 Another ID/File Inside ID delete mode 100644 tests/with_jdex/area_different_from_jdex_nested/jdex/00-09 System/01 System Stuff/01.00 An ID.md delete mode 100644 tests/with_jdex/area_different_from_jdex_nested/jdex/00-09 System/01 System Stuff/01.02 A Name.md delete mode 100644 tests/with_jdex/area_different_from_jdex_nested/jdex/00-09 System/01 System Stuff/01.03 Another ID.md delete mode 100644 tests/with_jdex/area_different_from_jdex_nested/result.json delete mode 100644 tests/with_jdex/area_not_in_jdex/files/00-09 System/01 System Stuff/01.03 Another ID/File Inside ID delete mode 100644 tests/with_jdex/area_not_in_jdex/files/10-19 Oops/11 Cat/11.12 ID/.placeholder delete mode 100644 tests/with_jdex/area_not_in_jdex/jdex/00.00 System.md delete mode 100644 tests/with_jdex/area_not_in_jdex/jdex/01.00 System Stuff.md delete mode 100644 tests/with_jdex/area_not_in_jdex/jdex/01.02 A Name.md delete mode 100644 tests/with_jdex/area_not_in_jdex/jdex/01.03 Another ID.md delete mode 100644 tests/with_jdex/area_not_in_jdex/jdex/11.00 Cat.md delete mode 100644 tests/with_jdex/area_not_in_jdex/jdex/11.12 ID.md delete mode 100644 tests/with_jdex/area_not_in_jdex/result.json delete mode 100644 tests/with_jdex/category_different_from_jdex_flat/files/00-09 System/01 System Stuf/01.02 A Name/.placeholder delete mode 100644 tests/with_jdex/category_different_from_jdex_flat/files/00-09 System/01 System Stuf/01.03 Another ID/Directory Inside Id/.placeholder delete mode 100644 tests/with_jdex/category_different_from_jdex_flat/files/00-09 System/01 System Stuf/01.03 Another ID/File Inside ID delete mode 100644 tests/with_jdex/category_different_from_jdex_flat/jdex/00.00 System.md delete mode 100644 tests/with_jdex/category_different_from_jdex_flat/jdex/01.00 System Stuff.md delete mode 100644 tests/with_jdex/category_different_from_jdex_flat/jdex/01.02 A Name.md delete mode 100644 tests/with_jdex/category_different_from_jdex_flat/jdex/01.03 Another ID.md delete mode 100644 tests/with_jdex/category_different_from_jdex_flat/result.json delete mode 100644 tests/with_jdex/category_different_from_jdex_nested/files/00-09 System/01 System Stuf/01.00 An ID/Other File delete mode 100644 tests/with_jdex/category_different_from_jdex_nested/files/00-09 System/01 System Stuf/01.02 A Name/.placeholder delete mode 100644 tests/with_jdex/category_different_from_jdex_nested/files/00-09 System/01 System Stuf/01.03 Another ID/Directory Inside Id/.placeholder delete mode 100644 tests/with_jdex/category_different_from_jdex_nested/files/00-09 System/01 System Stuf/01.03 Another ID/File Inside ID delete mode 100644 tests/with_jdex/category_different_from_jdex_nested/jdex/00-09 System/01 System Stuff/01.00 An ID.md delete mode 100644 tests/with_jdex/category_different_from_jdex_nested/jdex/00-09 System/01 System Stuff/01.02 A Name.md delete mode 100644 tests/with_jdex/category_different_from_jdex_nested/jdex/00-09 System/01 System Stuff/01.03 Another ID.md delete mode 100644 tests/with_jdex/category_different_from_jdex_nested/result.json delete mode 100644 tests/with_jdex/category_not_in_jdex/files/00-09 System/01 System Stuff/01.02 An ID/.placeholder delete mode 100644 tests/with_jdex/category_not_in_jdex/files/00-09 System/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder delete mode 100644 tests/with_jdex/category_not_in_jdex/files/00-09 System/01 System Stuff/01.03 Another ID/File Inside ID delete mode 100644 tests/with_jdex/category_not_in_jdex/jdex/00.00 System.md delete mode 100644 tests/with_jdex/category_not_in_jdex/jdex/01.02 An ID.md delete mode 100644 tests/with_jdex/category_not_in_jdex/jdex/01.03 Another ID.md delete mode 100644 tests/with_jdex/category_not_in_jdex/result.json delete mode 100644 tests/with_jdex/flat_alt_zeros/altzeros delete mode 100644 tests/with_jdex/flat_alt_zeros/files/00-09 System/01 Life Admin/01.03 Area Standard Zero/Directory Inside Id/.placeholder delete mode 100644 tests/with_jdex/flat_alt_zeros/files/00-09 System/01 Life Admin/01.03 Area Standard Zero/File Inside ID delete mode 100644 tests/with_jdex/flat_alt_zeros/files/10-19 Life Admin/10 Me, Myself, and I/10.02 An ID/.placeholder delete mode 100644 tests/with_jdex/flat_alt_zeros/jdex/00.00 System Area Management.md delete mode 100644 tests/with_jdex/flat_alt_zeros/jdex/01.00 Life Admin Area Management.md delete mode 100644 tests/with_jdex/flat_alt_zeros/jdex/01.03 Area Standard Zero.md delete mode 100644 tests/with_jdex/flat_alt_zeros/jdex/10.00 Me, Myself, and I.md delete mode 100644 tests/with_jdex/flat_alt_zeros/jdex/10.02 An ID.md delete mode 100644 tests/with_jdex/flat_alt_zeros/result.json delete mode 100644 tests/with_jdex/flat_alt_zeros_duplicate_header/altzeros delete mode 100644 tests/with_jdex/flat_alt_zeros_duplicate_header/files/00-09 System/01 Life Admin/01.03 Area Standard Zero/Directory Inside Id/.placeholder delete mode 100644 tests/with_jdex/flat_alt_zeros_duplicate_header/files/00-09 System/01 Life Admin/01.03 Area Standard Zero/File Inside ID delete mode 100644 tests/with_jdex/flat_alt_zeros_duplicate_header/files/10-19 Life Admin/10 Me, Myself, and I/10.02 An ID/.placeholder delete mode 100644 tests/with_jdex/flat_alt_zeros_duplicate_header/jdex/00.00 System Area Management.md delete mode 100644 tests/with_jdex/flat_alt_zeros_duplicate_header/jdex/01.00 Life Admin Area Management.md delete mode 100644 tests/with_jdex/flat_alt_zeros_duplicate_header/jdex/01.03 Area Standard Zero.md delete mode 100644 tests/with_jdex/flat_alt_zeros_duplicate_header/jdex/10. Life Admin.md delete mode 100644 tests/with_jdex/flat_alt_zeros_duplicate_header/jdex/10. Life Adminn.md delete mode 100644 tests/with_jdex/flat_alt_zeros_duplicate_header/jdex/10.00 Me, Myself, and I.md delete mode 100644 tests/with_jdex/flat_alt_zeros_duplicate_header/jdex/10.02 An ID.md delete mode 100644 tests/with_jdex/flat_alt_zeros_duplicate_header/result.json delete mode 100644 tests/with_jdex/flat_alt_zeros_with_headers/altzeros delete mode 100644 tests/with_jdex/flat_alt_zeros_with_headers/files/00-09 System/01 Life Admin/01.03 Area Standard Zero/Directory Inside Id/.placeholder delete mode 100644 tests/with_jdex/flat_alt_zeros_with_headers/files/00-09 System/01 Life Admin/01.03 Area Standard Zero/File Inside ID delete mode 100644 tests/with_jdex/flat_alt_zeros_with_headers/files/10-19 Life Admin/10 Me, Myself, and I/10.02 An ID/.placeholder delete mode 100644 tests/with_jdex/flat_alt_zeros_with_headers/jdex/00.00 System Area Management.md delete mode 100644 tests/with_jdex/flat_alt_zeros_with_headers/jdex/01.00 Life Admin Area Management.md delete mode 100644 tests/with_jdex/flat_alt_zeros_with_headers/jdex/01.03 Area Standard Zero.md delete mode 100644 tests/with_jdex/flat_alt_zeros_with_headers/jdex/10. Life Admin.md delete mode 100644 tests/with_jdex/flat_alt_zeros_with_headers/jdex/10.00 Me, Myself, and I.md delete mode 100644 tests/with_jdex/flat_alt_zeros_with_headers/jdex/10.02 An ID.md delete mode 100644 tests/with_jdex/flat_alt_zeros_with_headers/result.json delete mode 100644 tests/with_jdex/flat_with_index_suffix/files/00-09 System/01 System Stuff/01.02 A Name/.placeholder delete mode 100644 tests/with_jdex/flat_with_index_suffix/files/00-09 System/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder delete mode 100644 tests/with_jdex/flat_with_index_suffix/files/00-09 System/01 System Stuff/01.03 Another ID/File Inside ID delete mode 100644 tests/with_jdex/flat_with_index_suffix/jdex/00.00 System Index.md delete mode 100644 tests/with_jdex/flat_with_index_suffix/jdex/01.00 System Stuff Index.md delete mode 100644 tests/with_jdex/flat_with_index_suffix/jdex/01.02 A Name.md delete mode 100644 tests/with_jdex/flat_with_index_suffix/jdex/01.03 Another ID.md delete mode 100644 tests/with_jdex/flat_with_index_suffix/result.json delete mode 100644 tests/with_jdex/flat_with_management_suffixes/files/00-09 System/01 System Stuff/01.02 A Name/.placeholder delete mode 100644 tests/with_jdex/flat_with_management_suffixes/files/00-09 System/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder delete mode 100644 tests/with_jdex/flat_with_management_suffixes/files/00-09 System/01 System Stuff/01.03 Another ID/File Inside ID delete mode 100644 tests/with_jdex/flat_with_management_suffixes/jdex/00.00 System Area Management.md delete mode 100644 tests/with_jdex/flat_with_management_suffixes/jdex/01.00 System Stuff Category Management.md delete mode 100644 tests/with_jdex/flat_with_management_suffixes/jdex/01.02 A Name.md delete mode 100644 tests/with_jdex/flat_with_management_suffixes/jdex/01.03 Another ID.md delete mode 100644 tests/with_jdex/flat_with_management_suffixes/result.json delete mode 100644 tests/with_jdex/id_different_from_jdex/files/00-09 System/01 System Stuff/01.02 A Naem/.placeholder delete mode 100644 tests/with_jdex/id_different_from_jdex/files/00-09 System/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder delete mode 100644 tests/with_jdex/id_different_from_jdex/files/00-09 System/01 System Stuff/01.03 Another ID/File Inside ID delete mode 100644 tests/with_jdex/id_different_from_jdex/files/00-09 System/01 System Stuff/01.04 An ID/Other File delete mode 100644 tests/with_jdex/id_different_from_jdex/jdex/00.00 System.md delete mode 100644 tests/with_jdex/id_different_from_jdex/jdex/01.00 System Stuff.md delete mode 100644 tests/with_jdex/id_different_from_jdex/jdex/01.02 A Name.md delete mode 100644 tests/with_jdex/id_different_from_jdex/jdex/01.03 Another ID.md delete mode 100644 tests/with_jdex/id_different_from_jdex/jdex/01.04 An ID.md delete mode 100644 tests/with_jdex/id_different_from_jdex/result.json delete mode 100644 tests/with_jdex/id_not_in_jdex/files/00-09 System/01 System Stuff/01.02 Missing ID/.placeholder delete mode 100644 tests/with_jdex/id_not_in_jdex/files/00-09 System/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder delete mode 100644 tests/with_jdex/id_not_in_jdex/files/00-09 System/01 System Stuff/01.03 Another ID/File Inside ID delete mode 100644 tests/with_jdex/id_not_in_jdex/jdex/00.00 System.md delete mode 100644 tests/with_jdex/id_not_in_jdex/jdex/01.00 System Stuff.md delete mode 100644 tests/with_jdex/id_not_in_jdex/jdex/01.03 Another ID.md delete mode 100644 tests/with_jdex/id_not_in_jdex/result.json delete mode 100644 tests/with_jdex/jdex_area_header_different_from_area/altzeros delete mode 100644 tests/with_jdex/jdex_area_header_different_from_area/files/00-09 System/01 Life Admin/01.03 Area Standard Zero/Directory Inside Id/.placeholder delete mode 100644 tests/with_jdex/jdex_area_header_different_from_area/files/00-09 System/01 Life Admin/01.03 Area Standard Zero/File Inside ID delete mode 100644 tests/with_jdex/jdex_area_header_different_from_area/files/10-19 Life Admin/10 Me, Myself, and I/10.02 An ID/.placeholder delete mode 100644 tests/with_jdex/jdex_area_header_different_from_area/jdex/00.00 System Area Management.md delete mode 100644 tests/with_jdex/jdex_area_header_different_from_area/jdex/01.00 Life Admin Area Management.md delete mode 100644 tests/with_jdex/jdex_area_header_different_from_area/jdex/01.03 Area Standard Zero.md delete mode 100644 tests/with_jdex/jdex_area_header_different_from_area/jdex/10. Life Adminn.md delete mode 100644 tests/with_jdex/jdex_area_header_different_from_area/jdex/10.00 Me, Myself, and I.md delete mode 100644 tests/with_jdex/jdex_area_header_different_from_area/jdex/10.02 An ID.md delete mode 100644 tests/with_jdex/jdex_area_header_different_from_area/result.json delete mode 100644 tests/with_jdex/jdex_area_header_without_area/altzeros delete mode 100644 tests/with_jdex/jdex_area_header_without_area/files/00-09 System/01 Life Admin/01.03 Area Standard Zero/Directory Inside Id/.placeholder delete mode 100644 tests/with_jdex/jdex_area_header_without_area/files/00-09 System/01 Life Admin/01.03 Area Standard Zero/File Inside ID delete mode 100644 tests/with_jdex/jdex_area_header_without_area/files/10-19 Life Admin/10 Me, Myself, and I/10.02 An ID/.placeholder delete mode 100644 tests/with_jdex/jdex_area_header_without_area/jdex/00.00 System Area Management.md delete mode 100644 tests/with_jdex/jdex_area_header_without_area/jdex/01.00 Life Admin Area Management.md delete mode 100644 tests/with_jdex/jdex_area_header_without_area/jdex/01.03 Area Standard Zero.md delete mode 100644 tests/with_jdex/jdex_area_header_without_area/jdex/10. Life Admin.md delete mode 100644 tests/with_jdex/jdex_area_header_without_area/jdex/10.00 Me, Myself, and I.md delete mode 100644 tests/with_jdex/jdex_area_header_without_area/jdex/10.02 An ID.md delete mode 100644 tests/with_jdex/jdex_area_header_without_area/jdex/20. Digital Stuff.md delete mode 100644 tests/with_jdex/jdex_area_header_without_area/result.json delete mode 100644 tests/with_jdex/jdex_category_in_wrong_area/files/00-09 System/01 System Stuff/01.00 An ID/Directory Inside Id/.placeholder delete mode 100644 tests/with_jdex/jdex_category_in_wrong_area/files/00-09 System/01 System Stuff/01.00 An ID/File Inside ID delete mode 100644 tests/with_jdex/jdex_category_in_wrong_area/jdex/00-09 System/01 System Stuff/01.00 An ID.md delete mode 100644 tests/with_jdex/jdex_category_in_wrong_area/jdex/00-09 System/11 Whoops/11.01 Inbox.md delete mode 100644 tests/with_jdex/jdex_category_in_wrong_area/result.json delete mode 100644 tests/with_jdex/jdex_duplicate_area_flat/files/00-09 An Area/01 A Category/01.00 An ID/Directory Inside Id/.placeholder delete mode 100644 tests/with_jdex/jdex_duplicate_area_flat/files/00-09 An Area/01 A Category/01.00 An ID/File Inside ID delete mode 100644 tests/with_jdex/jdex_duplicate_area_flat/jdex/00.00 A Reuse.md delete mode 100644 tests/with_jdex/jdex_duplicate_area_flat/jdex/00.00 An Area.md delete mode 100644 tests/with_jdex/jdex_duplicate_area_flat/jdex/01.00 A Category.md delete mode 100644 tests/with_jdex/jdex_duplicate_area_flat/jdex/01.01 An ID.md delete mode 100644 tests/with_jdex/jdex_duplicate_area_flat/jdex/02.00 An Id.md delete mode 100644 tests/with_jdex/jdex_duplicate_area_flat/result.json delete mode 100644 tests/with_jdex/jdex_duplicate_area_header/altzeros delete mode 100644 tests/with_jdex/jdex_duplicate_area_header/files/10-19 An Area/10 A Category/10.02 An Id/Directory Inside Id/.placeholder delete mode 100644 tests/with_jdex/jdex_duplicate_area_header/files/10-19 An Area/10 A Category/10.02 An Id/File Inside ID delete mode 100644 tests/with_jdex/jdex_duplicate_area_header/jdex/01.00 An Area.md delete mode 100644 tests/with_jdex/jdex_duplicate_area_header/jdex/10. A Reuse.md delete mode 100644 tests/with_jdex/jdex_duplicate_area_header/jdex/10. An Area.md delete mode 100644 tests/with_jdex/jdex_duplicate_area_header/jdex/10.00 A Category.md delete mode 100644 tests/with_jdex/jdex_duplicate_area_header/jdex/10.02 An Id.md delete mode 100644 tests/with_jdex/jdex_duplicate_area_header/result.json delete mode 100644 tests/with_jdex/jdex_duplicate_area_nested/files/00-09 An Area/01 A Category/01.00 An ID/Directory Inside Id/.placeholder delete mode 100644 tests/with_jdex/jdex_duplicate_area_nested/files/00-09 An Area/01 A Category/01.00 An ID/File Inside ID delete mode 100644 tests/with_jdex/jdex_duplicate_area_nested/jdex/00-09 A Reuse/02 Another Category/02.00 An Id delete mode 100644 tests/with_jdex/jdex_duplicate_area_nested/jdex/00-09 An Area/01 A Category/01.00 An ID delete mode 100644 tests/with_jdex/jdex_duplicate_area_nested/result.json delete mode 100644 tests/with_jdex/jdex_duplicate_category_flat/files/00-09 An Area/01 A Category/01.00 An ID/Directory Inside Id/.placeholder delete mode 100644 tests/with_jdex/jdex_duplicate_category_flat/files/00-09 An Area/01 A Category/01.00 An ID/File Inside ID delete mode 100644 tests/with_jdex/jdex_duplicate_category_flat/jdex/00.00 An Area.md delete mode 100644 tests/with_jdex/jdex_duplicate_category_flat/jdex/01.00 A Category.md delete mode 100644 tests/with_jdex/jdex_duplicate_category_flat/jdex/01.00 A Reuse.md delete mode 100644 tests/with_jdex/jdex_duplicate_category_flat/jdex/01.01 An ID.md delete mode 100644 tests/with_jdex/jdex_duplicate_category_flat/jdex/02.00 An Id.md delete mode 100644 tests/with_jdex/jdex_duplicate_category_flat/result.json delete mode 100644 tests/with_jdex/jdex_duplicate_category_nested/files/00-09 An Area/01 A Category/01.00 An ID/Directory Inside Id/.placeholder delete mode 100644 tests/with_jdex/jdex_duplicate_category_nested/files/00-09 An Area/01 A Category/01.00 An ID/File Inside ID delete mode 100644 tests/with_jdex/jdex_duplicate_category_nested/jdex/00-09 An Area/01 A Category/01.00 An ID delete mode 100644 tests/with_jdex/jdex_duplicate_category_nested/jdex/00-09 An Area/01 A Reuse/01.02 Another ID delete mode 100644 tests/with_jdex/jdex_duplicate_category_nested/result.json delete mode 100644 tests/with_jdex/jdex_duplicate_id_flat/files/00-09 An Area/01 A Category/01.00 An ID/Directory Inside Id/.placeholder delete mode 100644 tests/with_jdex/jdex_duplicate_id_flat/files/00-09 An Area/01 A Category/01.00 An ID/File Inside ID delete mode 100644 tests/with_jdex/jdex_duplicate_id_flat/jdex/00.00 An Area.md delete mode 100644 tests/with_jdex/jdex_duplicate_id_flat/jdex/01.00 A Category.md delete mode 100644 tests/with_jdex/jdex_duplicate_id_flat/jdex/01.01 A Reuse.md delete mode 100644 tests/with_jdex/jdex_duplicate_id_flat/jdex/01.01 An ID.md delete mode 100644 tests/with_jdex/jdex_duplicate_id_flat/result.json delete mode 100644 tests/with_jdex/jdex_duplicate_id_nested/files/00-09 An Area/01 A Category/01.00 An ID/Directory Inside Id/.placeholder delete mode 100644 tests/with_jdex/jdex_duplicate_id_nested/files/00-09 An Area/01 A Category/01.00 An ID/File Inside ID delete mode 100644 tests/with_jdex/jdex_duplicate_id_nested/jdex/00-09 An Area/01 A Category/01.01 A Reuse.md delete mode 100644 tests/with_jdex/jdex_duplicate_id_nested/jdex/00-09 An Area/01 A Category/01.01 An ID.md delete mode 100644 tests/with_jdex/jdex_duplicate_id_nested/result.json delete mode 100644 tests/with_jdex/jdex_file_outside_category/files/00-09 System/01 System Stuff/01.02 A Name/.placeholder delete mode 100644 tests/with_jdex/jdex_file_outside_category/files/00-09 System/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder delete mode 100644 tests/with_jdex/jdex_file_outside_category/files/00-09 System/01 System Stuff/01.03 Another ID/File Inside ID delete mode 100644 tests/with_jdex/jdex_file_outside_category/jdex/00-09 System/01 System Stuff/01.02 A Name.md delete mode 100644 tests/with_jdex/jdex_file_outside_category/jdex/00-09 System/01 System Stuff/01.03 Another ID.md delete mode 100644 tests/with_jdex/jdex_file_outside_category/jdex/00-09 System/Nor here delete mode 100644 tests/with_jdex/jdex_file_outside_category/jdex/Not here delete mode 100644 tests/with_jdex/jdex_file_outside_category/result.json delete mode 100644 tests/with_jdex/jdex_id_in_wrong_category/files/00-09 System/01 System Stuff/01.00 An ID/Directory Inside Id/.placeholder delete mode 100644 tests/with_jdex/jdex_id_in_wrong_category/files/00-09 System/01 System Stuff/01.00 An ID/File Inside ID delete mode 100644 tests/with_jdex/jdex_id_in_wrong_category/jdex/00-09 System/01 System Stuff/01.00 An ID.md delete mode 100644 tests/with_jdex/jdex_id_in_wrong_category/jdex/00-09 System/01 System Stuff/02.00 Whoops.md delete mode 100644 tests/with_jdex/jdex_id_in_wrong_category/result.json delete mode 100644 tests/with_jdex/jdex_in_folders/files/00-09 System/01 System Stuff/01.02 A Naem/.placeholder delete mode 100644 tests/with_jdex/jdex_in_folders/files/00-09 System/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder delete mode 100644 tests/with_jdex/jdex_in_folders/files/00-09 System/01 System Stuff/01.03 Another ID/File Inside ID delete mode 100644 tests/with_jdex/jdex_in_folders/jdex/00-09 System/01 System Stuff/01.02 A Name.md delete mode 100644 tests/with_jdex/jdex_in_folders/jdex/00-09 System/01 System Stuff/01.03 Another ID.md delete mode 100644 tests/with_jdex/jdex_in_folders/result.json delete mode 100644 tests/with_jdex/jdex_invalid_area_name/files/00-09 System/01 System Stuff/01.02 A Name/.placeholder delete mode 100644 tests/with_jdex/jdex_invalid_area_name/files/00-09 System/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder delete mode 100644 tests/with_jdex/jdex_invalid_area_name/files/00-09 System/01 System Stuff/01.03 Another ID/File Inside ID delete mode 100644 tests/with_jdex/jdex_invalid_area_name/jdex/00-09 System/01 System Stuff/01.02 A Name.md delete mode 100644 tests/with_jdex/jdex_invalid_area_name/jdex/00-09 System/01 System Stuff/01.03 Another ID.md delete mode 100644 tests/with_jdex/jdex_invalid_area_name/jdex/10-18 Malformed Numbers/.placeholder delete mode 100644 tests/with_jdex/jdex_invalid_area_name/jdex/No Numbers/.placeholder delete mode 100644 tests/with_jdex/jdex_invalid_area_name/result.json delete mode 100644 tests/with_jdex/jdex_invalid_category_name/files/00-09 System/01 System Stuff/01.02 A Name/.placeholder delete mode 100644 tests/with_jdex/jdex_invalid_category_name/files/00-09 System/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder delete mode 100644 tests/with_jdex/jdex_invalid_category_name/files/00-09 System/01 System Stuff/01.03 Another ID/File Inside ID delete mode 100644 tests/with_jdex/jdex_invalid_category_name/jdex/00-09 System/01 System Stuff/01.02 A Name.md delete mode 100644 tests/with_jdex/jdex_invalid_category_name/jdex/00-09 System/01 System Stuff/01.03 Another ID.md delete mode 100644 tests/with_jdex/jdex_invalid_category_name/jdex/00-09 System/2 Malformed Numbers/.placeholder delete mode 100644 tests/with_jdex/jdex_invalid_category_name/jdex/00-09 System/No Numbers/.placeholder delete mode 100644 tests/with_jdex/jdex_invalid_category_name/result.json delete mode 100644 tests/with_jdex/jdex_invalid_id_name_flat/files/00-09 System/01 System Stuff/01.02 A Name/.placeholder delete mode 100644 tests/with_jdex/jdex_invalid_id_name_flat/files/00-09 System/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder delete mode 100644 tests/with_jdex/jdex_invalid_id_name_flat/files/00-09 System/01 System Stuff/01.03 Another ID/File Inside ID delete mode 100644 tests/with_jdex/jdex_invalid_id_name_flat/files/00-09 System/01 System Stuff/01.04 An ID/Other File delete mode 100644 tests/with_jdex/jdex_invalid_id_name_flat/jdex/00.00 System.md delete mode 100644 tests/with_jdex/jdex_invalid_id_name_flat/jdex/01.00 System Stuff.md delete mode 100644 tests/with_jdex/jdex_invalid_id_name_flat/jdex/01.02 A Name.md delete mode 100644 tests/with_jdex/jdex_invalid_id_name_flat/jdex/01.03 Another ID.md delete mode 100644 tests/with_jdex/jdex_invalid_id_name_flat/jdex/01.04 An ID.md delete mode 100644 tests/with_jdex/jdex_invalid_id_name_flat/jdex/01.5 Malformed Numbers.md delete mode 100644 tests/with_jdex/jdex_invalid_id_name_flat/jdex/No Numbers.md delete mode 100644 tests/with_jdex/jdex_invalid_id_name_flat/result.json delete mode 100644 tests/with_jdex/jdex_invalid_id_name_nested/files/00-09 System/01 System Stuff/01.02 A Name/.placeholder delete mode 100644 tests/with_jdex/jdex_invalid_id_name_nested/files/00-09 System/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder delete mode 100644 tests/with_jdex/jdex_invalid_id_name_nested/files/00-09 System/01 System Stuff/01.03 Another ID/File Inside ID delete mode 100644 tests/with_jdex/jdex_invalid_id_name_nested/jdex/00-09 System/01 System Stuff/01.02 A Name.md delete mode 100644 tests/with_jdex/jdex_invalid_id_name_nested/jdex/00-09 System/01 System Stuff/01.03 Another ID.md delete mode 100644 tests/with_jdex/jdex_invalid_id_name_nested/jdex/00-09 System/01 System Stuff/01.4 Malformed Numbers.md delete mode 100644 tests/with_jdex/jdex_invalid_id_name_nested/jdex/00-09 System/01 System Stuff/No ID.md delete mode 100644 tests/with_jdex/jdex_invalid_id_name_nested/result.json delete mode 100644 tests/with_jdex/single_file_jdex/files/00-09 System/01 System Stuff/01.02 A Naem/.placeholder delete mode 100644 tests/with_jdex/single_file_jdex/files/00-09 System/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder delete mode 100644 tests/with_jdex/single_file_jdex/files/00-09 System/01 System Stuff/01.03 Another ID/File Inside ID delete mode 100644 tests/with_jdex/single_file_jdex/jdex delete mode 100644 tests/with_jdex/single_file_jdex/result.json delete mode 100644 tests/without_jdex/category_in_wrong_area/files/00-09 System/01 System Stuff/01.00 An ID/Directory Inside Id/.placeholder delete mode 100644 tests/without_jdex/category_in_wrong_area/files/00-09 System/01 System Stuff/01.00 An ID/File Inside ID delete mode 100644 tests/without_jdex/category_in_wrong_area/files/00-09 System/11 Whoops/11.01 Inbox/.placeholder delete mode 100644 tests/without_jdex/category_in_wrong_area/result.json delete mode 100644 tests/without_jdex/duplicate_area/files/00-09 A Reuse/02 Another Category/02.00 An ID/Directory Inside Id/.placeholder delete mode 100644 tests/without_jdex/duplicate_area/files/00-09 A Reuse/02 Another Category/02.00 An ID/File Inside ID delete mode 100644 tests/without_jdex/duplicate_area/files/00-09 An Area/01 A Category/01.00 An ID/Directory Inside Id/.placeholder delete mode 100644 tests/without_jdex/duplicate_area/files/00-09 An Area/01 A Category/01.00 An ID/File Inside ID delete mode 100644 tests/without_jdex/duplicate_area/result.json delete mode 100644 tests/without_jdex/duplicate_category/files/00-09 System/01 A Category/01.00 An ID/Directory Inside Id/.placeholder delete mode 100644 tests/without_jdex/duplicate_category/files/00-09 System/01 A Category/01.00 An ID/File Inside ID delete mode 100644 tests/without_jdex/duplicate_category/files/00-09 System/01 A Reuse/01.02 Another ID/Directory Inside Id/.placeholder delete mode 100644 tests/without_jdex/duplicate_category/files/00-09 System/01 A Reuse/01.02 Another ID/File Inside ID delete mode 100644 tests/without_jdex/duplicate_category/result.json delete mode 100644 tests/without_jdex/duplicate_id/files/00-09 System/01 System Stuff/01.00 A Reuse/Other File delete mode 100644 tests/without_jdex/duplicate_id/files/00-09 System/01 System Stuff/01.00 An ID/Directory Inside Id/.placeholder delete mode 100644 tests/without_jdex/duplicate_id/files/00-09 System/01 System Stuff/01.00 An ID/File Inside ID delete mode 100644 tests/without_jdex/duplicate_id/result.json delete mode 100644 tests/without_jdex/file_outside_id/files/00-09 System/01 System Stuff/01.00 An ID/Directory Inside Id/.placeholder delete mode 100644 tests/without_jdex/file_outside_id/files/00-09 System/01 System Stuff/01.00 An ID/File Inside ID delete mode 100644 tests/without_jdex/file_outside_id/files/00-09 System/01 System Stuff/File Outside Id delete mode 100644 tests/without_jdex/file_outside_id/files/00-09 System/File Outside Category delete mode 100644 tests/without_jdex/file_outside_id/files/File Outside Area delete mode 100644 tests/without_jdex/file_outside_id/result.json delete mode 100644 tests/without_jdex/id_in_wrong_category/files/00-09 System/01 System Stuff/01.00 An ID/Directory Inside Id/.placeholder delete mode 100644 tests/without_jdex/id_in_wrong_category/files/00-09 System/01 System Stuff/01.00 An ID/File Inside ID delete mode 100644 tests/without_jdex/id_in_wrong_category/files/00-09 System/01 System Stuff/11.01 Whoops/.placeholder delete mode 100644 tests/without_jdex/id_in_wrong_category/result.json delete mode 100644 tests/without_jdex/ignore_files/files/.ignoreme delete mode 100644 tests/without_jdex/ignore_files/files/00-09 System/01 System Stuff/.ignoreme2 delete mode 100644 tests/without_jdex/ignore_files/files/00-09 System/01 System Stuff/01.00 An ID/Directory Inside Id/.placeholder delete mode 100644 tests/without_jdex/ignore_files/files/00-09 System/01 System Stuff/01.00 An ID/File Inside ID delete mode 100644 tests/without_jdex/ignore_files/files/00-09 System/01 System Stuff/ignoreme delete mode 100644 tests/without_jdex/ignore_files/files/00-09 System/ignoreme delete mode 100644 tests/without_jdex/ignore_files/ignore delete mode 100644 tests/without_jdex/ignore_files/result.json delete mode 100644 tests/without_jdex/invalid_area_name/files/00-09 System/01 System Stuff/01.00 An ID/Directory Inside Id/.placeholder delete mode 100644 tests/without_jdex/invalid_area_name/files/00-09 System/01 System Stuff/01.00 An ID/File Inside ID delete mode 100644 tests/without_jdex/invalid_area_name/files/10-18 Malformed/.placeholder delete mode 100644 tests/without_jdex/invalid_area_name/files/No ID/.placeholder delete mode 100644 tests/without_jdex/invalid_area_name/result.json delete mode 100644 tests/without_jdex/invalid_category_name/files/00-09 System/01 System Stuff/01.00 An ID/Directory Inside Id/.placeholder delete mode 100644 tests/without_jdex/invalid_category_name/files/00-09 System/01 System Stuff/01.00 An ID/File Inside ID delete mode 100644 tests/without_jdex/invalid_category_name/files/00-09 System/2 Malformed/.placeholder delete mode 100644 tests/without_jdex/invalid_category_name/files/00-09 System/No ID/.placeholder delete mode 100644 tests/without_jdex/invalid_category_name/result.json delete mode 100644 tests/without_jdex/invalid_id_name/files/00-09 System/01 System Stuff/01.00 An ID/Directory Inside Id/.placeholder delete mode 100644 tests/without_jdex/invalid_id_name/files/00-09 System/01 System Stuff/01.00 An ID/File Inside ID delete mode 100644 tests/without_jdex/invalid_id_name/files/00-09 System/01 System Stuff/01.1 Malformed/.placeholder delete mode 100644 tests/without_jdex/invalid_id_name/files/00-09 System/01 System Stuff/No ID/.placeholder delete mode 100644 tests/without_jdex/invalid_id_name/result.json delete mode 100644 tests/without_jdex/nonempty_inbox/files/00-09 System/01 System Stuff/01.00 An ID/Directory Inside Id/.placeholder delete mode 100644 tests/without_jdex/nonempty_inbox/files/00-09 System/01 System Stuff/01.00 An ID/File Inside ID delete mode 100644 tests/without_jdex/nonempty_inbox/files/00-09 System/01 System Stuff/01.01 Inbox/Not This delete mode 100644 tests/without_jdex/nonempty_inbox/files/00-09 System/01 System Stuff/01.01 Inbox/Or This/.placeholder delete mode 100644 tests/without_jdex/nonempty_inbox/result.json diff --git a/jdlint.py b/jdlint.py index b9bd692..621605c 100755 --- a/jdlint.py +++ b/jdlint.py @@ -10,6 +10,7 @@ import os import re import typing +import sys from dataclasses import dataclass from pathlib import Path, PurePath from typing import TYPE_CHECKING, Any, Literal, TypeVar @@ -19,6 +20,11 @@ import tomllib +############################################################################### +# Exceptions +############################################################################### + + class ConfigError(Exception): """An error in the jdlint config.""" @@ -62,6 +68,11 @@ def __init__(self, key, issue): super().__init__(key, f"Conflict in config. Issue: {issue}") +############################################################################### +# Config +############################################################################### + + class ConfigSystemRoot: """A root (base folder) of a JD system to check for correctness, e.g. ~/Documents.""" @@ -69,7 +80,7 @@ def __init__(self, at: str, from_file: dict) -> None: """Create a valid configuration given a loaded section of a config file.""" # Acquire and set defaults self.name = from_file.pop("name") - self.path = Path(from_file.pop("path")) + self.path = Path(from_file.pop("path")).expanduser() self.ignore = from_file.pop("ignore", []) if not isinstance(self.name, str): @@ -81,7 +92,7 @@ def __init__(self, at: str, from_file: dict) -> None: # Validate path is good if not self.path.is_dir(): - ConfigValueError( + raise ConfigValueError( f"{at}.path", "Root path isn't a folder that exists!", self.path, @@ -107,15 +118,25 @@ class ConfigSystemJDex: def __init__(self, at: str, from_file: dict) -> None: """Create a valid configuration given a loaded section of a config file.""" # Acquire and set defaults - self.path = Path(from_file.pop("path")) + self.path = Path(from_file.pop("path")).expanduser() + self.ignore = from_file.pop("ignore", []) - # Validate path is good + # Validate if not self.path.is_dir(): - ConfigValueError( + raise ConfigValueError( f"{at}.path", "JDex path isn't a folder that exists!", self.path, ) + if not isinstance(self.ignore, list): + raise ConfigTypeError( + f"{at}.ignore", + "list", + type(self.ignore).__name__, + ) + for r in self.ignore: + if not isinstance(r, str): + raise ConfigTypeError(f"{at}.ignore", "str", type(r).__name__) self.children = [ ConfigJDexTier( @@ -394,13 +415,13 @@ def __init__(self, at: str, parent_segments: list[str], from_file: str) -> None: if from_file.count("/") % 2 != 0: raise ConfigValueError( at, - "Malfored format; there must be an even number of / characters. You have an extra one/are missing one.", + "Malformed format; there must be an even number of / characters. You have an extra one/are missing one.", from_file, ) if from_file == "": raise ConfigValueError( at, - "Malfored format; must not be empty.", + "Malformed format; must not be empty.", from_file, ) regex = [] @@ -415,23 +436,36 @@ def __init__(self, at: str, parent_segments: list[str], from_file: str) -> None: if not match: raise ConfigValueError( at, - "Malfored format; variable segment must consist of =, *, or one or more # followed by an alphabetic identifier.", + "Malformed format; variable segment must consist of =, *, or one or more # followed by an alphabetic identifier.", v, ) if match.group(1) == "=": - if match.group(2) not in parent_segments: + if match.group(2) in parent_segments: + p = match.group(2) + regex.append(lambda d, p=p: re.escape(d[p])) + elif match.group(2) in new_segments: + identifier = match.group(2) + regex.append( + lambda _, identifier=identifier: f"(?P={identifier})", + ) + + else: raise ConfigValueError( at, - "Malfored format; variable segment referenced an identifier not bound in a parent.", + "Malformed format; variable segment referenced an identifier not bound in a parent.", v, ) - p = match.group(2) - regex.append(lambda d, p=p: re.escape(d[p])) else: if match.group(2) in parent_segments: raise ConfigValueError( at, - "Malfored format; variable segment tried to rebind an identifier already bound in a parent.", + "Malformed format; variable segment tried to rebind an identifier already bound in a parent.", + v, + ) + if match.group(2) in new_segments: + raise ConfigValueError( + at, + "Malformed format; variable segment tried to rebind an identifier already bound.", v, ) identifier = match.group(2) @@ -450,6 +484,7 @@ def __init__(self, at: str, parent_segments: list[str], from_file: str) -> None: self.known_segments = parent_segments + new_segments self.build_regex = lambda d: "".join([f(d) for f in regex]) + self.raw_format = from_file class Config: @@ -458,6 +493,27 @@ def __init__(self, from_file): self.system = ConfigSystem(from_file["system"]) +############################################################################### +# Issues +############################################################################### + + +@dataclass(frozen=True) +class JDexIssue: + """A single error detected in the JDex.""" + + file: PurePath + type = None + + def display(self) -> str: + """Display this particular instance of an error.""" + raise NotImplementedError + + def explain(self) -> _Explanation: + """Explain what this error is.""" + raise NotImplementedError + + @dataclass(frozen=True) class AreaDifferentFromJDex: """An area with a differently-named JDex entry.""" @@ -933,20 +989,42 @@ def explain(self) -> _Explanation: @dataclass(frozen=True) -class JDexFileOutsideCategory: - """A JDex file was encountered not in a terminal category folder.""" +class JDexIssueFileWhereFolderExpected(JDexIssue): + """A JDex file that matched an expected folder was found.""" - type: Literal["JDEX_FILE_OUTSIDE_CATEGORY"] = "JDEX_FILE_OUTSIDE_CATEGORY" + matched_format: str + type: Literal["JDEX_FILE_WHERE_FOLDER_EXPECTED"] = "JDEX_FILE_WHERE_FOLDER_EXPECTED" - def display(self, files: list[File]) -> str: + def display(self) -> str: """Display this particular instance of an error.""" - return _print_nest(files[0]) + return f'{self.file!s} (matched "{self.matched_format}")' + + def explain(self) -> _Explanation: + """Explain what this error is.""" + return _Explanation( + explanation="A JDex file was found that matched the format of an expected child folder.", + fix="Your JDex format should not mix folders and notes that share a naming scheme.", + ) + + +@dataclass(frozen=True) +class JDexIssueArbitraryContentWhereNotAllowed(JDexIssue): + """Content was found in the JDex that didn't match any expected format.""" + + possible_formats: list[str] + type: Literal["JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED"] = ( + "JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED" + ) + + def display(self) -> str: + """Display this particular instance of an error.""" + return f'{self.file!s} (matched none of "{self.possible_formats}")' def explain(self) -> _Explanation: """Explain what this error is.""" return _Explanation( - explanation="JDex files were found outside of categories in a nested structure.", - fix="JDex files should be entirely flat, or nested under area then category.", + explanation="Files or folders were found in the JDex that matched no expected format.", + fix="You should either make the content match or set allow_arbitrary_content to true if it is intended for random content to be mixed in.", ) @@ -1026,7 +1104,7 @@ def explain(self) -> _Explanation: ) -JDexErrorType = ( +JDexIssueType = ( JDexAreaHeaderDifferentFromArea | JDexAreaHeaderWithoutArea | JDexCategoryInWrongArea @@ -1034,7 +1112,7 @@ def explain(self) -> _Explanation: | JDexDuplicateAreaHeader | JDexDuplicateCategory | JDexDuplicateId - | JDexFileOutsideCategory + | JDexIssueFileWhereFolderExpected | JDexIdInWrongCategory | JDexInvalidAreaName | JDexInvalidCategoryName @@ -1046,9 +1124,8 @@ def explain(self) -> _Explanation: class File: """A file or folder that has been detected by jdlint.""" - name: str - full_path: str - nested_under: list[str] + name: Path + path: Path @dataclass(frozen=True) @@ -1078,40 +1155,27 @@ def explain(self) -> _Explanation: @dataclass(frozen=True) -class JDexError: - """A single error detected in the JDex.""" - - error: JDexErrorType - files: list[File] - - def type(self) -> str: - """Return the name (type) of the error.""" - return self.error.type - - def display(self) -> str: - """Display this particular instance of an error.""" - return self.error.display(self.files) +class StructureTree: + """A node in the tree of the system.""" - def explain(self) -> _Explanation: - """Explain what this error is.""" - return self.error.explain() + name: str + children: list[StructureTree] @dataclass(frozen=True) class LintResults: - """All errors returned from linting files, as well as dictionaries of all areas, categories, and IDs used (and their names).""" + """All errors returned from linting files, as well as a tree of the structure of the JD system.""" errors: list[Error] - used_areas: dict[str, list[tuple[str, File]]] - used_categories: dict[str, list[tuple[str, File]]] - used_ids: dict[str, list[tuple[str, File]]] + jdex_errors: list[JDexIssue] + structure: list[StructureTree] @dataclass class _JDexAccumulator: """Accumulator used by _get_jdex_entries to gather information about the JDex.""" - errors: list[JDexError] + errors: list[JDexIssue] areas: dict[str, list[tuple[str, File]]] categories: dict[str, list[tuple[str, File]]] ids: dict[str, list[tuple[str, File]]] @@ -1138,25 +1202,22 @@ class _EnhancedJSONEncoder(json.JSONEncoder): def default(self, o: object) -> object: # Add JSON encoding for dataclasses and paths if dataclasses.is_dataclass(o): - return dataclasses.asdict(o) # type: ignore[arg-type] + return dataclasses.asdict(o) # ty:ignore[invalid-argument-type] if isinstance(o, PurePath): return str(o) return super().default(o) -def _sort_error(e: Error | JDexError) -> tuple[str, list[tuple[list[str], str]]]: - # Sort errors alphabetically by type, then by file/s affected +def _sort_error(e: JDexIssue) -> tuple[str, tuple[tuple[str, ...], str]]: + # Sort errors alphabetically by type, then by file affected + if e.type is None: + raise NotImplementedError return ( - e.error.type, - [_sort_file(f) for f in e.files], + e.type, + (e.file.parent.parts, e.file.name), ) -def _sort_file(f: File) -> tuple[list[str], str]: - # Sort files first by degree of nesting, then alphabetically - return (f.nested_under, f.name) - - # Any valid area folder name valid_area_re = re.compile("([0-9])0-(?:\\1)9 (.+)") # Any valid category folder name @@ -1203,19 +1264,19 @@ def _entry_is_ignored( E = TypeVar("E") -def _error_if_dups( # Python's types are horrid and it just is awful to try to type this better - make_error_type: Callable[[str], Any], - make_error: Callable[[Any, list[File]], E], - d: dict[str, list[tuple[Any, File]]], -) -> list[E]: - return [ - make_error( - make_error_type(k), - sorted([file for (_, file) in v], key=_sort_file), - ) - for k, v in d.items() - if len(v) > 1 - ] +# def _error_if_dups( # Python's types are horrid and it just is awful to try to type this better +# make_error_type: Callable[[str], Any], +# make_error: Callable[[Any, list[File]], E], +# d: dict[str, list[tuple[Any, File]]], +# ) -> list[E]: +# return [ +# make_error( +# make_error_type(k), +# sorted([file for (_, file) in v], key=_sort_file), +# ) +# for k, v in d.items() +# if len(v) > 1 +# ] def _insert_append(k, v, d) -> None: # noqa: ANN001 @@ -1264,501 +1325,591 @@ def _process_single_file_jdex(path: Path) -> _JDexResults: ) -def _process_flat_jdex_structure( - files: list[os.DirEntry], - jdex: _JDexAccumulator, - *, - ignored: list[str] | None, - alt_zeros: bool = False, -) -> None: - """Process a JDex that is a series of flat files.""" - area_re = re.compile( - "0([0-9])\\.00 (.+?)( area management)?( index)?(\\.md)?" - if alt_zeros - else "([0-9])0\\.00 (.+?)( area management)?( index)?(\\.md)?", - flags=re.IGNORECASE, - ) - category_re = re.compile( - # We need to tolerate the "area management" suffix for a category as well, to create categories from e.g. `01.00 Life Admin Area Management` - "([0-9][0-9])\\.00 (.+?)( (category|area) management)?( index)?(\\.md)?" - if alt_zeros - else "([0-9][1-9])\\.00 (.+?)( category management)?( index)?(\\.md)?", - flags=re.IGNORECASE, - ) +# def _process_flat_jdex_structure( +# files: list[os.DirEntry], +# jdex: _JDexAccumulator, +# *, +# ignored: list[str] | None, +# alt_zeros: bool = False, +# ) -> None: +# """Process a JDex that is a series of flat files.""" +# area_re = re.compile( +# "0([0-9])\\.00 (.+?)( area management)?( index)?(\\.md)?" +# if alt_zeros +# else "([0-9])0\\.00 (.+?)( area management)?( index)?(\\.md)?", +# flags=re.IGNORECASE, +# ) +# category_re = re.compile( +# # We need to tolerate the "area management" suffix for a category as well, to create categories from e.g. `01.00 Life Admin Area Management` +# "([0-9][0-9])\\.00 (.+?)( (category|area) management)?( index)?(\\.md)?" +# if alt_zeros +# else "([0-9][1-9])\\.00 (.+?)( category management)?( index)?(\\.md)?", +# flags=re.IGNORECASE, +# ) + +# for jid in files: +# if _entry_is_ignored(ignored, [], jid): +# continue + +# file = File(name=jid.name, full_path=jid.path, nested_under=[]) + +# # Check if the file matches an area +# area_match = area_re.fullmatch(jid.name) +# if area_match: +# _insert_append( +# area_match.group(1), +# (area_match.group(2), file), +# jdex.areas, +# ) + +# # Check if the file matches a category +# cat_match = category_re.fullmatch(jid.name) +# if cat_match: +# _insert_append( +# cat_match.group(1), +# (cat_match.group(2), file), +# jdex.categories, +# ) + +# # Check if it's a header match for alt zeros +# header_match = jdex_note_header_re.fullmatch(jid.name) +# if header_match: +# _insert_append( +# header_match.group(1), +# (header_match.group(2), file), +# jdex.headers, +# ) +# continue + +# # The file should also be a valid ID (or is bad) +# id_match = jdex_note_generic_id_re.fullmatch(jid.name) +# if id_match: +# _insert_append( +# f"{id_match.group(1)}.{id_match.group(2)}", +# (id_match.group(3), file), +# jdex.ids, +# ) +# else: +# jdex.errors.append( +# JDexIssue(error=JDexInvalidIDName(), files=[file]), +# ) + + +# def _process_nested_jdex_structure( +# path: Path, +# jdex: _JDexAccumulator, +# root_level_files: list[os.DirEntry], +# *, +# ignored: list[str] | None, +# ) -> None: +# for area in os.scandir(path): +# if _entry_is_ignored(ignored, [], area): +# continue +# if area.is_file(): +# # Maybe we have a flat structure +# root_level_files.append(area) +# continue + +# # Otherwise, a directory, so nested structure +# area_file = File(name=area.name, full_path=area.path, nested_under=[]) +# area_match = valid_area_re.fullmatch(area.name) +# if not area_match: +# jdex.errors.append( +# JDexIssue(error=JDexInvalidAreaName(), files=[area_file]), +# ) +# continue +# _insert_append( +# area_match.group(1), +# (area_match.group(2), area_file), +# jdex.areas, +# ) +# cat_re = _valid_category_re(area_match.group(1)) +# with os.scandir(area.path) as cats_it: +# for cat in cats_it: +# if _entry_is_ignored(ignored, [area.name], cat): +# continue +# cat_file = File( +# name=cat.name, +# full_path=cat.path, +# nested_under=[area.name], +# ) +# if cat.is_file(): +# jdex.errors.append( +# JDexIssue( +# error=JDexFileOutsideCategory(), +# files=[cat_file], +# ), +# ) +# continue + +# if cat_match := cat_re.fullmatch(cat.name): +# _insert_append( +# cat_match.group(1), +# (cat_match.group(2), cat_file), +# jdex.categories, +# ) +# id_re = _jdex_note_id_re(cat_match.group(1)) +# with os.scandir(cat.path) as ids_it: +# nested_under = [area.name, cat.name] +# for jid in ids_it: +# if _entry_is_ignored(ignored, nested_under, jid): +# continue +# id_file = File( +# name=jid.name, +# full_path=jid.path, +# nested_under=nested_under, +# ) +# if id_match := id_re.fullmatch(jid.name): +# _insert_append( +# id_match.group(1), +# (id_match.group(2), id_file), +# jdex.ids, +# ) +# elif gen_match := jdex_note_generic_id_re.fullmatch( +# jid.name, +# ): +# jdex.errors.append( +# JDexIssue( +# error=JDexIdInWrongCategory( +# id_ac=gen_match.group(1), +# file_ac=cat_match.group(1), +# ), +# files=[id_file], +# ), +# ) +# else: +# jdex.errors.append( +# JDexIssue( +# error=JDexInvalidIDName(), +# files=[id_file], +# ), +# ) + +# elif gen_match := generic_category_re.fullmatch(cat.name): +# jdex.errors.append( +# JDexIssue( +# error=JDexCategoryInWrongArea( +# category_area=gen_match.group(1), +# file_area=area_match.group(1), +# ), +# files=[cat_file], +# ), +# ) +# else: +# jdex.errors.append( +# JDexIssue(error=JDexInvalidCategoryName(), files=[cat_file]), +# ) + + +# def _get_jdex_entries( +# jdex_dir: Path, +# *, +# ignored: list[str] | None, +# alt_zeros: bool = False, +# ) -> _JDexResults | list[JDexIssue]: +# """Return canonical JDex information or a list of errors for it.""" +# if jdex_dir.is_file(): +# # Single file JDex +# return _process_single_file_jdex(jdex_dir) + +# jdex: _JDexAccumulator = _JDexAccumulator() +# root_level_files: list[os.DirEntry] = [] + +# _process_nested_jdex_structure(jdex_dir, jdex, root_level_files, ignored=ignored) + +# if jdex.ids or jdex.errors: +# # Not a flat structure, so we need to add all root level files as invalid +# jdex.errors.extend( +# [ +# JDexIssue( +# error=JDexFileOutsideCategory(), +# files=[ +# File( +# name=f.name, +# full_path=f.path, +# nested_under=[], +# ), +# ], +# ) +# for f in root_level_files +# ], +# ) + +# else: +# # Nothing nested, and not a file, so assume a flat structure +# _process_flat_jdex_structure( +# root_level_files, +# jdex, +# ignored=ignored, +# alt_zeros=alt_zeros, +# ) + +# # These duplicate errors apply regardless of JDex type +# jdex.errors.extend(_error_if_dups(JDexDuplicateArea, JDexIssue, jdex.areas)) +# jdex.errors.extend( +# _error_if_dups(JDexDuplicateCategory, JDexIssue, jdex.categories), +# ) +# jdex.errors.extend(_error_if_dups(JDexDuplicateId, JDexIssue, jdex.ids)) +# jdex.errors.extend(_error_if_dups(JDexDuplicateAreaHeader, JDexIssue, jdex.headers)) + +# for header, files in jdex.headers.items(): +# if header not in jdex.areas: +# jdex.errors.append( +# JDexIssue( +# error=JDexAreaHeaderWithoutArea(area=header), +# files=[f for (_, f) in files], +# ), +# ) +# elif len(files) == 1 and files[0][0] != jdex.areas[header][0][0]: +# jdex.errors.append( +# JDexIssue( +# error=JDexAreaHeaderDifferentFromArea( +# area=header, +# jdex_name=f"{_print_area(header)} {jdex.areas[header][0][0]}", +# ), +# files=[f for (_, f) in files], +# ), +# ) + +# if jdex.errors: +# return jdex.errors +# return _JDexResults( +# areas={k: f"{_print_area(k)} {v[0][0]}" for k, v in jdex.areas.items()}, +# categories={k: f"{k} {v[0][0]}" for k, v in jdex.categories.items()}, +# ids={k: f"{k} {v[0][0]}" for k, v in jdex.ids.items()}, +# ) + + +# def lint_dir( +# path: Path, +# ignored: list[str] | None = None, +# ) -> LintResults: +# """Check a root of a JD system for issues.""" +# errors: list[Error] = [] +# used_areas: dict[str, list[tuple[str, File]]] = {} +# used_categories: dict[str, list[tuple[str, File]]] = {} +# used_ids: dict[str, list[tuple[str, File]]] = {} + +# def check_inbox(nested_under: list[str], f: os.DirEntry) -> None: +# if inbox_re.fullmatch(f.name): +# entries = len(os.listdir(f.path)) +# if entries: +# errors.append( +# Error( +# error=NonemptyInbox(num_items=entries), +# files=[ +# File( +# name=f.name, +# full_path=f.path, +# nested_under=nested_under, +# ), +# ], +# ), +# ) + +# def check_if_out_of_id(file: os.DirEntry, nested_under: list[str]) -> bool: +# if file.is_file(): +# errors.append( +# Error( +# error=FileOutsideId(), +# files=[ +# File( +# name=file.name, +# full_path=file.path, +# nested_under=nested_under, +# ), +# ], +# ), +# ) +# return True +# return False + +# with os.scandir(path) as areas_it: +# for area in areas_it: +# if _entry_is_ignored(ignored, [], area) or check_if_out_of_id(area, []): +# continue +# area_file = File( +# name=area.name, +# full_path=area.path, +# nested_under=[], +# ) +# area_match = valid_area_re.fullmatch(area.name) +# if not area_match: +# errors.append( +# Error( +# error=InvalidAreaName(), +# files=[area_file], +# ), +# ) +# continue +# # Valid area +# _insert_append( +# area_match.group(1), +# (area_match.group(2), area_file), +# used_areas, +# ) +# cat_re = _valid_category_re(area_match.group(1)) +# with os.scandir(area.path) as cats_it: +# for cat in cats_it: +# if _entry_is_ignored( +# ignored, +# [area.name], +# cat, +# ) or check_if_out_of_id(cat, [area.name]): +# continue +# cat_file = File( +# name=cat.name, +# full_path=cat.path, +# nested_under=[area.name], +# ) +# if cat_match := cat_re.fullmatch(cat.name): +# _insert_append( +# cat_match.group(1), +# (cat_match.group(2), cat_file), +# used_categories, +# ) +# id_re = _valid_id_re(cat_match.group(1)) +# with os.scandir(cat.path) as ids_it: +# nested_under = [area.name, cat.name] + +# for jid in ids_it: +# if _entry_is_ignored( +# ignored, +# nested_under, +# jid, +# ) or check_if_out_of_id(jid, nested_under): +# continue +# id_file = File( +# name=jid.name, +# full_path=jid.path, +# nested_under=nested_under, +# ) +# if id_match := id_re.fullmatch(jid.name): +# _insert_append( +# id_match.group(1), +# (id_match.group(2), id_file), +# used_ids, +# ) + +# check_inbox(nested_under, jid) +# elif gen_match := generic_id_re.fullmatch(jid.name): +# errors.append( +# Error( +# error=IdInWrongCategory( +# id_ac=gen_match.group( +# 1, +# ), +# file_ac=cat_match.group( +# 1, +# ), +# ), +# files=[id_file], +# ), +# ) + +# else: +# errors.append( +# Error( +# error=InvalidIDName(), +# files=[id_file], +# ), +# ) +# elif gen_match := generic_category_re.fullmatch(cat.name): +# errors.append( +# Error( +# error=CategoryInWrongArea( +# category_area=gen_match.group(1), +# file_area=area_match.group(1), +# ), +# files=[cat_file], +# ), +# ) +# else: +# errors.append( +# Error( +# error=InvalidCategoryName(), +# files=[cat_file], +# ), +# ) + +# errors.extend(_error_if_dups(DuplicateArea, Error, used_areas)) +# errors.extend(_error_if_dups(DuplicateCategory, Error, used_categories)) +# errors.extend(_error_if_dups(DuplicateId, Error, used_ids)) + +# return LintResults( +# errors=sorted(errors, key=_sort_error), +# used_areas=used_areas, +# used_categories=used_categories, +# used_ids=used_ids, +# ) + + +# def lint_dir_and_jdex( +# *, +# path: Path, +# jdex_path: Path, +# ignored: list[str] | None = None, +# alt_zeros: bool = False, +# ) -> tuple[list[Error], list[JDexIssue]]: +# """Check a root of a JD system and its JDex for issues.""" +# results = lint_dir(path, ignored) +# jdex = _get_jdex_entries(jdex_path, ignored=ignored, alt_zeros=alt_zeros) +# if isinstance(jdex, list): +# return (results.errors, sorted(jdex, key=_sort_error)) + +# errors = results.errors + +# for area, files in results.used_areas.items(): +# if area not in jdex.areas: +# errors.append( +# Error( +# error=AreaNotInJDex(area=area), +# files=[f for (_, f) in files], +# ), +# ) +# elif len(files) == 1 and files[0][1].name != jdex.areas[area]: +# errors.append( +# Error( +# error=AreaDifferentFromJDex( +# area=area, +# jdex_name=jdex.areas[area], +# ), +# files=[f for (_, f) in files], +# ), +# ) +# for category, files in results.used_categories.items(): +# if category not in jdex.categories: +# errors.append( +# Error( +# error=CategoryNotInJDex(category=category), +# files=[f for (_, f) in files], +# ), +# ) +# elif len(files) == 1 and files[0][1].name != jdex.categories[category]: +# errors.append( +# Error( +# error=CategoryDifferentFromJDex( +# category=category, +# jdex_name=jdex.categories[category], +# ), +# files=[f for (_, f) in files], +# ), +# ) +# for jid, files in results.used_ids.items(): +# if jid not in jdex.ids: +# errors.append( +# Error(error=IdNotInJDex(id=jid), files=[f for (_, f) in files]), +# ) +# elif len(files) == 1 and files[0][1].name != jdex.ids[jid]: +# errors.append( +# Error( +# error=IdDifferentFromJDex(id=jid, jdex_name=jdex.ids[jid]), +# files=[f for (_, f) in files], +# ), +# ) +# return (sorted(errors, key=_sort_error), []) - for jid in files: - if _entry_is_ignored(ignored, [], jid): - continue - - file = File(name=jid.name, full_path=jid.path, nested_under=[]) - - # Check if the file matches an area - area_match = area_re.fullmatch(jid.name) - if area_match: - _insert_append( - area_match.group(1), - (area_match.group(2), file), - jdex.areas, - ) - - # Check if the file matches a category - cat_match = category_re.fullmatch(jid.name) - if cat_match: - _insert_append( - cat_match.group(1), - (cat_match.group(2), file), - jdex.categories, - ) - - # Check if it's a header match for alt zeros - header_match = jdex_note_header_re.fullmatch(jid.name) - if header_match: - _insert_append( - header_match.group(1), - (header_match.group(2), file), - jdex.headers, - ) - continue - - # The file should also be a valid ID (or is bad) - id_match = jdex_note_generic_id_re.fullmatch(jid.name) - if id_match: - _insert_append( - f"{id_match.group(1)}.{id_match.group(2)}", - (id_match.group(3), file), - jdex.ids, - ) - else: - jdex.errors.append( - JDexError(error=JDexInvalidIDName(), files=[file]), - ) +def _print_area(d: str) -> str: + """Given the number of an area, pretty-print it.""" + return f"{d}0-{d}9" -def _process_nested_jdex_structure( - path: Path, - jdex: _JDexAccumulator, - root_level_files: list[os.DirEntry], - *, - ignored: list[str] | None, -) -> None: - for area in os.scandir(path): - if _entry_is_ignored(ignored, [], area): - continue - if area.is_file(): - # Maybe we have a flat structure - root_level_files.append(area) - continue - - # Otherwise, a directory, so nested structure - area_file = File(name=area.name, full_path=area.path, nested_under=[]) - area_match = valid_area_re.fullmatch(area.name) - if not area_match: - jdex.errors.append( - JDexError(error=JDexInvalidAreaName(), files=[area_file]), - ) - continue - _insert_append( - area_match.group(1), - (area_match.group(2), area_file), - jdex.areas, - ) - cat_re = _valid_category_re(area_match.group(1)) - with os.scandir(area.path) as cats_it: - for cat in cats_it: - if _entry_is_ignored(ignored, [area.name], cat): - continue - cat_file = File( - name=cat.name, - full_path=cat.path, - nested_under=[area.name], - ) - if cat.is_file(): - jdex.errors.append( - JDexError( - error=JDexFileOutsideCategory(), - files=[cat_file], - ), - ) - continue - if cat_match := cat_re.fullmatch(cat.name): - _insert_append( - cat_match.group(1), - (cat_match.group(2), cat_file), - jdex.categories, - ) - id_re = _jdex_note_id_re(cat_match.group(1)) - with os.scandir(cat.path) as ids_it: - nested_under = [area.name, cat.name] - for jid in ids_it: - if _entry_is_ignored(ignored, nested_under, jid): - continue - id_file = File( - name=jid.name, - full_path=jid.path, - nested_under=nested_under, - ) - if id_match := id_re.fullmatch(jid.name): - _insert_append( - id_match.group(1), - (id_match.group(2), id_file), - jdex.ids, - ) - elif gen_match := jdex_note_generic_id_re.fullmatch( - jid.name, - ): - jdex.errors.append( - JDexError( - error=JDexIdInWrongCategory( - id_ac=gen_match.group(1), - file_ac=cat_match.group(1), - ), - files=[id_file], - ), - ) - else: - jdex.errors.append( - JDexError( - error=JDexInvalidIDName(), - files=[id_file], - ), - ) - - elif gen_match := generic_category_re.fullmatch(cat.name): - jdex.errors.append( - JDexError( - error=JDexCategoryInWrongArea( - category_area=gen_match.group(1), - file_area=area_match.group(1), - ), - files=[cat_file], - ), - ) - else: - jdex.errors.append( - JDexError(error=JDexInvalidCategoryName(), files=[cat_file]), - ) +def _print_nest(f: File) -> str: + """Pretty-print a nested file.""" + if f.nested_under: + return str(PurePath(*f.nested_under, f.name)) + return f.name -def _get_jdex_entries( - jdex_dir: Path, - *, - ignored: list[str] | None, - alt_zeros: bool = False, -) -> _JDexResults | list[JDexError]: - """Return canonical JDex information or a list of errors for it.""" - if jdex_dir.is_file(): - # Single file JDex - return _process_single_file_jdex(jdex_dir) - - jdex: _JDexAccumulator = _JDexAccumulator() - root_level_files: list[os.DirEntry] = [] - - _process_nested_jdex_structure(jdex_dir, jdex, root_level_files, ignored=ignored) - - if jdex.ids or jdex.errors: - # Not a flat structure, so we need to add all root level files as invalid - jdex.errors.extend( - [ - JDexError( - error=JDexFileOutsideCategory(), - files=[ - File( - name=f.name, - full_path=f.path, - nested_under=[], - ), - ], - ) - for f in root_level_files - ], - ) +class JDexEntry: + """An entry in the JDex.""" - else: - # Nothing nested, and not a file, so assume a flat structure - _process_flat_jdex_structure( - root_level_files, - jdex, - ignored=ignored, - alt_zeros=alt_zeros, - ) + def __init__(self, name: str) -> None: + """Create a JDexEntry, given its filename.""" + self.name = name - # These duplicate errors apply regardless of JDex type - jdex.errors.extend(_error_if_dups(JDexDuplicateArea, JDexError, jdex.areas)) - jdex.errors.extend( - _error_if_dups(JDexDuplicateCategory, JDexError, jdex.categories), - ) - jdex.errors.extend(_error_if_dups(JDexDuplicateId, JDexError, jdex.ids)) - jdex.errors.extend(_error_if_dups(JDexDuplicateAreaHeader, JDexError, jdex.headers)) - - for header, files in jdex.headers.items(): - if header not in jdex.areas: - jdex.errors.append( - JDexError( - error=JDexAreaHeaderWithoutArea(area=header), - files=[f for (_, f) in files], - ), - ) - elif len(files) == 1 and files[0][0] != jdex.areas[header][0][0]: - jdex.errors.append( - JDexError( - error=JDexAreaHeaderDifferentFromArea( - area=header, - jdex_name=f"{_print_area(header)} {jdex.areas[header][0][0]}", - ), - files=[f for (_, f) in files], - ), - ) - if jdex.errors: - return jdex.errors - return _JDexResults( - areas={k: f"{_print_area(k)} {v[0][0]}" for k, v in jdex.areas.items()}, - categories={k: f"{k} {v[0][0]}" for k, v in jdex.categories.items()}, - ids={k: f"{k} {v[0][0]}" for k, v in jdex.ids.items()}, - ) +def _get_jdex_notes_here_or_children( + ignored: list[str], + bound_segments: dict[str, str], + path: os.PathLike, + tier: ConfigJDexTier | ConfigSystemJDex, +) -> tuple[list[JDexEntry], list[JDexIssue]]: + # Compile regexes for children + valid_children = [ + (re.compile(c.format.build_regex(bound_segments)), c) for c in tier.children + ] + valid_notes = [re.compile(n.format.build_regex(bound_segments)) for n in tier.notes] + accumulated_notes = [] + accumulated_errors = [] -def lint_dir( - path: Path, - ignored: list[str] | None = None, -) -> LintResults: - """Check a root of a JD system for issues.""" - errors: list[Error] = [] - used_areas: dict[str, list[tuple[str, File]]] = {} - used_categories: dict[str, list[tuple[str, File]]] = {} - used_ids: dict[str, list[tuple[str, File]]] = {} - - def check_inbox(nested_under: list[str], f: os.DirEntry) -> None: - if inbox_re.fullmatch(f.name): - entries = len(os.listdir(f.path)) - if entries: - errors.append( - Error( - error=NonemptyInbox(num_items=entries), - files=[ - File( - name=f.name, - full_path=f.path, - nested_under=nested_under, + with os.scandir(path) as contents: + for x in contents: + if _entry_is_ignored(ignored, [], x): + continue + for child_format, child in valid_children: + match = child_format.fullmatch(x.name) + if match: + # Is a valid child folder + if x.is_file(): + # This is an error + accumulated_errors.append( + JDexIssueFileWhereFolderExpected( + PurePath(x), + child.format.raw_format, ), - ], - ), - ) - - def check_if_out_of_id(file: os.DirEntry, nested_under: list[str]) -> bool: - if file.is_file(): - errors.append( - Error( - error=FileOutsideId(), - files=[ - File( - name=file.name, - full_path=file.path, - nested_under=nested_under, - ), - ], - ), - ) - return True - return False + ) + break - with os.scandir(path) as areas_it: - for area in areas_it: - if _entry_is_ignored(ignored, [], area) or check_if_out_of_id(area, []): - continue - area_file = File( - name=area.name, - full_path=area.path, - nested_under=[], - ) - area_match = valid_area_re.fullmatch(area.name) - if not area_match: - errors.append( - Error( - error=InvalidAreaName(), - files=[area_file], - ), - ) - continue - # Valid area - _insert_append( - area_match.group(1), - (area_match.group(2), area_file), - used_areas, - ) - cat_re = _valid_category_re(area_match.group(1)) - with os.scandir(area.path) as cats_it: - for cat in cats_it: - if _entry_is_ignored( + # Walk child + (child_notes, child_errors) = _get_jdex_notes_here_or_children( ignored, - [area.name], - cat, - ) or check_if_out_of_id(cat, [area.name]): - continue - cat_file = File( - name=cat.name, - full_path=cat.path, - nested_under=[area.name], + {**bound_segments, **match.groupdict()}, + x.path, + child, ) - if cat_match := cat_re.fullmatch(cat.name): - _insert_append( - cat_match.group(1), - (cat_match.group(2), cat_file), - used_categories, - ) - id_re = _valid_id_re(cat_match.group(1)) - with os.scandir(cat.path) as ids_it: - nested_under = [area.name, cat.name] - - for jid in ids_it: - if _entry_is_ignored( - ignored, - nested_under, - jid, - ) or check_if_out_of_id(jid, nested_under): - continue - id_file = File( - name=jid.name, - full_path=jid.path, - nested_under=nested_under, - ) - if id_match := id_re.fullmatch(jid.name): - _insert_append( - id_match.group(1), - (id_match.group(2), id_file), - used_ids, - ) - - check_inbox(nested_under, jid) - elif gen_match := generic_id_re.fullmatch(jid.name): - errors.append( - Error( - error=IdInWrongCategory( - id_ac=gen_match.group( - 1, - ), - file_ac=cat_match.group( - 1, - ), - ), - files=[id_file], - ), - ) - - else: - errors.append( - Error( - error=InvalidIDName(), - files=[id_file], - ), - ) - elif gen_match := generic_category_re.fullmatch(cat.name): - errors.append( - Error( - error=CategoryInWrongArea( - category_area=gen_match.group(1), - file_area=area_match.group(1), - ), - files=[cat_file], - ), - ) - else: - errors.append( - Error( - error=InvalidCategoryName(), - files=[cat_file], + accumulated_notes.extend(child_notes) + accumulated_errors.extend(child_errors) + break + else: + for note_format in valid_notes: + match = note_format.fullmatch(x.name) + if match: + # Is a valid JDex note + if x.is_dir(): + # This is an error + # TODO report error + break + + # Create note entry + accumulated_notes.append(JDexEntry(x.name)) + break + else: + # If we got here, it matched no known child/note + if not getattr(tier, "allow_arbitrary_contents", False): + # This is an error + accumulated_errors.append( + JDexIssueArbitraryContentWhereNotAllowed( + PurePath(x), + [c.format.raw_format for c in tier.children] + + [n.format.raw_format for n in tier.notes], ), ) + return (accumulated_notes, accumulated_errors) - errors.extend(_error_if_dups(DuplicateArea, Error, used_areas)) - errors.extend(_error_if_dups(DuplicateCategory, Error, used_categories)) - errors.extend(_error_if_dups(DuplicateId, Error, used_ids)) - - return LintResults( - errors=sorted(errors, key=_sort_error), - used_areas=used_areas, - used_categories=used_categories, - used_ids=used_ids, - ) - - -def lint_dir_and_jdex( - *, - path: Path, - jdex_path: Path, - ignored: list[str] | None = None, - alt_zeros: bool = False, -) -> tuple[list[Error], list[JDexError]]: - """Check a root of a JD system and its JDex for issues.""" - results = lint_dir(path, ignored) - jdex = _get_jdex_entries(jdex_path, ignored=ignored, alt_zeros=alt_zeros) - if isinstance(jdex, list): - return (results.errors, sorted(jdex, key=_sort_error)) - - errors = results.errors - - for area, files in results.used_areas.items(): - if area not in jdex.areas: - errors.append( - Error( - error=AreaNotInJDex(area=area), - files=[f for (_, f) in files], - ), - ) - elif len(files) == 1 and files[0][1].name != jdex.areas[area]: - errors.append( - Error( - error=AreaDifferentFromJDex( - area=area, - jdex_name=jdex.areas[area], - ), - files=[f for (_, f) in files], - ), - ) - for category, files in results.used_categories.items(): - if category not in jdex.categories: - errors.append( - Error( - error=CategoryNotInJDex(category=category), - files=[f for (_, f) in files], - ), - ) - elif len(files) == 1 and files[0][1].name != jdex.categories[category]: - errors.append( - Error( - error=CategoryDifferentFromJDex( - category=category, - jdex_name=jdex.categories[category], - ), - files=[f for (_, f) in files], - ), - ) - for jid, files in results.used_ids.items(): - if jid not in jdex.ids: - errors.append( - Error(error=IdNotInJDex(id=jid), files=[f for (_, f) in files]), - ) - elif len(files) == 1 and files[0][1].name != jdex.ids[jid]: - errors.append( - Error( - error=IdDifferentFromJDex(id=jid, jdex_name=jdex.ids[jid]), - files=[f for (_, f) in files], - ), - ) - return (sorted(errors, key=_sort_error), []) - - -def _print_area(d: str) -> str: - """Given the number of an area, pretty-print it.""" - return f"{d}0-{d}9" - -def _print_nest(f: File) -> str: - """Pretty-print a nested file.""" - if f.nested_under: - return str(PurePath(*f.nested_under, f.name)) - return f.name +def lint_system(config: Config) -> LintResults: + jdex_errors = [] + if config.system.jdex: + (jdex_notes, jdex_errors) = _get_jdex_notes_here_or_children( + config.linter.ignore + config.system.jdex.ignore, + {}, + config.system.jdex.path, + config.system.jdex, + ) + return LintResults([], sorted(jdex_errors, key=_sort_error), []) if __name__ == "__main__": diff --git a/run_tests.py b/run_tests.py index d3d88b4..ac580db 100755 --- a/run_tests.py +++ b/run_tests.py @@ -3,8 +3,10 @@ """Tests for jdlint.""" from __future__ import annotations +import contextlib import dataclasses +import tomllib import json import os import unittest @@ -14,88 +16,43 @@ import jdlint -def _convert_err(e: jdlint.Error | jdlint.JDexError) -> dict[str, Any]: - return { - "error": dataclasses.asdict(e.error) # type: ignore[arg-type] - if dataclasses.is_dataclass(e.error) - else e.error, - "files": [ - # Strip full_path, since it's dependent on where we're running the test - {"name": f.name, "nested_under": f.nested_under} - for f in e.files - ], - } - - class AllTests(unittest.TestCase): """Locate and run all tests.""" - def tests_without_jdex(self) -> None: - """Locate and run tests that don't require the jdex.""" + def tests(self) -> None: + """Locate and run all tests.""" self.maxDiff = None # Show full diff # Find all tests - with os.scandir(PurePath("tests", "without_jdex")) as test_it: + with os.scandir(PurePath("tests")) as test_it: for f in test_it: # Make a subtest and open result file - with self.subTest(msg=f.name, f=f), Path( - f, - "result.json", - ).open() as golden_file: - # Get ignore file if any - try: - ignore = Path(f, "ignore").read_text().splitlines() - except FileNotFoundError: - ignore = [] + with ( + self.subTest(msg=f.name, f=f), + Path( + f, + "result.json", + ).open() as golden_file, + Path(f, "jdlint.toml").open("rb") as config_file, + contextlib.chdir(f), + ): + # Load config + config = jdlint.Config(tomllib.load(config_file)) # Lint the test dir - results = jdlint.lint_dir( - Path(f, "files"), - ignored=ignore, - ) + results = jdlint.lint_system(config) expected = json.load(golden_file) # Convert lint results into loaded format - actual = { - "errors": [_convert_err(e) for e in results.errors], - "jdex_errors": [], - } - - # Compare results - self.assertEqual(expected, actual) # noqa: PT009 - - def tests_with_jdex(self) -> None: - """Locate and run tests that have the JDex.""" - self.maxDiff = None # Show full diff - - # Find all tests - with os.scandir(PurePath("tests", "with_jdex")) as test_it: - for f in test_it: - # Make a subtest and open result file - with self.subTest(msg=f.name, f=f), Path( - f, - "result.json", - ).open() as golden_file: - # Get ignore file if any - try: - ignore = Path(f, "ignore").read_text().splitlines() - except FileNotFoundError: - ignore = [] - - # Lint the test dir and JDex - (errors, jdex_errors) = jdlint.lint_dir_and_jdex( - path=Path(f, "files"), - jdex_path=Path(f, "jdex"), - ignored=ignore, - alt_zeros=Path(f, "altzeros").exists(), + actual = json.loads( + json.dumps( + { + "errors": results.errors, + "jdex_errors": results.jdex_errors, + }, + cls=jdlint._EnhancedJSONEncoder, + ) ) - expected = json.load(golden_file) - - # Convert lint results into loaded format - actual = { - "errors": [_convert_err(e) for e in errors], - "jdex_errors": [_convert_err(e) for e in jdex_errors], - } # Compare results self.assertEqual(expected, actual) # noqa: PT009 diff --git a/tests/with_jdex/area_different_from_jdex_flat/files/00-09 Systme/01 System Stuff/01.02 A Name/.placeholder b/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/1A/1A.md similarity index 100% rename from tests/with_jdex/area_different_from_jdex_flat/files/00-09 Systme/01 System Stuff/01.02 A Name/.placeholder rename to tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/1A/1A.md diff --git a/tests/with_jdex/area_different_from_jdex_flat/files/00-09 Systme/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder b/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/1A/File similarity index 100% rename from tests/with_jdex/area_different_from_jdex_flat/files/00-09 Systme/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder rename to tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/1A/File diff --git a/tests/with_jdex/area_different_from_jdex_nested/files/00-09 Systme/01 System Stuff/01.02 A Name/.placeholder b/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/1A/Folder/.placeholder similarity index 100% rename from tests/with_jdex/area_different_from_jdex_nested/files/00-09 Systme/01 System Stuff/01.02 A Name/.placeholder rename to tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/1A/Folder/.placeholder diff --git a/tests/with_jdex/area_different_from_jdex_flat/files/00-09 Systme/01 System Stuff/01.03 Another ID/File Inside ID b/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/A1/File similarity index 100% rename from tests/with_jdex/area_different_from_jdex_flat/files/00-09 Systme/01 System Stuff/01.03 Another ID/File Inside ID rename to tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/A1/File diff --git a/tests/with_jdex/area_different_from_jdex_nested/files/00-09 Systme/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder b/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/A1/Folder/.placeholder similarity index 100% rename from tests/with_jdex/area_different_from_jdex_nested/files/00-09 Systme/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder rename to tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/A1/Folder/.placeholder diff --git a/tests/with_jdex/area_different_from_jdex_flat/jdex/00.00 System.md b/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/A1/X.md similarity index 100% rename from tests/with_jdex/area_different_from_jdex_flat/jdex/00.00 System.md rename to tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/A1/X.md diff --git a/tests/with_jdex/area_different_from_jdex_flat/jdex/01.00 System Stuff.md b/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/File similarity index 100% rename from tests/with_jdex/area_different_from_jdex_flat/jdex/01.00 System Stuff.md rename to tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/File diff --git a/tests/with_jdex/area_not_in_jdex/files/00-09 System/01 System Stuff/01.02 A Name/.placeholder b/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/Folder/.placeholder similarity index 100% rename from tests/with_jdex/area_not_in_jdex/files/00-09 System/01 System Stuff/01.02 A Name/.placeholder rename to tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/Folder/.placeholder diff --git a/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml b/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml new file mode 100644 index 0000000..d4a142e --- /dev/null +++ b/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml @@ -0,0 +1,39 @@ +[linter] +json_output = true + +[[system.roots]] +name = "JDex" +path = "files" + +[system.jdex] +path = "files" + +[[system.jdex.children]] +name = "JDex Folder" +format = "/#A/" + +[[system.jdex.children.children]] +name = "JDex Folder" +format = "/*Name//=A/" +allow_arbitrary_contents = true + +[[system.jdex.children.children.notes]] +name = "JDex Note" +format = "X.md" + +[[system.jdex.children.children]] +name = "JDex Folder" +format = "/=A//*Name/" + +[[system.jdex.children.children.notes]] +name = "JDex Note" +format = "/=A//=Name/.md" + +## ########################################################### +# Standard +## ########################################################### +[[system.children]] +name = "Area" +format = "/#A/0-/=A/9 /*Area/" +jdex_note = "/=A/0.00 /=Area/.md" +allow_arbitrary_contents = true diff --git a/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/result.json b/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/result.json new file mode 100644 index 0000000..dcf4487 --- /dev/null +++ b/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/result.json @@ -0,0 +1,35 @@ +{ + "errors": [], + "jdex_errors": [ + { + "type": "JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED", + "file": "files/1/File", + "possible_formats": [ + "/*Name//=A/", + "/=A//*Name/" + ] + }, + { + "type": "JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED", + "file": "files/1/Folder", + "possible_formats": [ + "/*Name//=A/", + "/=A//*Name/" + ] + }, + { + "type": "JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED", + "file": "files/1/1A/File", + "possible_formats": [ + "/=A//=Name/.md" + ] + }, + { + "type": "JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED", + "file": "files/1/1A/Folder", + "possible_formats": [ + "/=A//=Name/.md" + ] + } + ] +} \ No newline at end of file diff --git a/tests/with_jdex/area_not_in_jdex/files/00-09 System/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder b/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/files/.obsidian/.placeholder similarity index 100% rename from tests/with_jdex/area_not_in_jdex/files/00-09 System/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder rename to tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/files/.obsidian/.placeholder diff --git a/tests/with_jdex/area_different_from_jdex_flat/jdex/01.02 A Name.md b/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/files/1/1A/1A.md similarity index 100% rename from tests/with_jdex/area_different_from_jdex_flat/jdex/01.02 A Name.md rename to tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/files/1/1A/1A.md diff --git a/tests/with_jdex/area_different_from_jdex_flat/jdex/01.03 Another ID.md b/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/files/1/1B similarity index 100% rename from tests/with_jdex/area_different_from_jdex_flat/jdex/01.03 Another ID.md rename to tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/files/1/1B diff --git a/tests/with_jdex/area_different_from_jdex_nested/files/00-09 Systme/01 System Stuff/01.00 An ID/Other File b/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/files/3 similarity index 100% rename from tests/with_jdex/area_different_from_jdex_nested/files/00-09 Systme/01 System Stuff/01.00 An ID/Other File rename to tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/files/3 diff --git a/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/jdlint.toml b/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/jdlint.toml new file mode 100644 index 0000000..dd5e111 --- /dev/null +++ b/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/jdlint.toml @@ -0,0 +1,32 @@ +[linter] +json_output = true + +[[system.roots]] +name = "JDex" +path = "files" +ignore = [".obsidian"] + +[system.jdex] +path = "files" +ignore = [".obsidian"] + +[[system.jdex.children]] +name = "JDex Folder" +format = "/#A/" + +[[system.jdex.children.children]] +name = "JDex Folder" +format = "/=A//*Name/" + +[[system.jdex.children.children.notes]] +name = "JDex Note" +format = "/=A//=Name/.md" + +## ########################################################### +# Standard +## ########################################################### +[[system.children]] +name = "Area" +format = "/#A/0-/=A/9 /*Area/" +jdex_note = "/=A/0.00 /=Area/.md" +allow_arbitrary_contents = true diff --git a/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/result.json b/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/result.json new file mode 100644 index 0000000..174e674 --- /dev/null +++ b/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/result.json @@ -0,0 +1,15 @@ +{ + "errors": [], + "jdex_errors": [ + { + "type": "JDEX_FILE_WHERE_FOLDER_EXPECTED", + "file": "files/3", + "matched_format": "/#A/" + }, + { + "type": "JDEX_FILE_WHERE_FOLDER_EXPECTED", + "file": "files/1/1B", + "matched_format": "/=A//*Name/" + } + ] +} \ No newline at end of file diff --git a/tests/with_jdex/area_different_from_jdex_flat/result.json b/tests/with_jdex/area_different_from_jdex_flat/result.json deleted file mode 100644 index b00c81d..0000000 --- a/tests/with_jdex/area_different_from_jdex_flat/result.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "errors": [ - { - "error": { - "type": "AREA_DIFFERENT_FROM_JDEX", - "area": "0", - "jdex_name": "00-09 System" - }, - "files": [ - { - "name": "00-09 Systme", - "nested_under": [] - } - ] - } - ], - "jdex_errors": [] -} diff --git a/tests/with_jdex/area_different_from_jdex_nested/files/00-09 Systme/01 System Stuff/01.03 Another ID/File Inside ID b/tests/with_jdex/area_different_from_jdex_nested/files/00-09 Systme/01 System Stuff/01.03 Another ID/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/area_different_from_jdex_nested/jdex/00-09 System/01 System Stuff/01.00 An ID.md b/tests/with_jdex/area_different_from_jdex_nested/jdex/00-09 System/01 System Stuff/01.00 An ID.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/area_different_from_jdex_nested/jdex/00-09 System/01 System Stuff/01.02 A Name.md b/tests/with_jdex/area_different_from_jdex_nested/jdex/00-09 System/01 System Stuff/01.02 A Name.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/area_different_from_jdex_nested/jdex/00-09 System/01 System Stuff/01.03 Another ID.md b/tests/with_jdex/area_different_from_jdex_nested/jdex/00-09 System/01 System Stuff/01.03 Another ID.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/area_different_from_jdex_nested/result.json b/tests/with_jdex/area_different_from_jdex_nested/result.json deleted file mode 100644 index b00c81d..0000000 --- a/tests/with_jdex/area_different_from_jdex_nested/result.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "errors": [ - { - "error": { - "type": "AREA_DIFFERENT_FROM_JDEX", - "area": "0", - "jdex_name": "00-09 System" - }, - "files": [ - { - "name": "00-09 Systme", - "nested_under": [] - } - ] - } - ], - "jdex_errors": [] -} diff --git a/tests/with_jdex/area_not_in_jdex/files/00-09 System/01 System Stuff/01.03 Another ID/File Inside ID b/tests/with_jdex/area_not_in_jdex/files/00-09 System/01 System Stuff/01.03 Another ID/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/area_not_in_jdex/files/10-19 Oops/11 Cat/11.12 ID/.placeholder b/tests/with_jdex/area_not_in_jdex/files/10-19 Oops/11 Cat/11.12 ID/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/area_not_in_jdex/jdex/00.00 System.md b/tests/with_jdex/area_not_in_jdex/jdex/00.00 System.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/area_not_in_jdex/jdex/01.00 System Stuff.md b/tests/with_jdex/area_not_in_jdex/jdex/01.00 System Stuff.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/area_not_in_jdex/jdex/01.02 A Name.md b/tests/with_jdex/area_not_in_jdex/jdex/01.02 A Name.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/area_not_in_jdex/jdex/01.03 Another ID.md b/tests/with_jdex/area_not_in_jdex/jdex/01.03 Another ID.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/area_not_in_jdex/jdex/11.00 Cat.md b/tests/with_jdex/area_not_in_jdex/jdex/11.00 Cat.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/area_not_in_jdex/jdex/11.12 ID.md b/tests/with_jdex/area_not_in_jdex/jdex/11.12 ID.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/area_not_in_jdex/result.json b/tests/with_jdex/area_not_in_jdex/result.json deleted file mode 100644 index 8638b38..0000000 --- a/tests/with_jdex/area_not_in_jdex/result.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "errors": [ - { - "error": { - "type": "AREA_NOT_IN_JDEX", - "area": "1" - }, - "files": [ - { - "name": "10-19 Oops", - "nested_under": [] - } - ] - } - ], - "jdex_errors": [] -} diff --git a/tests/with_jdex/category_different_from_jdex_flat/files/00-09 System/01 System Stuf/01.02 A Name/.placeholder b/tests/with_jdex/category_different_from_jdex_flat/files/00-09 System/01 System Stuf/01.02 A Name/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/category_different_from_jdex_flat/files/00-09 System/01 System Stuf/01.03 Another ID/Directory Inside Id/.placeholder b/tests/with_jdex/category_different_from_jdex_flat/files/00-09 System/01 System Stuf/01.03 Another ID/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/category_different_from_jdex_flat/files/00-09 System/01 System Stuf/01.03 Another ID/File Inside ID b/tests/with_jdex/category_different_from_jdex_flat/files/00-09 System/01 System Stuf/01.03 Another ID/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/category_different_from_jdex_flat/jdex/00.00 System.md b/tests/with_jdex/category_different_from_jdex_flat/jdex/00.00 System.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/category_different_from_jdex_flat/jdex/01.00 System Stuff.md b/tests/with_jdex/category_different_from_jdex_flat/jdex/01.00 System Stuff.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/category_different_from_jdex_flat/jdex/01.02 A Name.md b/tests/with_jdex/category_different_from_jdex_flat/jdex/01.02 A Name.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/category_different_from_jdex_flat/jdex/01.03 Another ID.md b/tests/with_jdex/category_different_from_jdex_flat/jdex/01.03 Another ID.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/category_different_from_jdex_flat/result.json b/tests/with_jdex/category_different_from_jdex_flat/result.json deleted file mode 100644 index 5411e6e..0000000 --- a/tests/with_jdex/category_different_from_jdex_flat/result.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "errors": [ - { - "error": { - "type": "CATEGORY_DIFFERENT_FROM_JDEX", - "category": "01", - "jdex_name": "01 System Stuff" - }, - "files": [ - { - "name": "01 System Stuf", - "nested_under": [ - "00-09 System" - ] - } - ] - } - ], - "jdex_errors": [] -} diff --git a/tests/with_jdex/category_different_from_jdex_nested/files/00-09 System/01 System Stuf/01.00 An ID/Other File b/tests/with_jdex/category_different_from_jdex_nested/files/00-09 System/01 System Stuf/01.00 An ID/Other File deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/category_different_from_jdex_nested/files/00-09 System/01 System Stuf/01.02 A Name/.placeholder b/tests/with_jdex/category_different_from_jdex_nested/files/00-09 System/01 System Stuf/01.02 A Name/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/category_different_from_jdex_nested/files/00-09 System/01 System Stuf/01.03 Another ID/Directory Inside Id/.placeholder b/tests/with_jdex/category_different_from_jdex_nested/files/00-09 System/01 System Stuf/01.03 Another ID/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/category_different_from_jdex_nested/files/00-09 System/01 System Stuf/01.03 Another ID/File Inside ID b/tests/with_jdex/category_different_from_jdex_nested/files/00-09 System/01 System Stuf/01.03 Another ID/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/category_different_from_jdex_nested/jdex/00-09 System/01 System Stuff/01.00 An ID.md b/tests/with_jdex/category_different_from_jdex_nested/jdex/00-09 System/01 System Stuff/01.00 An ID.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/category_different_from_jdex_nested/jdex/00-09 System/01 System Stuff/01.02 A Name.md b/tests/with_jdex/category_different_from_jdex_nested/jdex/00-09 System/01 System Stuff/01.02 A Name.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/category_different_from_jdex_nested/jdex/00-09 System/01 System Stuff/01.03 Another ID.md b/tests/with_jdex/category_different_from_jdex_nested/jdex/00-09 System/01 System Stuff/01.03 Another ID.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/category_different_from_jdex_nested/result.json b/tests/with_jdex/category_different_from_jdex_nested/result.json deleted file mode 100644 index 5411e6e..0000000 --- a/tests/with_jdex/category_different_from_jdex_nested/result.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "errors": [ - { - "error": { - "type": "CATEGORY_DIFFERENT_FROM_JDEX", - "category": "01", - "jdex_name": "01 System Stuff" - }, - "files": [ - { - "name": "01 System Stuf", - "nested_under": [ - "00-09 System" - ] - } - ] - } - ], - "jdex_errors": [] -} diff --git a/tests/with_jdex/category_not_in_jdex/files/00-09 System/01 System Stuff/01.02 An ID/.placeholder b/tests/with_jdex/category_not_in_jdex/files/00-09 System/01 System Stuff/01.02 An ID/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/category_not_in_jdex/files/00-09 System/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder b/tests/with_jdex/category_not_in_jdex/files/00-09 System/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/category_not_in_jdex/files/00-09 System/01 System Stuff/01.03 Another ID/File Inside ID b/tests/with_jdex/category_not_in_jdex/files/00-09 System/01 System Stuff/01.03 Another ID/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/category_not_in_jdex/jdex/00.00 System.md b/tests/with_jdex/category_not_in_jdex/jdex/00.00 System.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/category_not_in_jdex/jdex/01.02 An ID.md b/tests/with_jdex/category_not_in_jdex/jdex/01.02 An ID.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/category_not_in_jdex/jdex/01.03 Another ID.md b/tests/with_jdex/category_not_in_jdex/jdex/01.03 Another ID.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/category_not_in_jdex/result.json b/tests/with_jdex/category_not_in_jdex/result.json deleted file mode 100644 index 700977b..0000000 --- a/tests/with_jdex/category_not_in_jdex/result.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "errors": [ - { - "error": { - "type": "CATEGORY_NOT_IN_JDEX", - "category": "01" - }, - "files": [ - { - "name": "01 System Stuff", - "nested_under": [ - "00-09 System" - ] - } - ] - } - ], - "jdex_errors": [] -} diff --git a/tests/with_jdex/flat_alt_zeros/altzeros b/tests/with_jdex/flat_alt_zeros/altzeros deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_alt_zeros/files/00-09 System/01 Life Admin/01.03 Area Standard Zero/Directory Inside Id/.placeholder b/tests/with_jdex/flat_alt_zeros/files/00-09 System/01 Life Admin/01.03 Area Standard Zero/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_alt_zeros/files/00-09 System/01 Life Admin/01.03 Area Standard Zero/File Inside ID b/tests/with_jdex/flat_alt_zeros/files/00-09 System/01 Life Admin/01.03 Area Standard Zero/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_alt_zeros/files/10-19 Life Admin/10 Me, Myself, and I/10.02 An ID/.placeholder b/tests/with_jdex/flat_alt_zeros/files/10-19 Life Admin/10 Me, Myself, and I/10.02 An ID/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_alt_zeros/jdex/00.00 System Area Management.md b/tests/with_jdex/flat_alt_zeros/jdex/00.00 System Area Management.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_alt_zeros/jdex/01.00 Life Admin Area Management.md b/tests/with_jdex/flat_alt_zeros/jdex/01.00 Life Admin Area Management.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_alt_zeros/jdex/01.03 Area Standard Zero.md b/tests/with_jdex/flat_alt_zeros/jdex/01.03 Area Standard Zero.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_alt_zeros/jdex/10.00 Me, Myself, and I.md b/tests/with_jdex/flat_alt_zeros/jdex/10.00 Me, Myself, and I.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_alt_zeros/jdex/10.02 An ID.md b/tests/with_jdex/flat_alt_zeros/jdex/10.02 An ID.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_alt_zeros/result.json b/tests/with_jdex/flat_alt_zeros/result.json deleted file mode 100644 index b784b4e..0000000 --- a/tests/with_jdex/flat_alt_zeros/result.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "errors": [], - "jdex_errors": [] -} diff --git a/tests/with_jdex/flat_alt_zeros_duplicate_header/altzeros b/tests/with_jdex/flat_alt_zeros_duplicate_header/altzeros deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_alt_zeros_duplicate_header/files/00-09 System/01 Life Admin/01.03 Area Standard Zero/Directory Inside Id/.placeholder b/tests/with_jdex/flat_alt_zeros_duplicate_header/files/00-09 System/01 Life Admin/01.03 Area Standard Zero/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_alt_zeros_duplicate_header/files/00-09 System/01 Life Admin/01.03 Area Standard Zero/File Inside ID b/tests/with_jdex/flat_alt_zeros_duplicate_header/files/00-09 System/01 Life Admin/01.03 Area Standard Zero/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_alt_zeros_duplicate_header/files/10-19 Life Admin/10 Me, Myself, and I/10.02 An ID/.placeholder b/tests/with_jdex/flat_alt_zeros_duplicate_header/files/10-19 Life Admin/10 Me, Myself, and I/10.02 An ID/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_alt_zeros_duplicate_header/jdex/00.00 System Area Management.md b/tests/with_jdex/flat_alt_zeros_duplicate_header/jdex/00.00 System Area Management.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_alt_zeros_duplicate_header/jdex/01.00 Life Admin Area Management.md b/tests/with_jdex/flat_alt_zeros_duplicate_header/jdex/01.00 Life Admin Area Management.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_alt_zeros_duplicate_header/jdex/01.03 Area Standard Zero.md b/tests/with_jdex/flat_alt_zeros_duplicate_header/jdex/01.03 Area Standard Zero.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_alt_zeros_duplicate_header/jdex/10. Life Admin.md b/tests/with_jdex/flat_alt_zeros_duplicate_header/jdex/10. Life Admin.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_alt_zeros_duplicate_header/jdex/10. Life Adminn.md b/tests/with_jdex/flat_alt_zeros_duplicate_header/jdex/10. Life Adminn.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_alt_zeros_duplicate_header/jdex/10.00 Me, Myself, and I.md b/tests/with_jdex/flat_alt_zeros_duplicate_header/jdex/10.00 Me, Myself, and I.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_alt_zeros_duplicate_header/jdex/10.02 An ID.md b/tests/with_jdex/flat_alt_zeros_duplicate_header/jdex/10.02 An ID.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_alt_zeros_duplicate_header/result.json b/tests/with_jdex/flat_alt_zeros_duplicate_header/result.json deleted file mode 100644 index 2ccc67b..0000000 --- a/tests/with_jdex/flat_alt_zeros_duplicate_header/result.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "errors": [], - "jdex_errors": [ - { - "error": { - "type": "JDEX_DUPLICATE_AREA_HEADER", - "area": "1" - }, - "files": [ - { - "name": "10. Life Admin.md", - "nested_under": [] - }, - { - "name": "10. Life Adminn.md", - "nested_under": [] - } - ] - } - ] -} diff --git a/tests/with_jdex/flat_alt_zeros_with_headers/altzeros b/tests/with_jdex/flat_alt_zeros_with_headers/altzeros deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_alt_zeros_with_headers/files/00-09 System/01 Life Admin/01.03 Area Standard Zero/Directory Inside Id/.placeholder b/tests/with_jdex/flat_alt_zeros_with_headers/files/00-09 System/01 Life Admin/01.03 Area Standard Zero/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_alt_zeros_with_headers/files/00-09 System/01 Life Admin/01.03 Area Standard Zero/File Inside ID b/tests/with_jdex/flat_alt_zeros_with_headers/files/00-09 System/01 Life Admin/01.03 Area Standard Zero/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_alt_zeros_with_headers/files/10-19 Life Admin/10 Me, Myself, and I/10.02 An ID/.placeholder b/tests/with_jdex/flat_alt_zeros_with_headers/files/10-19 Life Admin/10 Me, Myself, and I/10.02 An ID/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_alt_zeros_with_headers/jdex/00.00 System Area Management.md b/tests/with_jdex/flat_alt_zeros_with_headers/jdex/00.00 System Area Management.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_alt_zeros_with_headers/jdex/01.00 Life Admin Area Management.md b/tests/with_jdex/flat_alt_zeros_with_headers/jdex/01.00 Life Admin Area Management.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_alt_zeros_with_headers/jdex/01.03 Area Standard Zero.md b/tests/with_jdex/flat_alt_zeros_with_headers/jdex/01.03 Area Standard Zero.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_alt_zeros_with_headers/jdex/10. Life Admin.md b/tests/with_jdex/flat_alt_zeros_with_headers/jdex/10. Life Admin.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_alt_zeros_with_headers/jdex/10.00 Me, Myself, and I.md b/tests/with_jdex/flat_alt_zeros_with_headers/jdex/10.00 Me, Myself, and I.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_alt_zeros_with_headers/jdex/10.02 An ID.md b/tests/with_jdex/flat_alt_zeros_with_headers/jdex/10.02 An ID.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_alt_zeros_with_headers/result.json b/tests/with_jdex/flat_alt_zeros_with_headers/result.json deleted file mode 100644 index b784b4e..0000000 --- a/tests/with_jdex/flat_alt_zeros_with_headers/result.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "errors": [], - "jdex_errors": [] -} diff --git a/tests/with_jdex/flat_with_index_suffix/files/00-09 System/01 System Stuff/01.02 A Name/.placeholder b/tests/with_jdex/flat_with_index_suffix/files/00-09 System/01 System Stuff/01.02 A Name/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_with_index_suffix/files/00-09 System/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder b/tests/with_jdex/flat_with_index_suffix/files/00-09 System/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_with_index_suffix/files/00-09 System/01 System Stuff/01.03 Another ID/File Inside ID b/tests/with_jdex/flat_with_index_suffix/files/00-09 System/01 System Stuff/01.03 Another ID/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_with_index_suffix/jdex/00.00 System Index.md b/tests/with_jdex/flat_with_index_suffix/jdex/00.00 System Index.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_with_index_suffix/jdex/01.00 System Stuff Index.md b/tests/with_jdex/flat_with_index_suffix/jdex/01.00 System Stuff Index.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_with_index_suffix/jdex/01.02 A Name.md b/tests/with_jdex/flat_with_index_suffix/jdex/01.02 A Name.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_with_index_suffix/jdex/01.03 Another ID.md b/tests/with_jdex/flat_with_index_suffix/jdex/01.03 Another ID.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_with_index_suffix/result.json b/tests/with_jdex/flat_with_index_suffix/result.json deleted file mode 100644 index b784b4e..0000000 --- a/tests/with_jdex/flat_with_index_suffix/result.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "errors": [], - "jdex_errors": [] -} diff --git a/tests/with_jdex/flat_with_management_suffixes/files/00-09 System/01 System Stuff/01.02 A Name/.placeholder b/tests/with_jdex/flat_with_management_suffixes/files/00-09 System/01 System Stuff/01.02 A Name/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_with_management_suffixes/files/00-09 System/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder b/tests/with_jdex/flat_with_management_suffixes/files/00-09 System/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_with_management_suffixes/files/00-09 System/01 System Stuff/01.03 Another ID/File Inside ID b/tests/with_jdex/flat_with_management_suffixes/files/00-09 System/01 System Stuff/01.03 Another ID/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_with_management_suffixes/jdex/00.00 System Area Management.md b/tests/with_jdex/flat_with_management_suffixes/jdex/00.00 System Area Management.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_with_management_suffixes/jdex/01.00 System Stuff Category Management.md b/tests/with_jdex/flat_with_management_suffixes/jdex/01.00 System Stuff Category Management.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_with_management_suffixes/jdex/01.02 A Name.md b/tests/with_jdex/flat_with_management_suffixes/jdex/01.02 A Name.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_with_management_suffixes/jdex/01.03 Another ID.md b/tests/with_jdex/flat_with_management_suffixes/jdex/01.03 Another ID.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/flat_with_management_suffixes/result.json b/tests/with_jdex/flat_with_management_suffixes/result.json deleted file mode 100644 index b784b4e..0000000 --- a/tests/with_jdex/flat_with_management_suffixes/result.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "errors": [], - "jdex_errors": [] -} diff --git a/tests/with_jdex/id_different_from_jdex/files/00-09 System/01 System Stuff/01.02 A Naem/.placeholder b/tests/with_jdex/id_different_from_jdex/files/00-09 System/01 System Stuff/01.02 A Naem/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/id_different_from_jdex/files/00-09 System/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder b/tests/with_jdex/id_different_from_jdex/files/00-09 System/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/id_different_from_jdex/files/00-09 System/01 System Stuff/01.03 Another ID/File Inside ID b/tests/with_jdex/id_different_from_jdex/files/00-09 System/01 System Stuff/01.03 Another ID/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/id_different_from_jdex/files/00-09 System/01 System Stuff/01.04 An ID/Other File b/tests/with_jdex/id_different_from_jdex/files/00-09 System/01 System Stuff/01.04 An ID/Other File deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/id_different_from_jdex/jdex/00.00 System.md b/tests/with_jdex/id_different_from_jdex/jdex/00.00 System.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/id_different_from_jdex/jdex/01.00 System Stuff.md b/tests/with_jdex/id_different_from_jdex/jdex/01.00 System Stuff.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/id_different_from_jdex/jdex/01.02 A Name.md b/tests/with_jdex/id_different_from_jdex/jdex/01.02 A Name.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/id_different_from_jdex/jdex/01.03 Another ID.md b/tests/with_jdex/id_different_from_jdex/jdex/01.03 Another ID.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/id_different_from_jdex/jdex/01.04 An ID.md b/tests/with_jdex/id_different_from_jdex/jdex/01.04 An ID.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/id_different_from_jdex/result.json b/tests/with_jdex/id_different_from_jdex/result.json deleted file mode 100644 index 76f3864..0000000 --- a/tests/with_jdex/id_different_from_jdex/result.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "errors": [ - { - "error": { - "type": "ID_DIFFERENT_FROM_JDEX", - "id": "01.02", - "jdex_name": "01.02 A Name" - }, - "files": [ - { - "name": "01.02 A Naem", - "nested_under": [ - "00-09 System", - "01 System Stuff" - ] - } - ] - } - ], - "jdex_errors": [] -} diff --git a/tests/with_jdex/id_not_in_jdex/files/00-09 System/01 System Stuff/01.02 Missing ID/.placeholder b/tests/with_jdex/id_not_in_jdex/files/00-09 System/01 System Stuff/01.02 Missing ID/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/id_not_in_jdex/files/00-09 System/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder b/tests/with_jdex/id_not_in_jdex/files/00-09 System/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/id_not_in_jdex/files/00-09 System/01 System Stuff/01.03 Another ID/File Inside ID b/tests/with_jdex/id_not_in_jdex/files/00-09 System/01 System Stuff/01.03 Another ID/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/id_not_in_jdex/jdex/00.00 System.md b/tests/with_jdex/id_not_in_jdex/jdex/00.00 System.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/id_not_in_jdex/jdex/01.00 System Stuff.md b/tests/with_jdex/id_not_in_jdex/jdex/01.00 System Stuff.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/id_not_in_jdex/jdex/01.03 Another ID.md b/tests/with_jdex/id_not_in_jdex/jdex/01.03 Another ID.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/id_not_in_jdex/result.json b/tests/with_jdex/id_not_in_jdex/result.json deleted file mode 100644 index 03f764c..0000000 --- a/tests/with_jdex/id_not_in_jdex/result.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "errors": [ - { - "error": { - "type": "ID_NOT_IN_JDEX", - "id": "01.02" - }, - "files": [ - { - "name": "01.02 Missing ID", - "nested_under": [ - "00-09 System", - "01 System Stuff" - ] - } - ] - } - ], - "jdex_errors": [] -} diff --git a/tests/with_jdex/jdex_area_header_different_from_area/altzeros b/tests/with_jdex/jdex_area_header_different_from_area/altzeros deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_area_header_different_from_area/files/00-09 System/01 Life Admin/01.03 Area Standard Zero/Directory Inside Id/.placeholder b/tests/with_jdex/jdex_area_header_different_from_area/files/00-09 System/01 Life Admin/01.03 Area Standard Zero/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_area_header_different_from_area/files/00-09 System/01 Life Admin/01.03 Area Standard Zero/File Inside ID b/tests/with_jdex/jdex_area_header_different_from_area/files/00-09 System/01 Life Admin/01.03 Area Standard Zero/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_area_header_different_from_area/files/10-19 Life Admin/10 Me, Myself, and I/10.02 An ID/.placeholder b/tests/with_jdex/jdex_area_header_different_from_area/files/10-19 Life Admin/10 Me, Myself, and I/10.02 An ID/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_area_header_different_from_area/jdex/00.00 System Area Management.md b/tests/with_jdex/jdex_area_header_different_from_area/jdex/00.00 System Area Management.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_area_header_different_from_area/jdex/01.00 Life Admin Area Management.md b/tests/with_jdex/jdex_area_header_different_from_area/jdex/01.00 Life Admin Area Management.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_area_header_different_from_area/jdex/01.03 Area Standard Zero.md b/tests/with_jdex/jdex_area_header_different_from_area/jdex/01.03 Area Standard Zero.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_area_header_different_from_area/jdex/10. Life Adminn.md b/tests/with_jdex/jdex_area_header_different_from_area/jdex/10. Life Adminn.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_area_header_different_from_area/jdex/10.00 Me, Myself, and I.md b/tests/with_jdex/jdex_area_header_different_from_area/jdex/10.00 Me, Myself, and I.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_area_header_different_from_area/jdex/10.02 An ID.md b/tests/with_jdex/jdex_area_header_different_from_area/jdex/10.02 An ID.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_area_header_different_from_area/result.json b/tests/with_jdex/jdex_area_header_different_from_area/result.json deleted file mode 100644 index 19e527a..0000000 --- a/tests/with_jdex/jdex_area_header_different_from_area/result.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "errors": [], - "jdex_errors": [ - { - "error": { - "type": "JDEX_AREA_HEADER_DIFFERENT_FROM_AREA", - "area": "1", - "jdex_name": "10-19 Life Admin" - }, - "files": [ - { - "name": "10. Life Adminn.md", - "nested_under": [] - } - ] - } - ] -} diff --git a/tests/with_jdex/jdex_area_header_without_area/altzeros b/tests/with_jdex/jdex_area_header_without_area/altzeros deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_area_header_without_area/files/00-09 System/01 Life Admin/01.03 Area Standard Zero/Directory Inside Id/.placeholder b/tests/with_jdex/jdex_area_header_without_area/files/00-09 System/01 Life Admin/01.03 Area Standard Zero/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_area_header_without_area/files/00-09 System/01 Life Admin/01.03 Area Standard Zero/File Inside ID b/tests/with_jdex/jdex_area_header_without_area/files/00-09 System/01 Life Admin/01.03 Area Standard Zero/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_area_header_without_area/files/10-19 Life Admin/10 Me, Myself, and I/10.02 An ID/.placeholder b/tests/with_jdex/jdex_area_header_without_area/files/10-19 Life Admin/10 Me, Myself, and I/10.02 An ID/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_area_header_without_area/jdex/00.00 System Area Management.md b/tests/with_jdex/jdex_area_header_without_area/jdex/00.00 System Area Management.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_area_header_without_area/jdex/01.00 Life Admin Area Management.md b/tests/with_jdex/jdex_area_header_without_area/jdex/01.00 Life Admin Area Management.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_area_header_without_area/jdex/01.03 Area Standard Zero.md b/tests/with_jdex/jdex_area_header_without_area/jdex/01.03 Area Standard Zero.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_area_header_without_area/jdex/10. Life Admin.md b/tests/with_jdex/jdex_area_header_without_area/jdex/10. Life Admin.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_area_header_without_area/jdex/10.00 Me, Myself, and I.md b/tests/with_jdex/jdex_area_header_without_area/jdex/10.00 Me, Myself, and I.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_area_header_without_area/jdex/10.02 An ID.md b/tests/with_jdex/jdex_area_header_without_area/jdex/10.02 An ID.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_area_header_without_area/jdex/20. Digital Stuff.md b/tests/with_jdex/jdex_area_header_without_area/jdex/20. Digital Stuff.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_area_header_without_area/result.json b/tests/with_jdex/jdex_area_header_without_area/result.json deleted file mode 100644 index 20c86c9..0000000 --- a/tests/with_jdex/jdex_area_header_without_area/result.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "errors": [], - "jdex_errors": [ - { - "error": { - "type": "JDEX_AREA_HEADER_WITHOUT_AREA", - "area": "2" - }, - "files": [ - { - "name": "20. Digital Stuff.md", - "nested_under": [] - } - ] - } - ] -} diff --git a/tests/with_jdex/jdex_category_in_wrong_area/files/00-09 System/01 System Stuff/01.00 An ID/Directory Inside Id/.placeholder b/tests/with_jdex/jdex_category_in_wrong_area/files/00-09 System/01 System Stuff/01.00 An ID/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_category_in_wrong_area/files/00-09 System/01 System Stuff/01.00 An ID/File Inside ID b/tests/with_jdex/jdex_category_in_wrong_area/files/00-09 System/01 System Stuff/01.00 An ID/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_category_in_wrong_area/jdex/00-09 System/01 System Stuff/01.00 An ID.md b/tests/with_jdex/jdex_category_in_wrong_area/jdex/00-09 System/01 System Stuff/01.00 An ID.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_category_in_wrong_area/jdex/00-09 System/11 Whoops/11.01 Inbox.md b/tests/with_jdex/jdex_category_in_wrong_area/jdex/00-09 System/11 Whoops/11.01 Inbox.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_category_in_wrong_area/result.json b/tests/with_jdex/jdex_category_in_wrong_area/result.json deleted file mode 100644 index c7a3ed2..0000000 --- a/tests/with_jdex/jdex_category_in_wrong_area/result.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "errors": [], - "jdex_errors": [ - { - "error": { - "type": "JDEX_CATEGORY_IN_WRONG_AREA", - "category_area": "1", - "file_area": "0" - }, - "files": [ - { - "name": "11 Whoops", - "nested_under": [ - "00-09 System" - ] - } - ] - } - ] -} diff --git a/tests/with_jdex/jdex_duplicate_area_flat/files/00-09 An Area/01 A Category/01.00 An ID/Directory Inside Id/.placeholder b/tests/with_jdex/jdex_duplicate_area_flat/files/00-09 An Area/01 A Category/01.00 An ID/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_duplicate_area_flat/files/00-09 An Area/01 A Category/01.00 An ID/File Inside ID b/tests/with_jdex/jdex_duplicate_area_flat/files/00-09 An Area/01 A Category/01.00 An ID/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_duplicate_area_flat/jdex/00.00 A Reuse.md b/tests/with_jdex/jdex_duplicate_area_flat/jdex/00.00 A Reuse.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_duplicate_area_flat/jdex/00.00 An Area.md b/tests/with_jdex/jdex_duplicate_area_flat/jdex/00.00 An Area.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_duplicate_area_flat/jdex/01.00 A Category.md b/tests/with_jdex/jdex_duplicate_area_flat/jdex/01.00 A Category.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_duplicate_area_flat/jdex/01.01 An ID.md b/tests/with_jdex/jdex_duplicate_area_flat/jdex/01.01 An ID.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_duplicate_area_flat/jdex/02.00 An Id.md b/tests/with_jdex/jdex_duplicate_area_flat/jdex/02.00 An Id.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_duplicate_area_flat/result.json b/tests/with_jdex/jdex_duplicate_area_flat/result.json deleted file mode 100644 index 367406f..0000000 --- a/tests/with_jdex/jdex_duplicate_area_flat/result.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "errors": [], - "jdex_errors": [ - { - "error": { - "area": "0", - "type": "JDEX_DUPLICATE_AREA" - }, - "files": [ - { - "name": "00.00 A Reuse.md", - "nested_under": [] - }, - { - "name": "00.00 An Area.md", - "nested_under": [] - } - ] - }, - { - "error": { - "id": "00.00", - "type": "JDEX_DUPLICATE_ID" - }, - "files": [ - { - "name": "00.00 A Reuse.md", - "nested_under": [] - }, - { - "name": "00.00 An Area.md", - "nested_under": [] - } - ] - } - ] -} diff --git a/tests/with_jdex/jdex_duplicate_area_header/altzeros b/tests/with_jdex/jdex_duplicate_area_header/altzeros deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_duplicate_area_header/files/10-19 An Area/10 A Category/10.02 An Id/Directory Inside Id/.placeholder b/tests/with_jdex/jdex_duplicate_area_header/files/10-19 An Area/10 A Category/10.02 An Id/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_duplicate_area_header/files/10-19 An Area/10 A Category/10.02 An Id/File Inside ID b/tests/with_jdex/jdex_duplicate_area_header/files/10-19 An Area/10 A Category/10.02 An Id/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_duplicate_area_header/jdex/01.00 An Area.md b/tests/with_jdex/jdex_duplicate_area_header/jdex/01.00 An Area.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_duplicate_area_header/jdex/10. A Reuse.md b/tests/with_jdex/jdex_duplicate_area_header/jdex/10. A Reuse.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_duplicate_area_header/jdex/10. An Area.md b/tests/with_jdex/jdex_duplicate_area_header/jdex/10. An Area.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_duplicate_area_header/jdex/10.00 A Category.md b/tests/with_jdex/jdex_duplicate_area_header/jdex/10.00 A Category.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_duplicate_area_header/jdex/10.02 An Id.md b/tests/with_jdex/jdex_duplicate_area_header/jdex/10.02 An Id.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_duplicate_area_header/result.json b/tests/with_jdex/jdex_duplicate_area_header/result.json deleted file mode 100644 index c38030c..0000000 --- a/tests/with_jdex/jdex_duplicate_area_header/result.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "errors": [], - "jdex_errors": [ - { - "error": { - "area": "1", - "type": "JDEX_DUPLICATE_AREA_HEADER" - }, - "files": [ - { - "name": "10. A Reuse.md", - "nested_under": [] - }, - { - "name": "10. An Area.md", - "nested_under": [] - } - ] - } - ] -} diff --git a/tests/with_jdex/jdex_duplicate_area_nested/files/00-09 An Area/01 A Category/01.00 An ID/Directory Inside Id/.placeholder b/tests/with_jdex/jdex_duplicate_area_nested/files/00-09 An Area/01 A Category/01.00 An ID/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_duplicate_area_nested/files/00-09 An Area/01 A Category/01.00 An ID/File Inside ID b/tests/with_jdex/jdex_duplicate_area_nested/files/00-09 An Area/01 A Category/01.00 An ID/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_duplicate_area_nested/jdex/00-09 A Reuse/02 Another Category/02.00 An Id b/tests/with_jdex/jdex_duplicate_area_nested/jdex/00-09 A Reuse/02 Another Category/02.00 An Id deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_duplicate_area_nested/jdex/00-09 An Area/01 A Category/01.00 An ID b/tests/with_jdex/jdex_duplicate_area_nested/jdex/00-09 An Area/01 A Category/01.00 An ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_duplicate_area_nested/result.json b/tests/with_jdex/jdex_duplicate_area_nested/result.json deleted file mode 100644 index 1552368..0000000 --- a/tests/with_jdex/jdex_duplicate_area_nested/result.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "errors": [], - "jdex_errors": [ - { - "error": { - "area": "0", - "type": "JDEX_DUPLICATE_AREA" - }, - "files": [ - { - "name": "00-09 A Reuse", - "nested_under": [] - }, - { - "name": "00-09 An Area", - "nested_under": [] - } - ] - } - ] -} diff --git a/tests/with_jdex/jdex_duplicate_category_flat/files/00-09 An Area/01 A Category/01.00 An ID/Directory Inside Id/.placeholder b/tests/with_jdex/jdex_duplicate_category_flat/files/00-09 An Area/01 A Category/01.00 An ID/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_duplicate_category_flat/files/00-09 An Area/01 A Category/01.00 An ID/File Inside ID b/tests/with_jdex/jdex_duplicate_category_flat/files/00-09 An Area/01 A Category/01.00 An ID/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_duplicate_category_flat/jdex/00.00 An Area.md b/tests/with_jdex/jdex_duplicate_category_flat/jdex/00.00 An Area.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_duplicate_category_flat/jdex/01.00 A Category.md b/tests/with_jdex/jdex_duplicate_category_flat/jdex/01.00 A Category.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_duplicate_category_flat/jdex/01.00 A Reuse.md b/tests/with_jdex/jdex_duplicate_category_flat/jdex/01.00 A Reuse.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_duplicate_category_flat/jdex/01.01 An ID.md b/tests/with_jdex/jdex_duplicate_category_flat/jdex/01.01 An ID.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_duplicate_category_flat/jdex/02.00 An Id.md b/tests/with_jdex/jdex_duplicate_category_flat/jdex/02.00 An Id.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_duplicate_category_flat/result.json b/tests/with_jdex/jdex_duplicate_category_flat/result.json deleted file mode 100644 index 97e94ef..0000000 --- a/tests/with_jdex/jdex_duplicate_category_flat/result.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "errors": [], - "jdex_errors": [ - { - "error": { - "category": "01", - "type": "JDEX_DUPLICATE_CATEGORY" - }, - "files": [ - { - "name": "01.00 A Category.md", - "nested_under": [] - }, - { - "name": "01.00 A Reuse.md", - "nested_under": [] - } - ] - }, - { - "error": { - "id": "01.00", - "type": "JDEX_DUPLICATE_ID" - }, - "files": [ - { - "name": "01.00 A Category.md", - "nested_under": [] - }, - { - "name": "01.00 A Reuse.md", - "nested_under": [] - } - ] - } - ] -} diff --git a/tests/with_jdex/jdex_duplicate_category_nested/files/00-09 An Area/01 A Category/01.00 An ID/Directory Inside Id/.placeholder b/tests/with_jdex/jdex_duplicate_category_nested/files/00-09 An Area/01 A Category/01.00 An ID/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_duplicate_category_nested/files/00-09 An Area/01 A Category/01.00 An ID/File Inside ID b/tests/with_jdex/jdex_duplicate_category_nested/files/00-09 An Area/01 A Category/01.00 An ID/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_duplicate_category_nested/jdex/00-09 An Area/01 A Category/01.00 An ID b/tests/with_jdex/jdex_duplicate_category_nested/jdex/00-09 An Area/01 A Category/01.00 An ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_duplicate_category_nested/jdex/00-09 An Area/01 A Reuse/01.02 Another ID b/tests/with_jdex/jdex_duplicate_category_nested/jdex/00-09 An Area/01 A Reuse/01.02 Another ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_duplicate_category_nested/result.json b/tests/with_jdex/jdex_duplicate_category_nested/result.json deleted file mode 100644 index d349568..0000000 --- a/tests/with_jdex/jdex_duplicate_category_nested/result.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "errors": [], - "jdex_errors": [ - { - "error": { - "category": "01", - "type": "JDEX_DUPLICATE_CATEGORY" - }, - "files": [ - { - "name": "01 A Category", - "nested_under": [ - "00-09 An Area" - ] - }, - { - "name": "01 A Reuse", - "nested_under": [ - "00-09 An Area" - ] - } - ] - } - ] -} diff --git a/tests/with_jdex/jdex_duplicate_id_flat/files/00-09 An Area/01 A Category/01.00 An ID/Directory Inside Id/.placeholder b/tests/with_jdex/jdex_duplicate_id_flat/files/00-09 An Area/01 A Category/01.00 An ID/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_duplicate_id_flat/files/00-09 An Area/01 A Category/01.00 An ID/File Inside ID b/tests/with_jdex/jdex_duplicate_id_flat/files/00-09 An Area/01 A Category/01.00 An ID/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_duplicate_id_flat/jdex/00.00 An Area.md b/tests/with_jdex/jdex_duplicate_id_flat/jdex/00.00 An Area.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_duplicate_id_flat/jdex/01.00 A Category.md b/tests/with_jdex/jdex_duplicate_id_flat/jdex/01.00 A Category.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_duplicate_id_flat/jdex/01.01 A Reuse.md b/tests/with_jdex/jdex_duplicate_id_flat/jdex/01.01 A Reuse.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_duplicate_id_flat/jdex/01.01 An ID.md b/tests/with_jdex/jdex_duplicate_id_flat/jdex/01.01 An ID.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_duplicate_id_flat/result.json b/tests/with_jdex/jdex_duplicate_id_flat/result.json deleted file mode 100644 index e9965a3..0000000 --- a/tests/with_jdex/jdex_duplicate_id_flat/result.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "errors": [], - "jdex_errors": [ - { - "error": { - "id": "01.01", - "type": "JDEX_DUPLICATE_ID" - }, - "files": [ - { - "name": "01.01 A Reuse.md", - "nested_under": [] - }, - { - "name": "01.01 An ID.md", - "nested_under": [] - } - ] - } - ] -} diff --git a/tests/with_jdex/jdex_duplicate_id_nested/files/00-09 An Area/01 A Category/01.00 An ID/Directory Inside Id/.placeholder b/tests/with_jdex/jdex_duplicate_id_nested/files/00-09 An Area/01 A Category/01.00 An ID/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_duplicate_id_nested/files/00-09 An Area/01 A Category/01.00 An ID/File Inside ID b/tests/with_jdex/jdex_duplicate_id_nested/files/00-09 An Area/01 A Category/01.00 An ID/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_duplicate_id_nested/jdex/00-09 An Area/01 A Category/01.01 A Reuse.md b/tests/with_jdex/jdex_duplicate_id_nested/jdex/00-09 An Area/01 A Category/01.01 A Reuse.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_duplicate_id_nested/jdex/00-09 An Area/01 A Category/01.01 An ID.md b/tests/with_jdex/jdex_duplicate_id_nested/jdex/00-09 An Area/01 A Category/01.01 An ID.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_duplicate_id_nested/result.json b/tests/with_jdex/jdex_duplicate_id_nested/result.json deleted file mode 100644 index 52722ac..0000000 --- a/tests/with_jdex/jdex_duplicate_id_nested/result.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "errors": [], - "jdex_errors": [ - { - "error": { - "id": "01.01", - "type": "JDEX_DUPLICATE_ID" - }, - "files": [ - { - "name": "01.01 A Reuse.md", - "nested_under": [ - "00-09 An Area", - "01 A Category" - ] - }, - { - "name": "01.01 An ID.md", - "nested_under": [ - "00-09 An Area", - "01 A Category" - ] - } - ] - } - ] -} diff --git a/tests/with_jdex/jdex_file_outside_category/files/00-09 System/01 System Stuff/01.02 A Name/.placeholder b/tests/with_jdex/jdex_file_outside_category/files/00-09 System/01 System Stuff/01.02 A Name/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_file_outside_category/files/00-09 System/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder b/tests/with_jdex/jdex_file_outside_category/files/00-09 System/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_file_outside_category/files/00-09 System/01 System Stuff/01.03 Another ID/File Inside ID b/tests/with_jdex/jdex_file_outside_category/files/00-09 System/01 System Stuff/01.03 Another ID/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_file_outside_category/jdex/00-09 System/01 System Stuff/01.02 A Name.md b/tests/with_jdex/jdex_file_outside_category/jdex/00-09 System/01 System Stuff/01.02 A Name.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_file_outside_category/jdex/00-09 System/01 System Stuff/01.03 Another ID.md b/tests/with_jdex/jdex_file_outside_category/jdex/00-09 System/01 System Stuff/01.03 Another ID.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_file_outside_category/jdex/00-09 System/Nor here b/tests/with_jdex/jdex_file_outside_category/jdex/00-09 System/Nor here deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_file_outside_category/jdex/Not here b/tests/with_jdex/jdex_file_outside_category/jdex/Not here deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_file_outside_category/result.json b/tests/with_jdex/jdex_file_outside_category/result.json deleted file mode 100644 index 0246b5a..0000000 --- a/tests/with_jdex/jdex_file_outside_category/result.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "errors": [], - "jdex_errors": [ - { - "error": { - "type": "JDEX_FILE_OUTSIDE_CATEGORY" - }, - "files": [ - { - "name": "Not here", - "nested_under": [] - } - ] - }, - { - "error": { - "type": "JDEX_FILE_OUTSIDE_CATEGORY" - }, - "files": [ - { - "name": "Nor here", - "nested_under": [ - "00-09 System" - ] - } - ] - } - ] -} diff --git a/tests/with_jdex/jdex_id_in_wrong_category/files/00-09 System/01 System Stuff/01.00 An ID/Directory Inside Id/.placeholder b/tests/with_jdex/jdex_id_in_wrong_category/files/00-09 System/01 System Stuff/01.00 An ID/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_id_in_wrong_category/files/00-09 System/01 System Stuff/01.00 An ID/File Inside ID b/tests/with_jdex/jdex_id_in_wrong_category/files/00-09 System/01 System Stuff/01.00 An ID/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_id_in_wrong_category/jdex/00-09 System/01 System Stuff/01.00 An ID.md b/tests/with_jdex/jdex_id_in_wrong_category/jdex/00-09 System/01 System Stuff/01.00 An ID.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_id_in_wrong_category/jdex/00-09 System/01 System Stuff/02.00 Whoops.md b/tests/with_jdex/jdex_id_in_wrong_category/jdex/00-09 System/01 System Stuff/02.00 Whoops.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_id_in_wrong_category/result.json b/tests/with_jdex/jdex_id_in_wrong_category/result.json deleted file mode 100644 index edda4f2..0000000 --- a/tests/with_jdex/jdex_id_in_wrong_category/result.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "errors": [], - "jdex_errors": [ - { - "error": { - "type": "JDEX_ID_IN_WRONG_CATEGORY", - "id_ac": "02", - "file_ac": "01" - }, - "files": [ - { - "name": "02.00 Whoops.md", - "nested_under": [ - "00-09 System", - "01 System Stuff" - ] - } - ] - } - ] -} diff --git a/tests/with_jdex/jdex_in_folders/files/00-09 System/01 System Stuff/01.02 A Naem/.placeholder b/tests/with_jdex/jdex_in_folders/files/00-09 System/01 System Stuff/01.02 A Naem/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_in_folders/files/00-09 System/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder b/tests/with_jdex/jdex_in_folders/files/00-09 System/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_in_folders/files/00-09 System/01 System Stuff/01.03 Another ID/File Inside ID b/tests/with_jdex/jdex_in_folders/files/00-09 System/01 System Stuff/01.03 Another ID/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_in_folders/jdex/00-09 System/01 System Stuff/01.02 A Name.md b/tests/with_jdex/jdex_in_folders/jdex/00-09 System/01 System Stuff/01.02 A Name.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_in_folders/jdex/00-09 System/01 System Stuff/01.03 Another ID.md b/tests/with_jdex/jdex_in_folders/jdex/00-09 System/01 System Stuff/01.03 Another ID.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_in_folders/result.json b/tests/with_jdex/jdex_in_folders/result.json deleted file mode 100644 index 76f3864..0000000 --- a/tests/with_jdex/jdex_in_folders/result.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "errors": [ - { - "error": { - "type": "ID_DIFFERENT_FROM_JDEX", - "id": "01.02", - "jdex_name": "01.02 A Name" - }, - "files": [ - { - "name": "01.02 A Naem", - "nested_under": [ - "00-09 System", - "01 System Stuff" - ] - } - ] - } - ], - "jdex_errors": [] -} diff --git a/tests/with_jdex/jdex_invalid_area_name/files/00-09 System/01 System Stuff/01.02 A Name/.placeholder b/tests/with_jdex/jdex_invalid_area_name/files/00-09 System/01 System Stuff/01.02 A Name/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_invalid_area_name/files/00-09 System/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder b/tests/with_jdex/jdex_invalid_area_name/files/00-09 System/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_invalid_area_name/files/00-09 System/01 System Stuff/01.03 Another ID/File Inside ID b/tests/with_jdex/jdex_invalid_area_name/files/00-09 System/01 System Stuff/01.03 Another ID/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_invalid_area_name/jdex/00-09 System/01 System Stuff/01.02 A Name.md b/tests/with_jdex/jdex_invalid_area_name/jdex/00-09 System/01 System Stuff/01.02 A Name.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_invalid_area_name/jdex/00-09 System/01 System Stuff/01.03 Another ID.md b/tests/with_jdex/jdex_invalid_area_name/jdex/00-09 System/01 System Stuff/01.03 Another ID.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_invalid_area_name/jdex/10-18 Malformed Numbers/.placeholder b/tests/with_jdex/jdex_invalid_area_name/jdex/10-18 Malformed Numbers/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_invalid_area_name/jdex/No Numbers/.placeholder b/tests/with_jdex/jdex_invalid_area_name/jdex/No Numbers/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_invalid_area_name/result.json b/tests/with_jdex/jdex_invalid_area_name/result.json deleted file mode 100644 index fc648ac..0000000 --- a/tests/with_jdex/jdex_invalid_area_name/result.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "errors": [], - "jdex_errors": [ - { - "error": { - "type": "JDEX_INVALID_AREA_NAME" - }, - "files": [ - { - "name": "10-18 Malformed Numbers", - "nested_under": [] - } - ] - }, - { - "error": { - "type": "JDEX_INVALID_AREA_NAME" - }, - "files": [ - { - "name": "No Numbers", - "nested_under": [] - } - ] - } - ] -} diff --git a/tests/with_jdex/jdex_invalid_category_name/files/00-09 System/01 System Stuff/01.02 A Name/.placeholder b/tests/with_jdex/jdex_invalid_category_name/files/00-09 System/01 System Stuff/01.02 A Name/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_invalid_category_name/files/00-09 System/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder b/tests/with_jdex/jdex_invalid_category_name/files/00-09 System/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_invalid_category_name/files/00-09 System/01 System Stuff/01.03 Another ID/File Inside ID b/tests/with_jdex/jdex_invalid_category_name/files/00-09 System/01 System Stuff/01.03 Another ID/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_invalid_category_name/jdex/00-09 System/01 System Stuff/01.02 A Name.md b/tests/with_jdex/jdex_invalid_category_name/jdex/00-09 System/01 System Stuff/01.02 A Name.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_invalid_category_name/jdex/00-09 System/01 System Stuff/01.03 Another ID.md b/tests/with_jdex/jdex_invalid_category_name/jdex/00-09 System/01 System Stuff/01.03 Another ID.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_invalid_category_name/jdex/00-09 System/2 Malformed Numbers/.placeholder b/tests/with_jdex/jdex_invalid_category_name/jdex/00-09 System/2 Malformed Numbers/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_invalid_category_name/jdex/00-09 System/No Numbers/.placeholder b/tests/with_jdex/jdex_invalid_category_name/jdex/00-09 System/No Numbers/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_invalid_category_name/result.json b/tests/with_jdex/jdex_invalid_category_name/result.json deleted file mode 100644 index 9ff4ed6..0000000 --- a/tests/with_jdex/jdex_invalid_category_name/result.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "errors": [], - "jdex_errors": [ - { - "error": { - "type": "JDEX_INVALID_CATEGORY_NAME" - }, - "files": [ - { - "name": "2 Malformed Numbers", - "nested_under": [ - "00-09 System" - ] - } - ] - }, - { - "error": { - "type": "JDEX_INVALID_CATEGORY_NAME" - }, - "files": [ - { - "name": "No Numbers", - "nested_under": [ - "00-09 System" - ] - } - ] - } - ] -} diff --git a/tests/with_jdex/jdex_invalid_id_name_flat/files/00-09 System/01 System Stuff/01.02 A Name/.placeholder b/tests/with_jdex/jdex_invalid_id_name_flat/files/00-09 System/01 System Stuff/01.02 A Name/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_invalid_id_name_flat/files/00-09 System/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder b/tests/with_jdex/jdex_invalid_id_name_flat/files/00-09 System/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_invalid_id_name_flat/files/00-09 System/01 System Stuff/01.03 Another ID/File Inside ID b/tests/with_jdex/jdex_invalid_id_name_flat/files/00-09 System/01 System Stuff/01.03 Another ID/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_invalid_id_name_flat/files/00-09 System/01 System Stuff/01.04 An ID/Other File b/tests/with_jdex/jdex_invalid_id_name_flat/files/00-09 System/01 System Stuff/01.04 An ID/Other File deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_invalid_id_name_flat/jdex/00.00 System.md b/tests/with_jdex/jdex_invalid_id_name_flat/jdex/00.00 System.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_invalid_id_name_flat/jdex/01.00 System Stuff.md b/tests/with_jdex/jdex_invalid_id_name_flat/jdex/01.00 System Stuff.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_invalid_id_name_flat/jdex/01.02 A Name.md b/tests/with_jdex/jdex_invalid_id_name_flat/jdex/01.02 A Name.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_invalid_id_name_flat/jdex/01.03 Another ID.md b/tests/with_jdex/jdex_invalid_id_name_flat/jdex/01.03 Another ID.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_invalid_id_name_flat/jdex/01.04 An ID.md b/tests/with_jdex/jdex_invalid_id_name_flat/jdex/01.04 An ID.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_invalid_id_name_flat/jdex/01.5 Malformed Numbers.md b/tests/with_jdex/jdex_invalid_id_name_flat/jdex/01.5 Malformed Numbers.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_invalid_id_name_flat/jdex/No Numbers.md b/tests/with_jdex/jdex_invalid_id_name_flat/jdex/No Numbers.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_invalid_id_name_flat/result.json b/tests/with_jdex/jdex_invalid_id_name_flat/result.json deleted file mode 100644 index 00230e1..0000000 --- a/tests/with_jdex/jdex_invalid_id_name_flat/result.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "errors": [], - "jdex_errors": [ - { - "error": { - "type": "JDEX_INVALID_ID_NAME" - }, - "files": [ - { - "name": "01.5 Malformed Numbers.md", - "nested_under": [] - } - ] - }, - { - "error": { - "type": "JDEX_INVALID_ID_NAME" - }, - "files": [ - { - "name": "No Numbers.md", - "nested_under": [] - } - ] - } - ] -} diff --git a/tests/with_jdex/jdex_invalid_id_name_nested/files/00-09 System/01 System Stuff/01.02 A Name/.placeholder b/tests/with_jdex/jdex_invalid_id_name_nested/files/00-09 System/01 System Stuff/01.02 A Name/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_invalid_id_name_nested/files/00-09 System/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder b/tests/with_jdex/jdex_invalid_id_name_nested/files/00-09 System/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_invalid_id_name_nested/files/00-09 System/01 System Stuff/01.03 Another ID/File Inside ID b/tests/with_jdex/jdex_invalid_id_name_nested/files/00-09 System/01 System Stuff/01.03 Another ID/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_invalid_id_name_nested/jdex/00-09 System/01 System Stuff/01.02 A Name.md b/tests/with_jdex/jdex_invalid_id_name_nested/jdex/00-09 System/01 System Stuff/01.02 A Name.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_invalid_id_name_nested/jdex/00-09 System/01 System Stuff/01.03 Another ID.md b/tests/with_jdex/jdex_invalid_id_name_nested/jdex/00-09 System/01 System Stuff/01.03 Another ID.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_invalid_id_name_nested/jdex/00-09 System/01 System Stuff/01.4 Malformed Numbers.md b/tests/with_jdex/jdex_invalid_id_name_nested/jdex/00-09 System/01 System Stuff/01.4 Malformed Numbers.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_invalid_id_name_nested/jdex/00-09 System/01 System Stuff/No ID.md b/tests/with_jdex/jdex_invalid_id_name_nested/jdex/00-09 System/01 System Stuff/No ID.md deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/jdex_invalid_id_name_nested/result.json b/tests/with_jdex/jdex_invalid_id_name_nested/result.json deleted file mode 100644 index f1cb4c1..0000000 --- a/tests/with_jdex/jdex_invalid_id_name_nested/result.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "errors": [], - "jdex_errors": [ - { - "error": { - "type": "JDEX_INVALID_ID_NAME" - }, - "files": [ - { - "name": "01.4 Malformed Numbers.md", - "nested_under": [ - "00-09 System", - "01 System Stuff" - ] - } - ] - }, - { - "error": { - "type": "JDEX_INVALID_ID_NAME" - }, - "files": [ - { - "name": "No ID.md", - "nested_under": [ - "00-09 System", - "01 System Stuff" - ] - } - ] - } - ] -} diff --git a/tests/with_jdex/single_file_jdex/files/00-09 System/01 System Stuff/01.02 A Naem/.placeholder b/tests/with_jdex/single_file_jdex/files/00-09 System/01 System Stuff/01.02 A Naem/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/single_file_jdex/files/00-09 System/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder b/tests/with_jdex/single_file_jdex/files/00-09 System/01 System Stuff/01.03 Another ID/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/single_file_jdex/files/00-09 System/01 System Stuff/01.03 Another ID/File Inside ID b/tests/with_jdex/single_file_jdex/files/00-09 System/01 System Stuff/01.03 Another ID/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/with_jdex/single_file_jdex/jdex b/tests/with_jdex/single_file_jdex/jdex deleted file mode 100644 index ca83f0e..0000000 --- a/tests/with_jdex/single_file_jdex/jdex +++ /dev/null @@ -1,12 +0,0 @@ -00-09 System // which I can comment like this // and a nested one - 01 System Stuff//Comments? - // More comments - 01.02 A Name - - Location: work email. - /* A - block comment - */ - - - -01.03 Another ID // With a trailing comment diff --git a/tests/with_jdex/single_file_jdex/result.json b/tests/with_jdex/single_file_jdex/result.json deleted file mode 100644 index 76f3864..0000000 --- a/tests/with_jdex/single_file_jdex/result.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "errors": [ - { - "error": { - "type": "ID_DIFFERENT_FROM_JDEX", - "id": "01.02", - "jdex_name": "01.02 A Name" - }, - "files": [ - { - "name": "01.02 A Naem", - "nested_under": [ - "00-09 System", - "01 System Stuff" - ] - } - ] - } - ], - "jdex_errors": [] -} diff --git a/tests/without_jdex/category_in_wrong_area/files/00-09 System/01 System Stuff/01.00 An ID/Directory Inside Id/.placeholder b/tests/without_jdex/category_in_wrong_area/files/00-09 System/01 System Stuff/01.00 An ID/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/category_in_wrong_area/files/00-09 System/01 System Stuff/01.00 An ID/File Inside ID b/tests/without_jdex/category_in_wrong_area/files/00-09 System/01 System Stuff/01.00 An ID/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/category_in_wrong_area/files/00-09 System/11 Whoops/11.01 Inbox/.placeholder b/tests/without_jdex/category_in_wrong_area/files/00-09 System/11 Whoops/11.01 Inbox/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/category_in_wrong_area/result.json b/tests/without_jdex/category_in_wrong_area/result.json deleted file mode 100644 index c5e1bf8..0000000 --- a/tests/without_jdex/category_in_wrong_area/result.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "errors": [ - { - "error": { - "type": "CATEGORY_IN_WRONG_AREA", - "category_area": "1", - "file_area": "0" - }, - "files": [ - { - "name": "11 Whoops", - "nested_under": [ - "00-09 System" - ] - } - ] - } - ], - "jdex_errors": [] -} diff --git a/tests/without_jdex/duplicate_area/files/00-09 A Reuse/02 Another Category/02.00 An ID/Directory Inside Id/.placeholder b/tests/without_jdex/duplicate_area/files/00-09 A Reuse/02 Another Category/02.00 An ID/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/duplicate_area/files/00-09 A Reuse/02 Another Category/02.00 An ID/File Inside ID b/tests/without_jdex/duplicate_area/files/00-09 A Reuse/02 Another Category/02.00 An ID/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/duplicate_area/files/00-09 An Area/01 A Category/01.00 An ID/Directory Inside Id/.placeholder b/tests/without_jdex/duplicate_area/files/00-09 An Area/01 A Category/01.00 An ID/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/duplicate_area/files/00-09 An Area/01 A Category/01.00 An ID/File Inside ID b/tests/without_jdex/duplicate_area/files/00-09 An Area/01 A Category/01.00 An ID/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/duplicate_area/result.json b/tests/without_jdex/duplicate_area/result.json deleted file mode 100644 index ed17559..0000000 --- a/tests/without_jdex/duplicate_area/result.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "errors": [ - { - "error": { - "area": "0", - "type": "DUPLICATE_AREA" - }, - "files": [ - { - "name": "00-09 A Reuse", - "nested_under": [] - }, - { - "name": "00-09 An Area", - "nested_under": [] - } - ] - } - ], - "jdex_errors": [] -} diff --git a/tests/without_jdex/duplicate_category/files/00-09 System/01 A Category/01.00 An ID/Directory Inside Id/.placeholder b/tests/without_jdex/duplicate_category/files/00-09 System/01 A Category/01.00 An ID/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/duplicate_category/files/00-09 System/01 A Category/01.00 An ID/File Inside ID b/tests/without_jdex/duplicate_category/files/00-09 System/01 A Category/01.00 An ID/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/duplicate_category/files/00-09 System/01 A Reuse/01.02 Another ID/Directory Inside Id/.placeholder b/tests/without_jdex/duplicate_category/files/00-09 System/01 A Reuse/01.02 Another ID/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/duplicate_category/files/00-09 System/01 A Reuse/01.02 Another ID/File Inside ID b/tests/without_jdex/duplicate_category/files/00-09 System/01 A Reuse/01.02 Another ID/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/duplicate_category/result.json b/tests/without_jdex/duplicate_category/result.json deleted file mode 100644 index ea222e5..0000000 --- a/tests/without_jdex/duplicate_category/result.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "errors": [ - { - "error": { - "type": "DUPLICATE_CATEGORY", - "category": "01" - }, - "files": [ - { - "name": "01 A Category", - "nested_under": [ - "00-09 System" - ] - }, - { - "name": "01 A Reuse", - "nested_under": [ - "00-09 System" - ] - } - ] - } - ], - "jdex_errors": [] -} diff --git a/tests/without_jdex/duplicate_id/files/00-09 System/01 System Stuff/01.00 A Reuse/Other File b/tests/without_jdex/duplicate_id/files/00-09 System/01 System Stuff/01.00 A Reuse/Other File deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/duplicate_id/files/00-09 System/01 System Stuff/01.00 An ID/Directory Inside Id/.placeholder b/tests/without_jdex/duplicate_id/files/00-09 System/01 System Stuff/01.00 An ID/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/duplicate_id/files/00-09 System/01 System Stuff/01.00 An ID/File Inside ID b/tests/without_jdex/duplicate_id/files/00-09 System/01 System Stuff/01.00 An ID/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/duplicate_id/result.json b/tests/without_jdex/duplicate_id/result.json deleted file mode 100644 index 84eb334..0000000 --- a/tests/without_jdex/duplicate_id/result.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "errors": [ - { - "error": { - "type": "DUPLICATE_ID", - "id": "01.00" - }, - "files": [ - { - "name": "01.00 A Reuse", - "nested_under": [ - "00-09 System", - "01 System Stuff" - ] - }, - { - "name": "01.00 An ID", - "nested_under": [ - "00-09 System", - "01 System Stuff" - ] - } - ] - } - ], - "jdex_errors": [] -} diff --git a/tests/without_jdex/file_outside_id/files/00-09 System/01 System Stuff/01.00 An ID/Directory Inside Id/.placeholder b/tests/without_jdex/file_outside_id/files/00-09 System/01 System Stuff/01.00 An ID/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/file_outside_id/files/00-09 System/01 System Stuff/01.00 An ID/File Inside ID b/tests/without_jdex/file_outside_id/files/00-09 System/01 System Stuff/01.00 An ID/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/file_outside_id/files/00-09 System/01 System Stuff/File Outside Id b/tests/without_jdex/file_outside_id/files/00-09 System/01 System Stuff/File Outside Id deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/file_outside_id/files/00-09 System/File Outside Category b/tests/without_jdex/file_outside_id/files/00-09 System/File Outside Category deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/file_outside_id/files/File Outside Area b/tests/without_jdex/file_outside_id/files/File Outside Area deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/file_outside_id/result.json b/tests/without_jdex/file_outside_id/result.json deleted file mode 100644 index 29718ac..0000000 --- a/tests/without_jdex/file_outside_id/result.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "errors": [ - { - "error": { - "type": "FILE_OUTSIDE_ID" - }, - "files": [ - { - "name": "File Outside Area", - "nested_under": [] - } - ] - }, - { - "error": { - "type": "FILE_OUTSIDE_ID" - }, - "files": [ - { - "name": "File Outside Category", - "nested_under": [ - "00-09 System" - ] - } - ] - }, - { - "error": { - "type": "FILE_OUTSIDE_ID" - }, - "files": [ - { - "name": "File Outside Id", - "nested_under": [ - "00-09 System", - "01 System Stuff" - ] - } - ] - } - ], - "jdex_errors": [] -} diff --git a/tests/without_jdex/id_in_wrong_category/files/00-09 System/01 System Stuff/01.00 An ID/Directory Inside Id/.placeholder b/tests/without_jdex/id_in_wrong_category/files/00-09 System/01 System Stuff/01.00 An ID/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/id_in_wrong_category/files/00-09 System/01 System Stuff/01.00 An ID/File Inside ID b/tests/without_jdex/id_in_wrong_category/files/00-09 System/01 System Stuff/01.00 An ID/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/id_in_wrong_category/files/00-09 System/01 System Stuff/11.01 Whoops/.placeholder b/tests/without_jdex/id_in_wrong_category/files/00-09 System/01 System Stuff/11.01 Whoops/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/id_in_wrong_category/result.json b/tests/without_jdex/id_in_wrong_category/result.json deleted file mode 100644 index 5ddcf4c..0000000 --- a/tests/without_jdex/id_in_wrong_category/result.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "errors": [ - { - "error": { - "type": "ID_IN_WRONG_CATEGORY", - "id_ac": "11", - "file_ac": "01" - }, - "files": [ - { - "name": "11.01 Whoops", - "nested_under": [ - "00-09 System", - "01 System Stuff" - ] - } - ] - } - ], - "jdex_errors": [] -} diff --git a/tests/without_jdex/ignore_files/files/.ignoreme b/tests/without_jdex/ignore_files/files/.ignoreme deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/ignore_files/files/00-09 System/01 System Stuff/.ignoreme2 b/tests/without_jdex/ignore_files/files/00-09 System/01 System Stuff/.ignoreme2 deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/ignore_files/files/00-09 System/01 System Stuff/01.00 An ID/Directory Inside Id/.placeholder b/tests/without_jdex/ignore_files/files/00-09 System/01 System Stuff/01.00 An ID/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/ignore_files/files/00-09 System/01 System Stuff/01.00 An ID/File Inside ID b/tests/without_jdex/ignore_files/files/00-09 System/01 System Stuff/01.00 An ID/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/ignore_files/files/00-09 System/01 System Stuff/ignoreme b/tests/without_jdex/ignore_files/files/00-09 System/01 System Stuff/ignoreme deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/ignore_files/files/00-09 System/ignoreme b/tests/without_jdex/ignore_files/files/00-09 System/ignoreme deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/ignore_files/ignore b/tests/without_jdex/ignore_files/ignore deleted file mode 100644 index 0c9b690..0000000 --- a/tests/without_jdex/ignore_files/ignore +++ /dev/null @@ -1,2 +0,0 @@ -.* -*/ignoreme diff --git a/tests/without_jdex/ignore_files/result.json b/tests/without_jdex/ignore_files/result.json deleted file mode 100644 index 169672d..0000000 --- a/tests/without_jdex/ignore_files/result.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "errors": [], - "jdex_errors": [] -} diff --git a/tests/without_jdex/invalid_area_name/files/00-09 System/01 System Stuff/01.00 An ID/Directory Inside Id/.placeholder b/tests/without_jdex/invalid_area_name/files/00-09 System/01 System Stuff/01.00 An ID/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/invalid_area_name/files/00-09 System/01 System Stuff/01.00 An ID/File Inside ID b/tests/without_jdex/invalid_area_name/files/00-09 System/01 System Stuff/01.00 An ID/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/invalid_area_name/files/10-18 Malformed/.placeholder b/tests/without_jdex/invalid_area_name/files/10-18 Malformed/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/invalid_area_name/files/No ID/.placeholder b/tests/without_jdex/invalid_area_name/files/No ID/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/invalid_area_name/result.json b/tests/without_jdex/invalid_area_name/result.json deleted file mode 100644 index e607ff7..0000000 --- a/tests/without_jdex/invalid_area_name/result.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "errors": [ - { - "error": { - "type": "INVALID_AREA_NAME" - }, - "files": [ - { - "name": "10-18 Malformed", - "nested_under": [] - } - ] - }, - { - "error": { - "type": "INVALID_AREA_NAME" - }, - "files": [ - { - "name": "No ID", - "nested_under": [] - } - ] - } - ], - "jdex_errors": [] -} diff --git a/tests/without_jdex/invalid_category_name/files/00-09 System/01 System Stuff/01.00 An ID/Directory Inside Id/.placeholder b/tests/without_jdex/invalid_category_name/files/00-09 System/01 System Stuff/01.00 An ID/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/invalid_category_name/files/00-09 System/01 System Stuff/01.00 An ID/File Inside ID b/tests/without_jdex/invalid_category_name/files/00-09 System/01 System Stuff/01.00 An ID/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/invalid_category_name/files/00-09 System/2 Malformed/.placeholder b/tests/without_jdex/invalid_category_name/files/00-09 System/2 Malformed/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/invalid_category_name/files/00-09 System/No ID/.placeholder b/tests/without_jdex/invalid_category_name/files/00-09 System/No ID/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/invalid_category_name/result.json b/tests/without_jdex/invalid_category_name/result.json deleted file mode 100644 index da84288..0000000 --- a/tests/without_jdex/invalid_category_name/result.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "errors": [ - { - "error": { - "type": "INVALID_CATEGORY_NAME" - }, - "files": [ - { - "name": "2 Malformed", - "nested_under": [ - "00-09 System" - ] - } - ] - }, - { - "error": { - "type": "INVALID_CATEGORY_NAME" - }, - "files": [ - { - "name": "No ID", - "nested_under": [ - "00-09 System" - ] - } - ] - } - ], - "jdex_errors": [] -} diff --git a/tests/without_jdex/invalid_id_name/files/00-09 System/01 System Stuff/01.00 An ID/Directory Inside Id/.placeholder b/tests/without_jdex/invalid_id_name/files/00-09 System/01 System Stuff/01.00 An ID/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/invalid_id_name/files/00-09 System/01 System Stuff/01.00 An ID/File Inside ID b/tests/without_jdex/invalid_id_name/files/00-09 System/01 System Stuff/01.00 An ID/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/invalid_id_name/files/00-09 System/01 System Stuff/01.1 Malformed/.placeholder b/tests/without_jdex/invalid_id_name/files/00-09 System/01 System Stuff/01.1 Malformed/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/invalid_id_name/files/00-09 System/01 System Stuff/No ID/.placeholder b/tests/without_jdex/invalid_id_name/files/00-09 System/01 System Stuff/No ID/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/invalid_id_name/result.json b/tests/without_jdex/invalid_id_name/result.json deleted file mode 100644 index 8e5524f..0000000 --- a/tests/without_jdex/invalid_id_name/result.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "errors": [ - { - "error": { - "type": "INVALID_ID_NAME" - }, - "files": [ - { - "name": "01.1 Malformed", - "nested_under": [ - "00-09 System", - "01 System Stuff" - ] - } - ] - }, - { - "error": { - "type": "INVALID_ID_NAME" - }, - "files": [ - { - "name": "No ID", - "nested_under": [ - "00-09 System", - "01 System Stuff" - ] - } - ] - } - ], - "jdex_errors": [] -} diff --git a/tests/without_jdex/nonempty_inbox/files/00-09 System/01 System Stuff/01.00 An ID/Directory Inside Id/.placeholder b/tests/without_jdex/nonempty_inbox/files/00-09 System/01 System Stuff/01.00 An ID/Directory Inside Id/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/nonempty_inbox/files/00-09 System/01 System Stuff/01.00 An ID/File Inside ID b/tests/without_jdex/nonempty_inbox/files/00-09 System/01 System Stuff/01.00 An ID/File Inside ID deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/nonempty_inbox/files/00-09 System/01 System Stuff/01.01 Inbox/Not This b/tests/without_jdex/nonempty_inbox/files/00-09 System/01 System Stuff/01.01 Inbox/Not This deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/nonempty_inbox/files/00-09 System/01 System Stuff/01.01 Inbox/Or This/.placeholder b/tests/without_jdex/nonempty_inbox/files/00-09 System/01 System Stuff/01.01 Inbox/Or This/.placeholder deleted file mode 100644 index e69de29..0000000 diff --git a/tests/without_jdex/nonempty_inbox/result.json b/tests/without_jdex/nonempty_inbox/result.json deleted file mode 100644 index 5c96c7c..0000000 --- a/tests/without_jdex/nonempty_inbox/result.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "errors": [ - { - "error": { - "type": "NONEMPTY_INBOX", - "num_items": 2 - }, - "files": [ - { - "name": "01.01 Inbox", - "nested_under": [ - "00-09 System", - "01 System Stuff" - ] - } - ] - } - ], - "jdex_errors": [] -} From 8c03909a2c4a7476c25a0f5258b349f86b981492 Mon Sep 17 00:00:00 2001 From: SiriusStarr <2049163+SiriusStarr@users.noreply.github.com> Date: Thu, 9 Jul 2026 15:41:25 -0700 Subject: [PATCH 03/23] Add JDEX_FOLDER_WHERE_NOTE_EXPECTED --- jdlint.py | 32 +++++++++++++++++-- .../files/1/1A/1A.md | 0 .../files/1/1B/1B.md/.placeholder | 0 .../jdlint.toml | 32 +++++++++++++++++++ .../result.json | 10 ++++++ 5 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/files/1/1A/1A.md create mode 100644 tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/files/1/1B/1B.md/.placeholder create mode 100644 tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/jdlint.toml create mode 100644 tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/result.json diff --git a/jdlint.py b/jdlint.py index 621605c..15df01b 100755 --- a/jdlint.py +++ b/jdlint.py @@ -1007,6 +1007,25 @@ def explain(self) -> _Explanation: ) +@dataclass(frozen=True) +class JDexIssueFolderWhereNoteExpected(JDexIssue): + """A JDex folder that matched an expected note was found.""" + + matched_format: str + type: Literal["JDEX_FOLDER_WHERE_NOTE_EXPECTED"] = "JDEX_FOLDER_WHERE_NOTE_EXPECTED" + + def display(self) -> str: + """Display this particular instance of an error.""" + return f'{self.file!s} (matched "{self.matched_format}")' + + def explain(self) -> _Explanation: + """Explain what this error is.""" + return _Explanation( + explanation="A JDex folder was found that matched the format of an expected note.", + fix="Your JDex format should not mix folders and notes that share a naming scheme.", + ) + + @dataclass(frozen=True) class JDexIssueArbitraryContentWhereNotAllowed(JDexIssue): """Content was found in the JDex that didn't match any expected format.""" @@ -1840,7 +1859,9 @@ def _get_jdex_notes_here_or_children( valid_children = [ (re.compile(c.format.build_regex(bound_segments)), c) for c in tier.children ] - valid_notes = [re.compile(n.format.build_regex(bound_segments)) for n in tier.notes] + valid_notes = [ + (re.compile(n.format.build_regex(bound_segments)), n) for n in tier.notes + ] accumulated_notes = [] accumulated_errors = [] @@ -1874,13 +1895,18 @@ def _get_jdex_notes_here_or_children( accumulated_errors.extend(child_errors) break else: - for note_format in valid_notes: + for note_format, note in valid_notes: match = note_format.fullmatch(x.name) if match: # Is a valid JDex note if x.is_dir(): # This is an error - # TODO report error + accumulated_errors.append( + JDexIssueFolderWhereNoteExpected( + PurePath(x), + note.format.raw_format, + ), + ) break # Create note entry diff --git a/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/files/1/1A/1A.md b/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/files/1/1A/1A.md new file mode 100644 index 0000000..e69de29 diff --git a/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/files/1/1B/1B.md/.placeholder b/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/files/1/1B/1B.md/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/jdlint.toml b/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/jdlint.toml new file mode 100644 index 0000000..dd5e111 --- /dev/null +++ b/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/jdlint.toml @@ -0,0 +1,32 @@ +[linter] +json_output = true + +[[system.roots]] +name = "JDex" +path = "files" +ignore = [".obsidian"] + +[system.jdex] +path = "files" +ignore = [".obsidian"] + +[[system.jdex.children]] +name = "JDex Folder" +format = "/#A/" + +[[system.jdex.children.children]] +name = "JDex Folder" +format = "/=A//*Name/" + +[[system.jdex.children.children.notes]] +name = "JDex Note" +format = "/=A//=Name/.md" + +## ########################################################### +# Standard +## ########################################################### +[[system.children]] +name = "Area" +format = "/#A/0-/=A/9 /*Area/" +jdex_note = "/=A/0.00 /=Area/.md" +allow_arbitrary_contents = true diff --git a/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/result.json b/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/result.json new file mode 100644 index 0000000..e758c1b --- /dev/null +++ b/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/result.json @@ -0,0 +1,10 @@ +{ + "errors": [], + "jdex_errors": [ + { + "type": "JDEX_FOLDER_WHERE_NOTE_EXPECTED", + "file": "files/1/1B/1B.md", + "matched_format": "/=A//=Name/.md" + } + ] +} \ No newline at end of file From edb20ecb55c218099ef1ed56a38d16737a29a682 Mon Sep 17 00:00:00 2001 From: SiriusStarr <2049163+SiriusStarr@users.noreply.github.com> Date: Thu, 9 Jul 2026 15:59:32 -0700 Subject: [PATCH 04/23] Include pattern match in errors --- jdlint.py | 35 ++++++++++++++----- .../jdlint.toml | 10 +++--- .../result.json | 30 ++++++++++++---- .../jdlint.toml | 6 ++-- .../result.json | 10 ++++-- .../jdlint.toml | 4 +-- .../result.json | 5 ++- 7 files changed, 72 insertions(+), 28 deletions(-) diff --git a/jdlint.py b/jdlint.py index 15df01b..6731019 100755 --- a/jdlint.py +++ b/jdlint.py @@ -992,12 +992,12 @@ def explain(self) -> _Explanation: class JDexIssueFileWhereFolderExpected(JDexIssue): """A JDex file that matched an expected folder was found.""" - matched_format: str + matched_pattern: ContentPattern type: Literal["JDEX_FILE_WHERE_FOLDER_EXPECTED"] = "JDEX_FILE_WHERE_FOLDER_EXPECTED" def display(self) -> str: """Display this particular instance of an error.""" - return f'{self.file!s} (matched "{self.matched_format}")' + return f'{self.file!s} (matched "{self.matched_pattern}")' def explain(self) -> _Explanation: """Explain what this error is.""" @@ -1011,12 +1011,12 @@ def explain(self) -> _Explanation: class JDexIssueFolderWhereNoteExpected(JDexIssue): """A JDex folder that matched an expected note was found.""" - matched_format: str + matched_pattern: ContentPattern type: Literal["JDEX_FOLDER_WHERE_NOTE_EXPECTED"] = "JDEX_FOLDER_WHERE_NOTE_EXPECTED" def display(self) -> str: """Display this particular instance of an error.""" - return f'{self.file!s} (matched "{self.matched_format}")' + return f'{self.file!s} (matched "{self.matched_pattern}")' def explain(self) -> _Explanation: """Explain what this error is.""" @@ -1026,11 +1026,19 @@ def explain(self) -> _Explanation: ) +@dataclass(frozen=True) +class ContentPattern: + """A possible pattern that could be matched.""" + + name: str + format: str + + @dataclass(frozen=True) class JDexIssueArbitraryContentWhereNotAllowed(JDexIssue): """Content was found in the JDex that didn't match any expected format.""" - possible_formats: list[str] + possible_formats: list[ContentPattern] type: Literal["JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED"] = ( "JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED" ) @@ -1879,7 +1887,7 @@ def _get_jdex_notes_here_or_children( accumulated_errors.append( JDexIssueFileWhereFolderExpected( PurePath(x), - child.format.raw_format, + ContentPattern(child.name, child.format.raw_format), ), ) break @@ -1904,7 +1912,10 @@ def _get_jdex_notes_here_or_children( accumulated_errors.append( JDexIssueFolderWhereNoteExpected( PurePath(x), - note.format.raw_format, + ContentPattern( + note.name, + note.format.raw_format, + ), ), ) break @@ -1919,8 +1930,14 @@ def _get_jdex_notes_here_or_children( accumulated_errors.append( JDexIssueArbitraryContentWhereNotAllowed( PurePath(x), - [c.format.raw_format for c in tier.children] - + [n.format.raw_format for n in tier.notes], + [ + ContentPattern(c.name, c.format.raw_format) + for c in tier.children + ] + + [ + ContentPattern(n.name, n.format.raw_format) + for n in tier.notes + ], ), ) return (accumulated_notes, accumulated_errors) diff --git a/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml b/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml index d4a142e..b875ebd 100644 --- a/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml +++ b/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml @@ -9,24 +9,24 @@ path = "files" path = "files" [[system.jdex.children]] -name = "JDex Folder" +name = "A" format = "/#A/" [[system.jdex.children.children]] -name = "JDex Folder" +name = "B1" format = "/*Name//=A/" allow_arbitrary_contents = true [[system.jdex.children.children.notes]] -name = "JDex Note" +name = "C1" format = "X.md" [[system.jdex.children.children]] -name = "JDex Folder" +name = "B2" format = "/=A//*Name/" [[system.jdex.children.children.notes]] -name = "JDex Note" +name = "C2" format = "/=A//=Name/.md" ## ########################################################### diff --git a/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/result.json b/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/result.json index dcf4487..2aed929 100644 --- a/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/result.json +++ b/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/result.json @@ -5,30 +5,48 @@ "type": "JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED", "file": "files/1/File", "possible_formats": [ - "/*Name//=A/", - "/=A//*Name/" + { + "name": "B1", + "format": "/*Name//=A/" + }, + { + "name": "B2", + "format": "/=A//*Name/" + } ] }, { "type": "JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED", "file": "files/1/Folder", "possible_formats": [ - "/*Name//=A/", - "/=A//*Name/" + { + "name": "B1", + "format": "/*Name//=A/" + }, + { + "name": "B2", + "format": "/=A//*Name/" + } ] }, { "type": "JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED", "file": "files/1/1A/File", "possible_formats": [ - "/=A//=Name/.md" + { + "name": "C2", + "format": "/=A//=Name/.md" + } ] }, { "type": "JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED", "file": "files/1/1A/Folder", "possible_formats": [ - "/=A//=Name/.md" + { + "name": "C2", + "format": "/=A//=Name/.md" + } ] } ] diff --git a/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/jdlint.toml b/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/jdlint.toml index dd5e111..80cbeb3 100644 --- a/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/jdlint.toml +++ b/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/jdlint.toml @@ -11,15 +11,15 @@ path = "files" ignore = [".obsidian"] [[system.jdex.children]] -name = "JDex Folder" +name = "A" format = "/#A/" [[system.jdex.children.children]] -name = "JDex Folder" +name = "B" format = "/=A//*Name/" [[system.jdex.children.children.notes]] -name = "JDex Note" +name = "C" format = "/=A//=Name/.md" ## ########################################################### diff --git a/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/result.json b/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/result.json index 174e674..4d0283b 100644 --- a/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/result.json +++ b/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/result.json @@ -4,12 +4,18 @@ { "type": "JDEX_FILE_WHERE_FOLDER_EXPECTED", "file": "files/3", - "matched_format": "/#A/" + "matched_pattern": { + "name": "A", + "format": "/#A/" + } }, { "type": "JDEX_FILE_WHERE_FOLDER_EXPECTED", "file": "files/1/1B", - "matched_format": "/=A//*Name/" + "matched_pattern": { + "name": "B", + "format": "/=A//*Name/" + } } ] } \ No newline at end of file diff --git a/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/jdlint.toml b/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/jdlint.toml index dd5e111..7ad42d9 100644 --- a/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/jdlint.toml +++ b/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/jdlint.toml @@ -11,11 +11,11 @@ path = "files" ignore = [".obsidian"] [[system.jdex.children]] -name = "JDex Folder" +name = "A" format = "/#A/" [[system.jdex.children.children]] -name = "JDex Folder" +name = "B" format = "/=A//*Name/" [[system.jdex.children.children.notes]] diff --git a/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/result.json b/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/result.json index e758c1b..24be0a7 100644 --- a/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/result.json +++ b/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/result.json @@ -4,7 +4,10 @@ { "type": "JDEX_FOLDER_WHERE_NOTE_EXPECTED", "file": "files/1/1B/1B.md", - "matched_format": "/=A//=Name/.md" + "matched_pattern": { + "name": "JDex Note", + "format": "/=A//=Name/.md" + } } ] } \ No newline at end of file From a04a6a498bae1265e256381076ad72a17b55a5d3 Mon Sep 17 00:00:00 2001 From: SiriusStarr <2049163+SiriusStarr@users.noreply.github.com> Date: Thu, 9 Jul 2026 16:19:18 -0700 Subject: [PATCH 05/23] Add empty folder in jdex detection --- jdlint.py | 23 +++++++++++++ .../files/1/1A}/.placeholder | 0 tests/JDEX_EMPTY_FOLDER/jdlint.toml | 32 +++++++++++++++++++ tests/JDEX_EMPTY_FOLDER/result.json | 9 ++++++ .../jdlint.toml | 2 -- .../jdlint.toml | 2 -- 6 files changed, 64 insertions(+), 4 deletions(-) rename tests/{JDEX_FILE_WHERE_FOLDER_EXPECTED/files/.obsidian => JDEX_EMPTY_FOLDER/files/1/1A}/.placeholder (100%) create mode 100644 tests/JDEX_EMPTY_FOLDER/jdlint.toml create mode 100644 tests/JDEX_EMPTY_FOLDER/result.json diff --git a/jdlint.py b/jdlint.py index 6731019..d48aa57 100755 --- a/jdlint.py +++ b/jdlint.py @@ -1055,6 +1055,24 @@ def explain(self) -> _Explanation: ) +@dataclass(frozen=True) +class JDexIssueEmptyFolder(JDexIssue): + """A folder in the JDex is completely empty (and is not arbitrary content).""" + + type: Literal["JDEX_EMPTY_FOLDER"] = "JDEX_EMPTY_FOLDER" + + def display(self) -> str: + """Display this particular instance of an error.""" + return f"{self.file!s}" + + def explain(self) -> _Explanation: + """Explain what this error is.""" + return _Explanation( + explanation="A folder that matched a pattern in the JDex has no contents.", + fix="You should ensure that all JDex notes exist; if this folder truly contains no IDs, it shouldn't exist.", + ) + + @dataclass(frozen=True) class JDexIdInWrongCategory: """A JDex ID that, by its number, has been put in the wrong category.""" @@ -1874,10 +1892,12 @@ def _get_jdex_notes_here_or_children( accumulated_notes = [] accumulated_errors = [] + has_content = False with os.scandir(path) as contents: for x in contents: if _entry_is_ignored(ignored, [], x): continue + has_content = True for child_format, child in valid_children: match = child_format.fullmatch(x.name) if match: @@ -1940,6 +1960,9 @@ def _get_jdex_notes_here_or_children( ], ), ) + if not has_content: + # We have a fully empty JDex folder; it shouldn't exist if it's doing nothing. + accumulated_errors.append(JDexIssueEmptyFolder(PurePath(path))) return (accumulated_notes, accumulated_errors) diff --git a/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/files/.obsidian/.placeholder b/tests/JDEX_EMPTY_FOLDER/files/1/1A/.placeholder similarity index 100% rename from tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/files/.obsidian/.placeholder rename to tests/JDEX_EMPTY_FOLDER/files/1/1A/.placeholder diff --git a/tests/JDEX_EMPTY_FOLDER/jdlint.toml b/tests/JDEX_EMPTY_FOLDER/jdlint.toml new file mode 100644 index 0000000..4a8214e --- /dev/null +++ b/tests/JDEX_EMPTY_FOLDER/jdlint.toml @@ -0,0 +1,32 @@ +[linter] +json_output = true +ignore = [".placeholder"] + +[[system.roots]] +name = "JDex" +path = "files" + +[system.jdex] +path = "files" + +[[system.jdex.children]] +name = "A" +format = "/#A/" + +[[system.jdex.children.children]] +name = "B" +format = "/=A//*Name/" +allow_arbitrary_contents = true + +[[system.jdex.children.children.notes]] +name = "C1" +format = "X.md" + +## ########################################################### +# Standard +## ########################################################### +[[system.children]] +name = "Area" +format = "/#A/0-/=A/9 /*Area/" +jdex_note = "/=A/0.00 /=Area/.md" +allow_arbitrary_contents = true diff --git a/tests/JDEX_EMPTY_FOLDER/result.json b/tests/JDEX_EMPTY_FOLDER/result.json new file mode 100644 index 0000000..73ea4fb --- /dev/null +++ b/tests/JDEX_EMPTY_FOLDER/result.json @@ -0,0 +1,9 @@ +{ + "errors": [], + "jdex_errors": [ + { + "type": "JDEX_EMPTY_FOLDER", + "file": "files/1/1A" + } + ] +} \ No newline at end of file diff --git a/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/jdlint.toml b/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/jdlint.toml index 80cbeb3..064d82d 100644 --- a/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/jdlint.toml +++ b/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/jdlint.toml @@ -4,11 +4,9 @@ json_output = true [[system.roots]] name = "JDex" path = "files" -ignore = [".obsidian"] [system.jdex] path = "files" -ignore = [".obsidian"] [[system.jdex.children]] name = "A" diff --git a/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/jdlint.toml b/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/jdlint.toml index 7ad42d9..4bde85e 100644 --- a/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/jdlint.toml +++ b/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/jdlint.toml @@ -4,11 +4,9 @@ json_output = true [[system.roots]] name = "JDex" path = "files" -ignore = [".obsidian"] [system.jdex] path = "files" -ignore = [".obsidian"] [[system.jdex.children]] name = "A" From 7f6ffc6c76fb047f354b524b32e944256bcaf5bd Mon Sep 17 00:00:00 2001 From: SiriusStarr <2049163+SiriusStarr@users.noreply.github.com> Date: Fri, 10 Jul 2026 16:55:40 -0700 Subject: [PATCH 06/23] Finish JDex scraping and add duplicate ID detection --- jdlint.py | 476 ++++++++++-------- .../files/1/{A1 => B1}/File | 0 .../files/1/{A1 => B1}/Folder/.placeholder | 0 .../files/1/{A1 => B1}/X.md | 0 .../jdlint.toml | 3 + .../result.json | 32 +- tests/JDEX_DUPLICATE_ID/files/1/11 A.md | 0 tests/JDEX_DUPLICATE_ID/files/1/11 B.md | 0 tests/JDEX_DUPLICATE_ID/jdlint.toml | 28 ++ tests/JDEX_DUPLICATE_ID/result.json | 14 + tests/JDEX_EMPTY_FOLDER/jdlint.toml | 2 + .../jdlint.toml | 2 + .../result.json | 9 +- .../jdlint.toml | 2 + .../result.json | 6 +- 15 files changed, 353 insertions(+), 221 deletions(-) rename tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/{A1 => B1}/File (100%) rename tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/{A1 => B1}/Folder/.placeholder (100%) rename tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/{A1 => B1}/X.md (100%) create mode 100644 tests/JDEX_DUPLICATE_ID/files/1/11 A.md create mode 100644 tests/JDEX_DUPLICATE_ID/files/1/11 B.md create mode 100644 tests/JDEX_DUPLICATE_ID/jdlint.toml create mode 100644 tests/JDEX_DUPLICATE_ID/result.json diff --git a/jdlint.py b/jdlint.py index d48aa57..ef362fa 100755 --- a/jdlint.py +++ b/jdlint.py @@ -33,10 +33,21 @@ def __init__(self, key: str, message: str) -> None: super().__init__(f"Error in config at key: {key}. {message}") -class ConfigKeyError(ConfigError): +class ConfigMissingKeyError(ConfigError): + """A missing key in the jdlint config.""" + + def __init__(self, key: str) -> None: + """Create a missing key error.""" + super().__init__( + key, + "Required key not found.", + ) + + +class ConfigExtraKeyError(ConfigError): """An unexpected key in the jdlint config.""" - def __init__(self, key: str, valid: list[str]) -> None: + def __init__(self, key: str, valid: tuple[str, ...]) -> None: """Create a key error, given the extra key and a list of valid keys.""" super().__init__( key, @@ -79,7 +90,11 @@ class ConfigSystemRoot: def __init__(self, at: str, from_file: dict) -> None: """Create a valid configuration given a loaded section of a config file.""" # Acquire and set defaults + if "name" not in from_file: + raise (ConfigMissingKeyError(f"{at}.name")) self.name = from_file.pop("name") + if "path" not in from_file: + raise (ConfigMissingKeyError(f"{at}.path")) self.path = Path(from_file.pop("path")).expanduser() self.ignore = from_file.pop("ignore", []) @@ -109,7 +124,7 @@ def __init__(self, at: str, from_file: dict) -> None: # Ensure no extra fields for key in from_file: - raise ConfigKeyError(f"{at}.{key}", list(self.__dict__.keys())) + raise ConfigExtraKeyError(f"{at}.{key}", tuple(self.__dict__.keys())) class ConfigSystemJDex: @@ -118,6 +133,8 @@ class ConfigSystemJDex: def __init__(self, at: str, from_file: dict) -> None: """Create a valid configuration given a loaded section of a config file.""" # Acquire and set defaults + if "path" not in from_file: + raise (ConfigMissingKeyError(f"{at}.path")) self.path = Path(from_file.pop("path")).expanduser() self.ignore = from_file.pop("ignore", []) @@ -141,7 +158,7 @@ def __init__(self, at: str, from_file: dict) -> None: self.children = [ ConfigJDexTier( f"{at}.children[{i}]", - [], + ConfigFormatAncestorInfo((), ()), v, ) for i, v in enumerate(from_file.pop("children", [])) @@ -149,7 +166,7 @@ def __init__(self, at: str, from_file: dict) -> None: self.notes = [ ConfigJDexNotes( f"{at}.notes[{i}]", - [], + ConfigFormatAncestorInfo((), ()), v, ) for i, v in enumerate(from_file.pop("notes", [])) @@ -162,7 +179,7 @@ def __init__(self, at: str, from_file: dict) -> None: # Ensure no extra fields for key in from_file: - raise ConfigKeyError(f"{at}.{key}", list(self.__dict__.keys())) + raise ConfigExtraKeyError(f"{at}.{key}", tuple(self.__dict__.keys())) class ConfigLinter: @@ -206,7 +223,7 @@ def __init__(self, from_file: dict) -> None: # Ensure no extra fields for key in from_file: - raise ConfigKeyError(f"linter.{key}", list(self.__dict__.keys())) + raise ConfigExtraKeyError(f"linter.{key}", tuple(self.__dict__.keys())) class ConfigSystem: @@ -230,52 +247,109 @@ def __init__(self, from_file: dict) -> None: self.children = [ ConfigSystemTier( f"system.children[{i}]", - [], + ConfigFormatAncestorInfo((), ()), v, ) for i, v in enumerate(from_file.pop("children", [])) ] + # Ensure no extra fields for key in from_file: - raise ConfigKeyError(f"system.{key}", list(self.__dict__.keys())) + raise ConfigExtraKeyError(f"system.{key}", tuple(self.__dict__.keys())) -class ConfigJDexNotes: - """Configuration for how JDex notes are formatted.""" +class ConfigStaticFormat: + """Configuration for how to assign a static ID or JDex note.""" + + # A valid variable segment of an ID format + variable_static_segment_re = re.compile(r"=([A-Za-z]+)") def __init__( self, at: str, - parent_segments: list[str], - from_file: dict, + ancestors: ConfigFormatAncestorInfo, + from_file: str, ) -> None: - """Create a valid note format given a loaded section of a config file.""" - # Acquire and set defaults - self.name = from_file.pop("name") + """Create a valid format given a string from a config file.""" # Validate - if not isinstance(self.name, str): + if not isinstance(from_file, str): raise ConfigTypeError( - f"{at}.name", + at, "str", - type(self.name).__name__, + type(from_file).__name__, ) - if not isinstance(from_file["format"], str): - raise ConfigTypeError( - f"{at}.format", - "str", - type(from_file["format"]).__name__, + if from_file.count("/") % 2 != 0: + raise ConfigValueError( + at, + "Malformed id/JDex note format; there must be an even number of / characters. You have an extra one/are missing one.", + from_file, ) + if from_file == "": + raise ConfigValueError( + at, + "Malformed id/JDex note format; must not be empty.", + from_file, + ) + + build_id = [] + + for i, v in enumerate(from_file.split("/")): + if i % 2 == 0: + # Literal segment + build_id.append(lambda _, v=v: v) + else: + # Variable segment + match = ConfigStaticFormat.variable_static_segment_re.fullmatch(v) + if not match: + raise ConfigValueError( + at, + "Malformed id/JDex note format; variable segment must consist of = followed by an alphabetic identifier.", + v, + ) + if match.group(1) in ancestors.segments: + p = match.group(1) + build_id.append(lambda d, p=p: d[p]) + + else: + raise ConfigValueError( + at, + "Malformed id/JDex note format; variable segment referenced an identifier never bound.", + v, + ) + + self.build_id = lambda d: "".join([f(d) for f in build_id]) + + +class ConfigJDexNotes: + """Configuration for how JDex notes are formatted.""" + + def __init__( + self, + at: str, + ancestors: ConfigFormatAncestorInfo, + from_file: dict, + ) -> None: + """Create a valid note format given a loaded section of a config file.""" # Compile Format + if "format" not in from_file: + raise (ConfigMissingKeyError(f"{at}.format")) self.format = ConfigFormat( - f"{at}.format", - parent_segments, - from_file.pop("format"), + f"{at}", + ancestors, + from_file, + ) + if "id" not in from_file: + raise (ConfigMissingKeyError(f"{at}.id")) + self.id = ConfigStaticFormat( + f"{at}.id", + self.format, + from_file.pop("id"), ) # Ensure no extra fields for key in from_file: - raise ConfigKeyError(f"{at}.{key}", list(self.__dict__.keys())) + raise ConfigExtraKeyError(f"{at}.{key}", tuple(self.__dict__.keys())) class ConfigFolderTier: @@ -285,27 +359,14 @@ def __init__( self, child_class: Callable, at: str, - parent_segments: list[str], + ancestors: ConfigFormatAncestorInfo, from_file: dict, ) -> None: """Create a valid tier given a loaded section of a config file.""" # Acquire and set defaults - self.name = from_file.pop("name") self.allow_arbitrary_contents = from_file.pop("allow_arbitrary_contents", False) # Validate - if not isinstance(self.name, str): - raise ConfigTypeError( - f"{at}.name", - "str", - type(self.name).__name__, - ) - if not isinstance(from_file["format"], str): - raise ConfigTypeError( - f"{at}.format", - "str", - type(from_file["format"]).__name__, - ) if not isinstance(self.allow_arbitrary_contents, bool): raise ConfigTypeError( f"{at}.allow_arbitrary_contents", @@ -315,14 +376,14 @@ def __init__( # Compile Format & Children self.format = ConfigFormat( - f"{at}.format", - parent_segments, - from_file.pop("format"), + at, + ancestors, + from_file, ) self.children = [ child_class( f"{at}.children[{i}]", - self.format.known_segments, + self.format, v, ) for i, v in enumerate(from_file.pop("children", [])) @@ -337,22 +398,37 @@ def __init__( class ConfigSystemTier(ConfigFolderTier): """A tier (hierarchical level) of a JD system, e.g. a Category, in the system (not the JDex).""" - def __init__(self, at: str, parent_segments: list[str], from_file: dict) -> None: + def __init__( + self, + at: str, + ancestors: ConfigFormatAncestorInfo, + from_file: dict, + ) -> None: """Create a valid tier given a loaded section of a config file.""" # Acquire and set defaults - self.jdex_note = from_file.pop("jdex_note", None) + self.can_be_file = from_file.pop("can_be_file", False) # Call the folder tier stuff - super().__init__(ConfigSystemTier, at, parent_segments, from_file) + super().__init__(ConfigSystemTier, at, ancestors, from_file) - # Validate - if self.jdex_note is not None and not isinstance(self.jdex_note, str): - raise ConfigTypeError( + if "jdex_note" in from_file: + self.jdex_note = ConfigStaticFormat( f"{at}.jdex_note", - "str", - type(self.jdex_note).__name__, + self.format, + from_file.pop("jdex_note"), ) + else: + self.jdex_note = None + + if "id" not in from_file: + raise (ConfigMissingKeyError(f"{at}.id")) + self.id = ConfigStaticFormat( + f"{at}.id", + self.format, + from_file.pop("id"), + ) + if not isinstance(self.can_be_file, bool): raise ConfigTypeError( f"{at}.can_be_file", @@ -367,21 +443,26 @@ def __init__(self, at: str, parent_segments: list[str], from_file: dict) -> None ) # Ensure no extra fields for key in from_file: - raise ConfigKeyError(f"{at}.{key}", list(self.__dict__.keys())) + raise ConfigExtraKeyError(f"{at}.{key}", tuple(self.__dict__.keys())) class ConfigJDexTier(ConfigFolderTier): """A tier (hierarchical level) of a JD system, e.g. a Category, in the JDex.""" - def __init__(self, at: str, parent_segments: list[str], from_file: dict) -> None: + def __init__( + self, + at: str, + ancestors: ConfigFormatAncestorInfo, + from_file: dict, + ) -> None: """Create a valid tier given a loaded section of a config file.""" # Call the folder tier stuff - super().__init__(ConfigJDexTier, at, parent_segments, from_file) + super().__init__(ConfigJDexTier, at, ancestors, from_file) self.notes = [ ConfigJDexNotes( f"{at}.notes[{i}]", - self.format.known_segments, + self.format, v, ) for i, v in enumerate(from_file.pop("notes", [])) @@ -401,32 +482,73 @@ def __init__(self, at: str, parent_segments: list[str], from_file: dict) -> None # Ensure no extra fields for key in from_file: - raise ConfigKeyError(f"{at}.{key}", list(self.__dict__.keys())) + raise ConfigExtraKeyError(f"{at}.{key}", tuple(self.__dict__.keys())) + + +@dataclass +class ConfigFormatAncestorInfo: + name: tuple[str, ...] + segments: tuple[str, ...] -class ConfigFormat: +class ConfigFormat(ConfigFormatAncestorInfo): """A format for a file or folder.""" # A valid variable segment of a format variable_segment_re = re.compile(r"(=|\*|[#]+)([A-Za-z]+)") - def __init__(self, at: str, parent_segments: list[str], from_file: str) -> None: + def __init__( + self, + at: str, + ancestors: ConfigFormatAncestorInfo, + from_file: dict, + ) -> None: """Create a valid format given a string from a config file.""" - if from_file.count("/") % 2 != 0: + if "format" not in from_file: + raise (ConfigMissingKeyError(f"{at}.format")) + + self.raw_format = from_file.pop("format") + + if "name" not in from_file: + raise (ConfigMissingKeyError(f"{at}.name")) + + # Validate + if not isinstance(from_file["name"], str): + raise ConfigTypeError( + f"{at}.name", + "str", + type(from_file["name"]).__name__, + ) + if not isinstance(self.raw_format, str): + raise ConfigTypeError( + f"{at}.format", + "str", + type(self.raw_format).__name__, + ) + + if from_file["name"] == "": raise ConfigValueError( - at, + f"{at}.name", + "Malformed name; must not be empty.", + from_file, + ) + if self.raw_format.count("/") % 2 != 0: + raise ConfigValueError( + f"{at}.format", "Malformed format; there must be an even number of / characters. You have an extra one/are missing one.", from_file, ) - if from_file == "": + if self.raw_format == "": raise ConfigValueError( - at, + f"{at}.format", "Malformed format; must not be empty.", from_file, ) + regex = [] new_segments = [] - for i, v in enumerate(from_file.split("/")): + + for i, v in enumerate(self.raw_format.split("/")): if i % 2 == 0: # Literal segment regex.append(lambda _, v=v: re.escape(v)) @@ -435,12 +557,12 @@ def __init__(self, at: str, parent_segments: list[str], from_file: str) -> None: match = ConfigFormat.variable_segment_re.fullmatch(v) if not match: raise ConfigValueError( - at, + f"{at}.format", "Malformed format; variable segment must consist of =, *, or one or more # followed by an alphabetic identifier.", v, ) if match.group(1) == "=": - if match.group(2) in parent_segments: + if match.group(2) in ancestors.segments: p = match.group(2) regex.append(lambda d, p=p: re.escape(d[p])) elif match.group(2) in new_segments: @@ -451,25 +573,25 @@ def __init__(self, at: str, parent_segments: list[str], from_file: str) -> None: else: raise ConfigValueError( - at, - "Malformed format; variable segment referenced an identifier not bound in a parent.", + f"{at}.format", + "Malformed format; variable segment referenced an identifier never bound.", v, ) else: - if match.group(2) in parent_segments: + if match.group(2) in ancestors.segments: raise ConfigValueError( - at, + f"{at}.format", "Malformed format; variable segment tried to rebind an identifier already bound in a parent.", v, ) if match.group(2) in new_segments: raise ConfigValueError( - at, + f"{at}.format", "Malformed format; variable segment tried to rebind an identifier already bound.", v, ) identifier = match.group(2) - new_segments.append(match.group(2)) + new_segments.append(identifier) if match.group(1) == "*": regex.append( lambda _, identifier=identifier: f"(?P<{identifier}>.+)", @@ -482,14 +604,17 @@ def __init__(self, at: str, parent_segments: list[str], from_file: str) -> None: ), ) - self.known_segments = parent_segments + new_segments + self.name = (*ancestors.name, from_file.pop("name")) + self.segments = ancestors.segments + tuple(new_segments) self.build_regex = lambda d: "".join([f(d) for f in regex]) - self.raw_format = from_file class Config: def __init__(self, from_file): self.linter = ConfigLinter(from_file.get("linter", {})) + + if "system" not in from_file: + raise (ConfigMissingKeyError("system")) self.system = ConfigSystem(from_file["system"]) @@ -960,25 +1085,24 @@ def display(self, files: list[File]) -> str: return ( self.category + ":\n " + "\n ".join([_print_nest(f) for f in files]) ) +class File: + """A file or folder that has been detected by jdlint.""" - def explain(self) -> _Explanation: - """Explain what this error is.""" - return _Explanation( - explanation="Duplicate categories were used in the JDex.", - fix="Assign a new category to one of them.", - ) + name: Path + path: Path @dataclass(frozen=True) -class JDexDuplicateId: +class JDexIssueDuplicateId(JDexIssue): """A JDex ID that has been used multiple times.""" + files: tuple[PurePath, ...] id: str type: Literal["JDEX_DUPLICATE_ID"] = "JDEX_DUPLICATE_ID" - def display(self, files: list[File]) -> str: + def display(self) -> str: """Display this particular instance of an error.""" - return self.id + ":\n " + "\n ".join([_print_nest(f) for f in files]) + return f"{self.id}:\n " + "\n ".join([str(f.name) for f in self.files]) def explain(self) -> _Explanation: """Explain what this error is.""" @@ -1030,7 +1154,7 @@ def explain(self) -> _Explanation: class ContentPattern: """A possible pattern that could be matched.""" - name: str + name: tuple[str, ...] format: str @@ -1038,7 +1162,7 @@ class ContentPattern: class JDexIssueArbitraryContentWhereNotAllowed(JDexIssue): """Content was found in the JDex that didn't match any expected format.""" - possible_formats: list[ContentPattern] + possible_formats: tuple[ContentPattern, ...] type: Literal["JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED"] = ( "JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED" ) @@ -1073,106 +1197,6 @@ def explain(self) -> _Explanation: ) -@dataclass(frozen=True) -class JDexIdInWrongCategory: - """A JDex ID that, by its number, has been put in the wrong category.""" - - id_ac: str - file_ac: str - type: Literal["JDEX_ID_IN_WRONG_CATEGORY"] = "JDEX_ID_IN_WRONG_CATEGORY" - - def display(self, files: list[File]) -> str: - """Display this particular instance of an error.""" - return ( - f"{_print_nest(files[0])} [in {self.file_ac} but should be in {self.id_ac}]" - ) - - def explain(self) -> _Explanation: - """Explain what this error is.""" - return _Explanation( - explanation="Some JDex IDs are in the wrong category.", - fix="Move them into the correct category folder, or use a flat JDex structure.", - ) - - -@dataclass(frozen=True) -class JDexInvalidAreaName: - """A folder at the JDex area level that doesn't match the normal format.""" - - type: Literal["JDEX_INVALID_AREA_NAME"] = "JDEX_INVALID_AREA_NAME" - - def display(self, files: list[File]) -> str: - """Display this particular instance of an error.""" - return _print_nest(files[0]) - - def explain(self) -> _Explanation: - """Explain what this error is.""" - return _Explanation( - explanation="Some JDex areas have invalid names.", - fix='Valid area names look like "10-19 Life Admin", so edit the names to match that format.', - ) - - -@dataclass(frozen=True) -class JDexInvalidCategoryName: - """A folder at the JDex category level that doesn't match the normal format.""" - - type: Literal["JDEX_INVALID_CATEGORY_NAME"] = "JDEX_INVALID_CATEGORY_NAME" - - def display(self, files: list[File]) -> str: - """Display this particular instance of an error.""" - return _print_nest(files[0]) - - def explain(self) -> _Explanation: - """Explain what this error is.""" - return _Explanation( - explanation="Some JDex categories have invalid names.", - fix='Valid category names look like "11 Me, Myself, & I", so edit the names to match that format.', - ) - - -@dataclass(frozen=True) -class JDexInvalidIDName: - """A JDex note that doesn't match the normal format.""" - - type: Literal["JDEX_INVALID_ID_NAME"] = "JDEX_INVALID_ID_NAME" - - def display(self, files: list[File]) -> str: - """Display this particular instance of an error.""" - return _print_nest(files[0]) - - def explain(self) -> _Explanation: - """Explain what this error is.""" - return _Explanation( - explanation="Some JDex IDs have invalid names.", - fix='Valid ID names look like "11.11 A Cool Project", so edit the names to match that format.', - ) - - -JDexIssueType = ( - JDexAreaHeaderDifferentFromArea - | JDexAreaHeaderWithoutArea - | JDexCategoryInWrongArea - | JDexDuplicateArea - | JDexDuplicateAreaHeader - | JDexDuplicateCategory - | JDexDuplicateId - | JDexIssueFileWhereFolderExpected - | JDexIdInWrongCategory - | JDexInvalidAreaName - | JDexInvalidCategoryName - | JDexInvalidIDName -) - - -@dataclass(frozen=True) -class File: - """A file or folder that has been detected by jdlint.""" - - name: Path - path: Path - - @dataclass(frozen=True) class _Explanation: explanation: str @@ -1295,11 +1319,12 @@ def _jdex_note_id_re(ac: str) -> re.Pattern: def _entry_is_ignored( - ignored: list[str] | None, + ignored: tuple[str] | None, nested_under: list[str], f: os.DirEntry, ) -> bool: """Check if a given file/directory should be ignored.""" + # TODO this is now wrong with the nested under shit and needs fixing if not ignored: return False p = PurePath(*nested_under, f.name) @@ -1332,6 +1357,14 @@ def _insert_append(k, v, d) -> None: # noqa: ANN001 d[k].append(v) +def _insert_concat(k, vs: list, d) -> None: # noqa: ANN001 + """Add value as a singleton if it's not already in the dict, else append it to the list.""" + if k not in d: + d.update({k: []}) + + d[k].extend(vs) + + def _process_single_file_jdex(path: Path) -> _JDexResults: """Process a JDex located in a single file.""" # Matches JDex areas in a single-file format @@ -1867,20 +1900,12 @@ def _print_nest(f: File) -> str: return f.name -class JDexEntry: - """An entry in the JDex.""" - - def __init__(self, name: str) -> None: - """Create a JDexEntry, given its filename.""" - self.name = name - - def _get_jdex_notes_here_or_children( - ignored: list[str], + ignored: tuple[str], bound_segments: dict[str, str], path: os.PathLike, tier: ConfigJDexTier | ConfigSystemJDex, -) -> tuple[list[JDexEntry], list[JDexIssue]]: +) -> tuple[dict[str, list[PurePath]], list[JDexIssue]]: # Compile regexes for children valid_children = [ (re.compile(c.format.build_regex(bound_segments)), c) for c in tier.children @@ -1889,7 +1914,7 @@ def _get_jdex_notes_here_or_children( (re.compile(n.format.build_regex(bound_segments)), n) for n in tier.notes ] - accumulated_notes = [] + accumulated_notes = {} accumulated_errors = [] has_content = False @@ -1907,7 +1932,9 @@ def _get_jdex_notes_here_or_children( accumulated_errors.append( JDexIssueFileWhereFolderExpected( PurePath(x), - ContentPattern(child.name, child.format.raw_format), + ContentPattern( + child.format.name, child.format.raw_format + ), ), ) break @@ -1916,10 +1943,11 @@ def _get_jdex_notes_here_or_children( (child_notes, child_errors) = _get_jdex_notes_here_or_children( ignored, {**bound_segments, **match.groupdict()}, - x.path, + PurePath(x.path), child, ) - accumulated_notes.extend(child_notes) + for id, notes in child_notes.items(): + _insert_concat(id, notes, accumulated_notes) accumulated_errors.extend(child_errors) break else: @@ -1933,7 +1961,7 @@ def _get_jdex_notes_here_or_children( JDexIssueFolderWhereNoteExpected( PurePath(x), ContentPattern( - note.name, + note.format.name, note.format.raw_format, ), ), @@ -1941,7 +1969,11 @@ def _get_jdex_notes_here_or_children( break # Create note entry - accumulated_notes.append(JDexEntry(x.name)) + _insert_append( + note.id.build_id({**bound_segments, **match.groupdict()}), + PurePath(x.path), + accumulated_notes, + ) break else: # If we got here, it matched no known child/note @@ -1950,14 +1982,14 @@ def _get_jdex_notes_here_or_children( accumulated_errors.append( JDexIssueArbitraryContentWhereNotAllowed( PurePath(x), - [ - ContentPattern(c.name, c.format.raw_format) + tuple( + ContentPattern(c.format.name, c.format.raw_format) for c in tier.children - ] - + [ - ContentPattern(n.name, n.format.raw_format) + ) + + tuple( + ContentPattern(n.format.name, n.format.raw_format) for n in tier.notes - ], + ), ), ) if not has_content: @@ -1966,12 +1998,32 @@ def _get_jdex_notes_here_or_children( return (accumulated_notes, accumulated_errors) +def _process_jdex( + ignored: tuple[str], + path: os.PathLike, + jdex_root: ConfigSystemJDex, +) -> tuple[dict[str, list[PurePath]], list[JDexIssue]]: + (jdex_notes_by_id, jdex_errors) = _get_jdex_notes_here_or_children( + ignored, + {}, + path, + jdex_root, + ) + + # Check for duplicate ids + duplicate_id_errors = [ + JDexIssueDuplicateId(ns[0], tuple(ns), id) + for id, ns in jdex_notes_by_id.items() + if len(ns) != 1 + ] + return (jdex_notes_by_id, jdex_errors + duplicate_id_errors) + + def lint_system(config: Config) -> LintResults: jdex_errors = [] if config.system.jdex: - (jdex_notes, jdex_errors) = _get_jdex_notes_here_or_children( + (jdex_notes, jdex_errors) = _process_jdex( config.linter.ignore + config.system.jdex.ignore, - {}, config.system.jdex.path, config.system.jdex, ) diff --git a/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/A1/File b/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/B1/File similarity index 100% rename from tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/A1/File rename to tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/B1/File diff --git a/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/A1/Folder/.placeholder b/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/B1/Folder/.placeholder similarity index 100% rename from tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/A1/Folder/.placeholder rename to tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/B1/Folder/.placeholder diff --git a/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/A1/X.md b/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/B1/X.md similarity index 100% rename from tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/A1/X.md rename to tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/B1/X.md diff --git a/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml b/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml index b875ebd..6d3ad33 100644 --- a/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml +++ b/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml @@ -20,6 +20,7 @@ allow_arbitrary_contents = true [[system.jdex.children.children.notes]] name = "C1" format = "X.md" +id = "/=A/./=Name/" [[system.jdex.children.children]] name = "B2" @@ -28,6 +29,7 @@ format = "/=A//*Name/" [[system.jdex.children.children.notes]] name = "C2" format = "/=A//=Name/.md" +id = "/=A/./=Name/" ## ########################################################### # Standard @@ -36,4 +38,5 @@ format = "/=A//=Name/.md" name = "Area" format = "/#A/0-/=A/9 /*Area/" jdex_note = "/=A/0.00 /=Area/.md" +id = "/=A/./=Area/" allow_arbitrary_contents = true diff --git a/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/result.json b/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/result.json index 2aed929..4e8d438 100644 --- a/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/result.json +++ b/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/result.json @@ -6,11 +6,17 @@ "file": "files/1/File", "possible_formats": [ { - "name": "B1", + "name": [ + "A", + "B1" + ], "format": "/*Name//=A/" }, { - "name": "B2", + "name": [ + "A", + "B2" + ], "format": "/=A//*Name/" } ] @@ -20,11 +26,17 @@ "file": "files/1/Folder", "possible_formats": [ { - "name": "B1", + "name": [ + "A", + "B1" + ], "format": "/*Name//=A/" }, { - "name": "B2", + "name": [ + "A", + "B2" + ], "format": "/=A//*Name/" } ] @@ -34,7 +46,11 @@ "file": "files/1/1A/File", "possible_formats": [ { - "name": "C2", + "name": [ + "A", + "B2", + "C2" + ], "format": "/=A//=Name/.md" } ] @@ -44,7 +60,11 @@ "file": "files/1/1A/Folder", "possible_formats": [ { - "name": "C2", + "name": [ + "A", + "B2", + "C2" + ], "format": "/=A//=Name/.md" } ] diff --git a/tests/JDEX_DUPLICATE_ID/files/1/11 A.md b/tests/JDEX_DUPLICATE_ID/files/1/11 A.md new file mode 100644 index 0000000..e69de29 diff --git a/tests/JDEX_DUPLICATE_ID/files/1/11 B.md b/tests/JDEX_DUPLICATE_ID/files/1/11 B.md new file mode 100644 index 0000000..e69de29 diff --git a/tests/JDEX_DUPLICATE_ID/jdlint.toml b/tests/JDEX_DUPLICATE_ID/jdlint.toml new file mode 100644 index 0000000..2aad4ae --- /dev/null +++ b/tests/JDEX_DUPLICATE_ID/jdlint.toml @@ -0,0 +1,28 @@ +[linter] +json_output = true + +[[system.roots]] +name = "JDex" +path = "files" + +[system.jdex] +path = "files" + +[[system.jdex.children]] +name = "A" +format = "/#A/" + +[[system.jdex.children.notes]] +name = "B" +format = "/=A//#ID//*Name/.md" +id = "/=A/./=ID/" + +## ########################################################### +# Standard +## ########################################################### +[[system.children]] +name = "Area" +format = "/#A/0-/=A/9 /*Area/" +jdex_note = "/=A/0.00 /=Area/.md" +id = "/=A/./=Area/" +allow_arbitrary_contents = true diff --git a/tests/JDEX_DUPLICATE_ID/result.json b/tests/JDEX_DUPLICATE_ID/result.json new file mode 100644 index 0000000..6feafb5 --- /dev/null +++ b/tests/JDEX_DUPLICATE_ID/result.json @@ -0,0 +1,14 @@ +{ + "errors": [], + "jdex_errors": [ + { + "type": "JDEX_DUPLICATE_ID", + "file": "files/1/11 A.md", + "files": [ + "files/1/11 A.md", + "files/1/11 B.md" + ], + "id": "1.1" + } + ] +} \ No newline at end of file diff --git a/tests/JDEX_EMPTY_FOLDER/jdlint.toml b/tests/JDEX_EMPTY_FOLDER/jdlint.toml index 4a8214e..27803d9 100644 --- a/tests/JDEX_EMPTY_FOLDER/jdlint.toml +++ b/tests/JDEX_EMPTY_FOLDER/jdlint.toml @@ -21,6 +21,7 @@ allow_arbitrary_contents = true [[system.jdex.children.children.notes]] name = "C1" format = "X.md" +id = "/=A/./=Name/" ## ########################################################### # Standard @@ -29,4 +30,5 @@ format = "X.md" name = "Area" format = "/#A/0-/=A/9 /*Area/" jdex_note = "/=A/0.00 /=Area/.md" +id = "/=A/./=Area/" allow_arbitrary_contents = true diff --git a/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/jdlint.toml b/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/jdlint.toml index 064d82d..7f69473 100644 --- a/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/jdlint.toml +++ b/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/jdlint.toml @@ -19,6 +19,7 @@ format = "/=A//*Name/" [[system.jdex.children.children.notes]] name = "C" format = "/=A//=Name/.md" +id = "/=A/./=Name/" ## ########################################################### # Standard @@ -27,4 +28,5 @@ format = "/=A//=Name/.md" name = "Area" format = "/#A/0-/=A/9 /*Area/" jdex_note = "/=A/0.00 /=Area/.md" +id = "/=A/./=Area/" allow_arbitrary_contents = true diff --git a/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/result.json b/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/result.json index 4d0283b..f27de4f 100644 --- a/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/result.json +++ b/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/result.json @@ -5,7 +5,9 @@ "type": "JDEX_FILE_WHERE_FOLDER_EXPECTED", "file": "files/3", "matched_pattern": { - "name": "A", + "name": [ + "A" + ], "format": "/#A/" } }, @@ -13,7 +15,10 @@ "type": "JDEX_FILE_WHERE_FOLDER_EXPECTED", "file": "files/1/1B", "matched_pattern": { - "name": "B", + "name": [ + "A", + "B" + ], "format": "/=A//*Name/" } } diff --git a/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/jdlint.toml b/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/jdlint.toml index 4bde85e..99308b3 100644 --- a/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/jdlint.toml +++ b/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/jdlint.toml @@ -19,6 +19,7 @@ format = "/=A//*Name/" [[system.jdex.children.children.notes]] name = "JDex Note" format = "/=A//=Name/.md" +id = "/=A/./=Name/" ## ########################################################### # Standard @@ -27,4 +28,5 @@ format = "/=A//=Name/.md" name = "Area" format = "/#A/0-/=A/9 /*Area/" jdex_note = "/=A/0.00 /=Area/.md" +id = "/=A/./=Area/" allow_arbitrary_contents = true diff --git a/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/result.json b/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/result.json index 24be0a7..6a36c14 100644 --- a/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/result.json +++ b/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/result.json @@ -5,7 +5,11 @@ "type": "JDEX_FOLDER_WHERE_NOTE_EXPECTED", "file": "files/1/1B/1B.md", "matched_pattern": { - "name": "JDex Note", + "name": [ + "A", + "B", + "JDex Note" + ], "format": "/=A//=Name/.md" } } From 796cfc4da0e065feeffc9d7383268c33055b400b Mon Sep 17 00:00:00 2001 From: SiriusStarr <2049163+SiriusStarr@users.noreply.github.com> Date: Thu, 16 Jul 2026 12:44:13 -0700 Subject: [PATCH 07/23] Add initial parsing of system files --- jdlint.py | 382 ++++++++++-------- .../files/1/1A/1A/.placeholder} | 0 .../files/1/1A/File | 0 .../files/1/1A/Folder/.placeholder | 0 .../files/1/B1/File | 0 .../files/1/B1/Folder/.placeholder | 0 .../files/1/B1/X.md | 0 .../files/1/File | 0 .../files/1/Folder/.placeholder | 0 .../jdlint.toml | 28 ++ .../result.json | 75 ++++ tests/EMPTY_FOLDER/files/1/1A/.placeholder | 0 tests/EMPTY_FOLDER/jdlint.toml | 18 + tests/EMPTY_FOLDER/result.json | 11 + .../FILE_WHERE_FOLDER_EXPECTED/files/1/1A/1A | 0 tests/FILE_WHERE_FOLDER_EXPECTED/files/1/1B | 0 tests/FILE_WHERE_FOLDER_EXPECTED/jdlint.toml | 21 + tests/FILE_WHERE_FOLDER_EXPECTED/result.json | 30 ++ .../jdlint.toml | 9 +- .../result.json | 2 +- tests/JDEX_DUPLICATE_ID/jdlint.toml | 9 +- tests/JDEX_DUPLICATE_ID/result.json | 2 +- tests/JDEX_EMPTY_FOLDER/jdlint.toml | 9 +- tests/JDEX_EMPTY_FOLDER/result.json | 2 +- .../jdlint.toml | 9 +- .../result.json | 12 +- .../jdlint.toml | 9 +- .../result.json | 2 +- 28 files changed, 427 insertions(+), 203 deletions(-) rename tests/{JDEX_FILE_WHERE_FOLDER_EXPECTED/files/3 => ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/1A/1A/.placeholder} (100%) create mode 100644 tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/1A/File create mode 100644 tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/1A/Folder/.placeholder create mode 100644 tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/B1/File create mode 100644 tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/B1/Folder/.placeholder create mode 100644 tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/B1/X.md create mode 100644 tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/File create mode 100644 tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/Folder/.placeholder create mode 100644 tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml create mode 100644 tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/result.json create mode 100644 tests/EMPTY_FOLDER/files/1/1A/.placeholder create mode 100644 tests/EMPTY_FOLDER/jdlint.toml create mode 100644 tests/EMPTY_FOLDER/result.json create mode 100644 tests/FILE_WHERE_FOLDER_EXPECTED/files/1/1A/1A create mode 100644 tests/FILE_WHERE_FOLDER_EXPECTED/files/1/1B create mode 100644 tests/FILE_WHERE_FOLDER_EXPECTED/jdlint.toml create mode 100644 tests/FILE_WHERE_FOLDER_EXPECTED/result.json diff --git a/jdlint.py b/jdlint.py index ef362fa..0f4626e 100755 --- a/jdlint.py +++ b/jdlint.py @@ -9,17 +9,16 @@ import json import os import re -import typing import sys +import typing from dataclasses import dataclass from pathlib import Path, PurePath -from typing import TYPE_CHECKING, Any, Literal, TypeVar +from typing import TYPE_CHECKING, Literal, TypeAlias, TypeVar if TYPE_CHECKING: from collections.abc import Callable import tomllib - ############################################################################### # Exceptions ############################################################################### @@ -239,6 +238,20 @@ def __init__(self, from_file: dict) -> None: for i, v in enumerate(from_file.pop("roots", [])) ] + accum_names = {} + accum_paths = {} + for root in self.roots: + if root.name in accum_names: + raise ConfigConflictError( + "system.roots", + f"System root names must be unique. {root.name} occurs multiple times.", + ) + if root.path in accum_paths: + raise ConfigConflictError( + "system.roots", + f"System root paths must be unique. {root.path} occurs multiple times.", + ) + if "jdex" in from_file: self.jdex = ConfigSystemJDex("system.jdex", from_file.pop("jdex")) else: @@ -271,7 +284,6 @@ def __init__( from_file: str, ) -> None: """Create a valid format given a string from a config file.""" - # Validate if not isinstance(from_file, str): raise ConfigTypeError( @@ -623,6 +635,22 @@ def __init__(self, from_file): ############################################################################### +@dataclass(frozen=True) +class Issue: + """A single error detected in the system.""" + + file: PurePath + type = None + + def display(self) -> str: + """Display this particular instance of an error.""" + raise NotImplementedError + + def explain(self) -> _Explanation: + """Explain what this error is.""" + raise NotImplementedError + + @dataclass(frozen=True) class JDexIssue: """A single error detected in the JDex.""" @@ -639,6 +667,64 @@ def explain(self) -> _Explanation: raise NotImplementedError +@dataclass(frozen=True) +class IssueEmptyFolder(Issue): + """A folder is completely empty (and is not arbitrary content).""" + + type: Literal["EMPTY_FOLDER"] = "EMPTY_FOLDER" + + def display(self) -> str: + """Display this particular instance of an error.""" + return f"{self.file!s}" + + def explain(self) -> _Explanation: + """Explain what this error is.""" + return _Explanation( + explanation="A folder that matched a pattern in the system has no contents.", + fix="If this folder is unused, it shouldn't exist. Remove it or explicitly set it ignored if it must exist.", + ) + + +@dataclass(frozen=True) +class IssueFileWhereFolderExpected(Issue): + """A file that matched an expected folder was found.""" + + matched_pattern: ContentPattern + type: Literal["FILE_WHERE_FOLDER_EXPECTED"] = "FILE_WHERE_FOLDER_EXPECTED" + + def display(self) -> str: + """Display this particular instance of an error.""" + return f'{self.file!s} (matched "{self.matched_pattern}")' + + def explain(self) -> _Explanation: + """Explain what this error is.""" + return _Explanation( + explanation="A file was found that matched the format of an expected child folder.", + fix="Your format should not mix folders and notes that share a naming scheme.", + ) + + +@dataclass(frozen=True) +class IssueArbitraryContentWhereNotAllowed(Issue): + """Content was found in the that didn't match any expected format.""" + + possible_formats: tuple[ContentPattern, ...] + type: Literal["ARBITRARY_CONTENT_WHERE_NOT_ALLOWED"] = ( + "ARBITRARY_CONTENT_WHERE_NOT_ALLOWED" + ) + + def display(self) -> str: + """Display this particular instance of an error.""" + return f'{self.file!s} (matched none of "{self.possible_formats}")' + + def explain(self) -> _Explanation: + """Explain what this error is.""" + return _Explanation( + explanation="Files or folders were found that matched no expected format.", + fix="You should either make the content match or set allow_arbitrary_content to true if it is intended for random content to be mixed in.", + ) + + @dataclass(frozen=True) class AreaDifferentFromJDex: """An area with a differently-named JDex entry.""" @@ -971,120 +1057,6 @@ def explain(self) -> _Explanation: @dataclass(frozen=True) -class JDexAreaHeaderDifferentFromArea: - """An area header with a different name than the correspnoding area.""" - - area: str - jdex_name: str - type: Literal["JDEX_AREA_HEADER_DIFFERENT_FROM_AREA"] = ( - "JDEX_AREA_HEADER_DIFFERENT_FROM_AREA" - ) - - def display(self, files: list[File]) -> str: - """Display this particular instance of an error.""" - return f"{_print_nest(files[0])} [JDex name: {self.jdex_name}]" - - def explain(self) -> _Explanation: - """Explain what this error is.""" - return _Explanation( - explanation="An area header was found, the name of which is different from its corresponding JDex entry.", - fix="Update the one that is incorrect.", - ) - - -@dataclass(frozen=True) -class JDexAreaHeaderWithoutArea: - """An area header with no corresponding area.""" - - area: str - type: Literal["JDEX_AREA_HEADER_WITHOUT_AREA"] = "JDEX_AREA_HEADER_WITHOUT_AREA" - - def display(self, files: list[File]) -> str: - """Display this particular instance of an error.""" - return f"{_print_nest(files[0])} [area: {_print_area(self.area)}]" - - def explain(self) -> _Explanation: - """Explain what this error is.""" - return _Explanation( - explanation="An area header was found in the JDex with no corresponding area entry.", - fix="Go add a corresponding entry to your JDex, or delete this header if it is no longer needed.", - ) - - -@dataclass(frozen=True) -class JDexCategoryInWrongArea: - """A JDex category that, by its number, has been put in the wrong area.""" - - category_area: str - file_area: str - type: Literal["JDEX_CATEGORY_IN_WRONG_AREA"] = "JDEX_CATEGORY_IN_WRONG_AREA" - - def display(self, files: list[File]) -> str: - """Given the file's name, print the error message for it.""" - return f"{_print_nest(files[0])} [in {_print_area(self.file_area)} but should be in {_print_area(self.category_area)}]" - - def explain(self) -> _Explanation: - """Explain what this error is.""" - return _Explanation( - explanation="Some JDex categories are in the wrong area.", - fix="Move them into the correct area folder, or use a flat JDex structure.", - ) - - -@dataclass(frozen=True) -class JDexDuplicateArea: - """A JDex area that has been used multiple times.""" - - area: str - type: Literal["JDEX_DUPLICATE_AREA"] = "JDEX_DUPLICATE_AREA" - - def display(self, files: list[File]) -> str: - """Display this particular instance of an error.""" - return f"Area {_print_area(self.area)}:\n " + "\n ".join( - [_print_nest(f) for f in files], - ) - - def explain(self) -> _Explanation: - """Explain what this error is.""" - return _Explanation( - explanation="Duplicate areas were used in the JDex.", - fix="Assign a new area to one of them.", - ) - - -@dataclass(frozen=True) -class JDexDuplicateAreaHeader: - """Multiple headers for the same area.""" - - area: str - type: Literal["JDEX_DUPLICATE_AREA_HEADER"] = "JDEX_DUPLICATE_AREA_HEADER" - - def display(self, files: list[File]) -> str: - """Display this particular instance of an error.""" - return f"Area {_print_area(self.area)}:\n " + "\n ".join( - [_print_nest(f) for f in files], - ) - - def explain(self) -> _Explanation: - """Explain what this error is.""" - return _Explanation( - explanation="Duplicate headers were found for the same area in the JDex.", - fix="Delete the one that is incorrect or fix the area number.", - ) - - -@dataclass(frozen=True) -class JDexDuplicateCategory: - """A JDex category that has been used multiple times.""" - - category: str - type: Literal["JDEX_DUPLICATE_CATEGORY"] = "JDEX_DUPLICATE_CATEGORY" - - def display(self, files: list[File]) -> str: - """Display this particular instance of an error.""" - return ( - self.category + ":\n " + "\n ".join([_print_nest(f) for f in files]) - ) class File: """A file or folder that has been detected by jdlint.""" @@ -1203,41 +1175,17 @@ class _Explanation: fix: str -@dataclass(frozen=True) -class Error: - """A single error detected.""" - - error: ErrorType - files: list[File] - - def type(self) -> str: - """Return the name (type) of the error.""" - return self.error.type - - def display(self) -> str: - """Display this particular instance of an error.""" - return self.error.display(self.files) - - def explain(self) -> _Explanation: - """Explain what this error is.""" - return self.error.explain() - - -@dataclass(frozen=True) -class StructureTree: - """A node in the tree of the system.""" - - name: str - children: list[StructureTree] +StructureTree: TypeAlias = dict[PurePath, "StructureTree"] @dataclass(frozen=True) class LintResults: - """All errors returned from linting files, as well as a tree of the structure of the JD system.""" + """All errors returned from linting files, as well as the JDex and filesystems structures.""" - errors: list[Error] + errors: dict[str, list[Issue]] jdex_errors: list[JDexIssue] - structure: list[StructureTree] + jdex: dict[str, list[PurePath]] + structure: dict[str, StructureTree] @dataclass @@ -1277,8 +1225,20 @@ def default(self, o: object) -> object: return super().default(o) -def _sort_error(e: JDexIssue) -> tuple[str, tuple[tuple[str, ...], str]]: +def _sort_jdex_error(e: JDexIssue) -> tuple[str, tuple[tuple[str, ...], str]]: + # Sort errors alphabetically by type, then by file affected + # This is split from _sort_error for type-checking nonsense + if e.type is None: + raise NotImplementedError + return ( + e.type, + (e.file.parent.parts, e.file.name), + ) + + +def _sort_error(e: Issue) -> tuple[str, tuple[tuple[str, ...], str]]: # Sort errors alphabetically by type, then by file affected + # This is split from _sort_jdex_error for type-checking nonsense if e.type is None: raise NotImplementedError return ( @@ -1933,7 +1893,8 @@ def _get_jdex_notes_here_or_children( JDexIssueFileWhereFolderExpected( PurePath(x), ContentPattern( - child.format.name, child.format.raw_format + child.format.name, + child.format.raw_format, ), ), ) @@ -2000,14 +1961,13 @@ def _get_jdex_notes_here_or_children( def _process_jdex( ignored: tuple[str], - path: os.PathLike, - jdex_root: ConfigSystemJDex, + jdex: ConfigSystemJDex, ) -> tuple[dict[str, list[PurePath]], list[JDexIssue]]: (jdex_notes_by_id, jdex_errors) = _get_jdex_notes_here_or_children( - ignored, + ignored + jdex.ignore, {}, - path, - jdex_root, + jdex.path, + jdex, ) # Check for duplicate ids @@ -2016,18 +1976,124 @@ def _process_jdex( for id, ns in jdex_notes_by_id.items() if len(ns) != 1 ] - return (jdex_notes_by_id, jdex_errors + duplicate_id_errors) + return ( + jdex_notes_by_id, + jdex_errors + duplicate_id_errors, + ) + + +def _process_system_level_and_children( + ignored: tuple[str], + bound_segments: dict[str, str], + path: os.PathLike, + tier: ConfigSystem | ConfigSystemTier, + jdex: None | dict[str, list[PurePath]], +) -> tuple[StructureTree, list[Issue]]: + # Compile regexes for children + valid_children = [ + (re.compile(c.format.build_regex(bound_segments)), c) for c in tier.children + ] + + accumulated_errors = [] + accumulated_structure = {} + + has_content = False + with os.scandir(path) as contents: + for x in contents: + if _entry_is_ignored(ignored, [], x): + continue + has_content = True + for child_format, child in valid_children: + match = child_format.fullmatch(x.name) + if match: + # Is a valid child folder + if x.is_file(): + # This is an error + accumulated_errors.append( + IssueFileWhereFolderExpected( + PurePath(x), + ContentPattern( + child.format.name, + child.format.raw_format, + ), + ), + ) + break + + # Walk child + (child_structure, child_errors) = ( + _process_system_level_and_children( + ignored, + {**bound_segments, **match.groupdict()}, + PurePath(x), + child, + jdex, + ) + ) + accumulated_structure.update({PurePath(x): child_structure}) + accumulated_errors.extend(child_errors) + break + else: + # If we got here, it matched no known child/note + if not getattr(tier, "allow_arbitrary_contents", False): + # This is an error + accumulated_errors.append( + IssueArbitraryContentWhereNotAllowed( + PurePath(x), + tuple( + ContentPattern(c.format.name, c.format.raw_format) + for c in tier.children + ), + ), + ) + if not has_content: + # We have a fully empty folder; it shouldn't exist if it's doing nothing. + accumulated_errors.append(IssueEmptyFolder(PurePath(path))) + return (accumulated_structure, accumulated_errors) + + +def _process_system_root( + ignored: tuple[str], + root: ConfigSystemRoot, + system: ConfigSystem, + jdex: None | dict[str, list[PurePath]], +) -> tuple[StructureTree, list[Issue]]: + return _process_system_level_and_children( + ignored + root.ignore, + {}, + root.path, + system, + jdex, + ) def lint_system(config: Config) -> LintResults: jdex_errors = [] + jdex_notes = {} if config.system.jdex: (jdex_notes, jdex_errors) = _process_jdex( - config.linter.ignore + config.system.jdex.ignore, - config.system.jdex.path, + config.linter.ignore, config.system.jdex, ) - return LintResults([], sorted(jdex_errors, key=_sort_error), []) + errors = {} + structure = {} + for root in config.system.roots: + (root_structure, root_errors) = _process_system_root( + config.linter.ignore, + root, + config.system, + jdex_notes, + ) + if root_errors: + errors[root.name] = sorted(root_errors, key=_sort_error) + structure[root.name] = root_structure + + return LintResults( + errors, + sorted(jdex_errors, key=_sort_jdex_error), + jdex_notes, + structure, + ) if __name__ == "__main__": diff --git a/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/files/3 b/tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/1A/1A/.placeholder similarity index 100% rename from tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/files/3 rename to tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/1A/1A/.placeholder diff --git a/tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/1A/File b/tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/1A/File new file mode 100644 index 0000000..e69de29 diff --git a/tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/1A/Folder/.placeholder b/tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/1A/Folder/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/B1/File b/tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/B1/File new file mode 100644 index 0000000..e69de29 diff --git a/tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/B1/Folder/.placeholder b/tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/B1/Folder/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/B1/X.md b/tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/B1/X.md new file mode 100644 index 0000000..e69de29 diff --git a/tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/File b/tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/File new file mode 100644 index 0000000..e69de29 diff --git a/tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/Folder/.placeholder b/tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/Folder/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml b/tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml new file mode 100644 index 0000000..6dc7d01 --- /dev/null +++ b/tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml @@ -0,0 +1,28 @@ +[linter] +json_output = true + +[[system.roots]] +name = "JDex" +path = "files" + +[[system.children]] +name = "A" +format = "/#A/" +id = "/=A/" + +[[system.children.children]] +name = "B1" +format = "/*Name//=A/" +id = "/=A/./=Name/" +allow_arbitrary_contents = true + +[[system.children.children]] +name = "B2" +id = "/=A/./=Name/" +format = "/=A//*Name/" + +[[system.children.children.children]] +name = "C2" +format = "/=A//=Name/" +id = "/=A/./=Name/" +allow_arbitrary_contents = true diff --git a/tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/result.json b/tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/result.json new file mode 100644 index 0000000..a191615 --- /dev/null +++ b/tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/result.json @@ -0,0 +1,75 @@ +{ + "errors": { + "JDex": [ + { + "type": "ARBITRARY_CONTENT_WHERE_NOT_ALLOWED", + "file": "files/1/File", + "possible_formats": [ + { + "name": [ + "A", + "B1" + ], + "format": "/*Name//=A/" + }, + { + "name": [ + "A", + "B2" + ], + "format": "/=A//*Name/" + } + ] + }, + { + "type": "ARBITRARY_CONTENT_WHERE_NOT_ALLOWED", + "file": "files/1/Folder", + "possible_formats": [ + { + "name": [ + "A", + "B1" + ], + "format": "/*Name//=A/" + }, + { + "name": [ + "A", + "B2" + ], + "format": "/=A//*Name/" + } + ] + }, + { + "type": "ARBITRARY_CONTENT_WHERE_NOT_ALLOWED", + "file": "files/1/1A/File", + "possible_formats": [ + { + "name": [ + "A", + "B2", + "C2" + ], + "format": "/=A//=Name/" + } + ] + }, + { + "type": "ARBITRARY_CONTENT_WHERE_NOT_ALLOWED", + "file": "files/1/1A/Folder", + "possible_formats": [ + { + "name": [ + "A", + "B2", + "C2" + ], + "format": "/=A//=Name/" + } + ] + } + ] + }, + "jdex_errors": [] +} \ No newline at end of file diff --git a/tests/EMPTY_FOLDER/files/1/1A/.placeholder b/tests/EMPTY_FOLDER/files/1/1A/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/tests/EMPTY_FOLDER/jdlint.toml b/tests/EMPTY_FOLDER/jdlint.toml new file mode 100644 index 0000000..294f8dd --- /dev/null +++ b/tests/EMPTY_FOLDER/jdlint.toml @@ -0,0 +1,18 @@ +[linter] +json_output = true +ignore = [".placeholder"] + +[[system.roots]] +name = "JDex" +path = "files" + +[[system.children]] +name = "A" +format = "/#A/" +id = "/=A/" + +[[system.children.children]] +name = "B" +format = "/=A//*Name/" +id = "/=A/./=Name/" +allow_arbitrary_contents = true diff --git a/tests/EMPTY_FOLDER/result.json b/tests/EMPTY_FOLDER/result.json new file mode 100644 index 0000000..909c596 --- /dev/null +++ b/tests/EMPTY_FOLDER/result.json @@ -0,0 +1,11 @@ +{ + "errors": { + "JDex": [ + { + "type": "EMPTY_FOLDER", + "file": "files/1/1A" + } + ] + }, + "jdex_errors": [] +} \ No newline at end of file diff --git a/tests/FILE_WHERE_FOLDER_EXPECTED/files/1/1A/1A b/tests/FILE_WHERE_FOLDER_EXPECTED/files/1/1A/1A new file mode 100644 index 0000000..e69de29 diff --git a/tests/FILE_WHERE_FOLDER_EXPECTED/files/1/1B b/tests/FILE_WHERE_FOLDER_EXPECTED/files/1/1B new file mode 100644 index 0000000..e69de29 diff --git a/tests/FILE_WHERE_FOLDER_EXPECTED/jdlint.toml b/tests/FILE_WHERE_FOLDER_EXPECTED/jdlint.toml new file mode 100644 index 0000000..830f230 --- /dev/null +++ b/tests/FILE_WHERE_FOLDER_EXPECTED/jdlint.toml @@ -0,0 +1,21 @@ +[linter] +json_output = true + +[[system.roots]] +name = "JDex" +path = "files" + +[[system.children]] +name = "A" +format = "/#A/" +id = "/=A/" + +[[system.children.children]] +name = "B" +format = "/=A//*Name/" +id = "/=A/./=Name/" + +[[system.children.children.children]] +name = "C" +format = "/=A//=Name/" +id = "/=A/./=Name/" diff --git a/tests/FILE_WHERE_FOLDER_EXPECTED/result.json b/tests/FILE_WHERE_FOLDER_EXPECTED/result.json new file mode 100644 index 0000000..69df4c5 --- /dev/null +++ b/tests/FILE_WHERE_FOLDER_EXPECTED/result.json @@ -0,0 +1,30 @@ +{ + "errors": { + "JDex": [ + { + "type": "FILE_WHERE_FOLDER_EXPECTED", + "file": "files/1/1B", + "matched_pattern": { + "name": [ + "A", + "B" + ], + "format": "/=A//*Name/" + } + }, + { + "type": "FILE_WHERE_FOLDER_EXPECTED", + "file": "files/1/1A/1A", + "matched_pattern": { + "name": [ + "A", + "B", + "C" + ], + "format": "/=A//=Name/" + } + } + ] + }, + "jdex_errors": [] +} \ No newline at end of file diff --git a/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml b/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml index 6d3ad33..f7970f6 100644 --- a/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml +++ b/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml @@ -31,12 +31,9 @@ name = "C2" format = "/=A//=Name/.md" id = "/=A/./=Name/" -## ########################################################### -# Standard -## ########################################################### [[system.children]] name = "Area" -format = "/#A/0-/=A/9 /*Area/" -jdex_note = "/=A/0.00 /=Area/.md" -id = "/=A/./=Area/" +format = "/#A/" +jdex_note = "/=A/.md" +id = "/=A/" allow_arbitrary_contents = true diff --git a/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/result.json b/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/result.json index 4e8d438..52777c2 100644 --- a/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/result.json +++ b/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/result.json @@ -1,5 +1,5 @@ { - "errors": [], + "errors": {}, "jdex_errors": [ { "type": "JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED", diff --git a/tests/JDEX_DUPLICATE_ID/jdlint.toml b/tests/JDEX_DUPLICATE_ID/jdlint.toml index 2aad4ae..332d633 100644 --- a/tests/JDEX_DUPLICATE_ID/jdlint.toml +++ b/tests/JDEX_DUPLICATE_ID/jdlint.toml @@ -17,12 +17,9 @@ name = "B" format = "/=A//#ID//*Name/.md" id = "/=A/./=ID/" -## ########################################################### -# Standard -## ########################################################### [[system.children]] name = "Area" -format = "/#A/0-/=A/9 /*Area/" -jdex_note = "/=A/0.00 /=Area/.md" -id = "/=A/./=Area/" +format = "/#A/" +jdex_note = "/=A/.md" +id = "/=A/" allow_arbitrary_contents = true diff --git a/tests/JDEX_DUPLICATE_ID/result.json b/tests/JDEX_DUPLICATE_ID/result.json index 6feafb5..73469a5 100644 --- a/tests/JDEX_DUPLICATE_ID/result.json +++ b/tests/JDEX_DUPLICATE_ID/result.json @@ -1,5 +1,5 @@ { - "errors": [], + "errors": {}, "jdex_errors": [ { "type": "JDEX_DUPLICATE_ID", diff --git a/tests/JDEX_EMPTY_FOLDER/jdlint.toml b/tests/JDEX_EMPTY_FOLDER/jdlint.toml index 27803d9..403c771 100644 --- a/tests/JDEX_EMPTY_FOLDER/jdlint.toml +++ b/tests/JDEX_EMPTY_FOLDER/jdlint.toml @@ -23,12 +23,9 @@ name = "C1" format = "X.md" id = "/=A/./=Name/" -## ########################################################### -# Standard -## ########################################################### [[system.children]] name = "Area" -format = "/#A/0-/=A/9 /*Area/" -jdex_note = "/=A/0.00 /=Area/.md" -id = "/=A/./=Area/" +format = "/#A/" +jdex_note = "/=A/.md" +id = "/=A/" allow_arbitrary_contents = true diff --git a/tests/JDEX_EMPTY_FOLDER/result.json b/tests/JDEX_EMPTY_FOLDER/result.json index 73ea4fb..7e27b22 100644 --- a/tests/JDEX_EMPTY_FOLDER/result.json +++ b/tests/JDEX_EMPTY_FOLDER/result.json @@ -1,5 +1,5 @@ { - "errors": [], + "errors": {}, "jdex_errors": [ { "type": "JDEX_EMPTY_FOLDER", diff --git a/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/jdlint.toml b/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/jdlint.toml index 7f69473..b7f4085 100644 --- a/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/jdlint.toml +++ b/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/jdlint.toml @@ -21,12 +21,9 @@ name = "C" format = "/=A//=Name/.md" id = "/=A/./=Name/" -## ########################################################### -# Standard -## ########################################################### [[system.children]] name = "Area" -format = "/#A/0-/=A/9 /*Area/" -jdex_note = "/=A/0.00 /=Area/.md" -id = "/=A/./=Area/" +format = "/#A/" +jdex_note = "/=A/.md" +id = "/=A/" allow_arbitrary_contents = true diff --git a/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/result.json b/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/result.json index f27de4f..594f93b 100644 --- a/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/result.json +++ b/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/result.json @@ -1,16 +1,6 @@ { - "errors": [], + "errors": {}, "jdex_errors": [ - { - "type": "JDEX_FILE_WHERE_FOLDER_EXPECTED", - "file": "files/3", - "matched_pattern": { - "name": [ - "A" - ], - "format": "/#A/" - } - }, { "type": "JDEX_FILE_WHERE_FOLDER_EXPECTED", "file": "files/1/1B", diff --git a/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/jdlint.toml b/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/jdlint.toml index 99308b3..1641385 100644 --- a/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/jdlint.toml +++ b/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/jdlint.toml @@ -21,12 +21,9 @@ name = "JDex Note" format = "/=A//=Name/.md" id = "/=A/./=Name/" -## ########################################################### -# Standard -## ########################################################### [[system.children]] name = "Area" -format = "/#A/0-/=A/9 /*Area/" -jdex_note = "/=A/0.00 /=Area/.md" -id = "/=A/./=Area/" +format = "/#A/" +jdex_note = "/=A/.md" +id = "/=A/" allow_arbitrary_contents = true diff --git a/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/result.json b/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/result.json index 6a36c14..62f7d4d 100644 --- a/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/result.json +++ b/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/result.json @@ -1,5 +1,5 @@ { - "errors": [], + "errors": {}, "jdex_errors": [ { "type": "JDEX_FOLDER_WHERE_NOTE_EXPECTED", From eb11cfb75eba642aa7f9268077d2aa08c08ae480 Mon Sep 17 00:00:00 2001 From: SiriusStarr <2049163+SiriusStarr@users.noreply.github.com> Date: Fri, 17 Jul 2026 10:05:58 -0700 Subject: [PATCH 08/23] Add duplicate ID detection --- jdlint.py | 66 ++++++++++++++++--- .../jdlint.toml | 2 +- tests/DUPLICATE_ID/files/1/11 A/.placeholder | 0 tests/DUPLICATE_ID/files/1/11 B/.placeholder | 0 tests/DUPLICATE_ID/files/B1/11 A/.placeholder | 0 tests/DUPLICATE_ID/files/B1/11 B/.placeholder | 0 tests/DUPLICATE_ID/jdlint.toml | 28 ++++++++ tests/DUPLICATE_ID/result.json | 27 ++++++++ 8 files changed, 114 insertions(+), 9 deletions(-) create mode 100644 tests/DUPLICATE_ID/files/1/11 A/.placeholder create mode 100644 tests/DUPLICATE_ID/files/1/11 B/.placeholder create mode 100644 tests/DUPLICATE_ID/files/B1/11 A/.placeholder create mode 100644 tests/DUPLICATE_ID/files/B1/11 B/.placeholder create mode 100644 tests/DUPLICATE_ID/jdlint.toml create mode 100644 tests/DUPLICATE_ID/result.json diff --git a/jdlint.py b/jdlint.py index 0f4626e..80b3d12 100755 --- a/jdlint.py +++ b/jdlint.py @@ -725,6 +725,26 @@ def explain(self) -> _Explanation: ) +@dataclass(frozen=True) +class IssueDuplicateId(Issue): + """An ID that has been used multiple times.""" + + files: tuple[PurePath, ...] + id: str + type: Literal["DUPLICATE_ID"] = "DUPLICATE_ID" + + def display(self) -> str: + """Display this particular instance of an error.""" + return f"{self.id}:\n " + "\n ".join([str(f.name) for f in self.files]) + + def explain(self) -> _Explanation: + """Explain what this error is.""" + return _Explanation( + explanation="Duplicate IDs were used.", + fix="Assign a new ID to one of them.", + ) + + @dataclass(frozen=True) class AreaDifferentFromJDex: """An area with a differently-named JDex entry.""" @@ -1175,7 +1195,12 @@ class _Explanation: fix: str -StructureTree: TypeAlias = dict[PurePath, "StructureTree"] +@dataclass(frozen=True) +class SystemFolder: + """A folder detected in a JD root, including its path and its children (by ID)""" + + path: PurePath + children: dict[str, list[SystemFolder]] @dataclass(frozen=True) @@ -1185,7 +1210,7 @@ class LintResults: errors: dict[str, list[Issue]] jdex_errors: list[JDexIssue] jdex: dict[str, list[PurePath]] - structure: dict[str, StructureTree] + structure: dict[str, dict[str, list[SystemFolder]]] @dataclass @@ -1904,7 +1929,7 @@ def _get_jdex_notes_here_or_children( (child_notes, child_errors) = _get_jdex_notes_here_or_children( ignored, {**bound_segments, **match.groupdict()}, - PurePath(x.path), + PurePath(x), child, ) for id, notes in child_notes.items(): @@ -1932,7 +1957,7 @@ def _get_jdex_notes_here_or_children( # Create note entry _insert_append( note.id.build_id({**bound_segments, **match.groupdict()}), - PurePath(x.path), + PurePath(x), accumulated_notes, ) break @@ -1988,7 +2013,7 @@ def _process_system_level_and_children( path: os.PathLike, tier: ConfigSystem | ConfigSystemTier, jdex: None | dict[str, list[PurePath]], -) -> tuple[StructureTree, list[Issue]]: +) -> tuple[dict[str, list[SystemFolder]], list[Issue]]: # Compile regexes for children valid_children = [ (re.compile(c.format.build_regex(bound_segments)), c) for c in tier.children @@ -2030,7 +2055,12 @@ def _process_system_level_and_children( jdex, ) ) - accumulated_structure.update({PurePath(x): child_structure}) + + _insert_append( + child.id.build_id({**bound_segments, **match.groupdict()}), + SystemFolder(PurePath(x), child_structure), + accumulated_structure, + ) accumulated_errors.extend(child_errors) break else: @@ -2052,19 +2082,39 @@ def _process_system_level_and_children( return (accumulated_structure, accumulated_errors) +def _flatten_tree( + acc: dict[str, list[SystemFolder]], xs: dict[str, list[SystemFolder]] +) -> None: + for id, clashes in xs.items(): + for folder in clashes: + if folder.children: + _flatten_tree(acc, folder.children) + _insert_append(id, folder, acc) + + def _process_system_root( ignored: tuple[str], root: ConfigSystemRoot, system: ConfigSystem, jdex: None | dict[str, list[PurePath]], -) -> tuple[StructureTree, list[Issue]]: - return _process_system_level_and_children( +) -> tuple[dict[str, list[SystemFolder]], list[Issue]]: + (root_structure, root_errors) = _process_system_level_and_children( ignored + root.ignore, {}, root.path, system, jdex, ) + # Check for duplicate ids + # First, we flatten, in case there are children of IDs with ID clashes (this would be stupid, but someone could set up their system that way) + flattened_by_id = {} + _flatten_tree(flattened_by_id, root_structure) + duplicate_id_errors = [ + IssueDuplicateId(ns[0].path, tuple([n.path for n in ns]), id) + for id, ns in flattened_by_id.items() + if len(ns) != 1 + ] + return (root_structure, root_errors + duplicate_id_errors) def lint_system(config: Config) -> LintResults: diff --git a/tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml b/tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml index 6dc7d01..f848783 100644 --- a/tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml +++ b/tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml @@ -24,5 +24,5 @@ format = "/=A//*Name/" [[system.children.children.children]] name = "C2" format = "/=A//=Name/" -id = "/=A/./=Name/" +id = "/=A/./=Name/ Child" allow_arbitrary_contents = true diff --git a/tests/DUPLICATE_ID/files/1/11 A/.placeholder b/tests/DUPLICATE_ID/files/1/11 A/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/tests/DUPLICATE_ID/files/1/11 B/.placeholder b/tests/DUPLICATE_ID/files/1/11 B/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/tests/DUPLICATE_ID/files/B1/11 A/.placeholder b/tests/DUPLICATE_ID/files/B1/11 A/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/tests/DUPLICATE_ID/files/B1/11 B/.placeholder b/tests/DUPLICATE_ID/files/B1/11 B/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/tests/DUPLICATE_ID/jdlint.toml b/tests/DUPLICATE_ID/jdlint.toml new file mode 100644 index 0000000..760db43 --- /dev/null +++ b/tests/DUPLICATE_ID/jdlint.toml @@ -0,0 +1,28 @@ +[linter] +json_output = true + +[[system.roots]] +name = "JDex" +path = "files" + +[[system.children]] +name = "A" +format = "/#A/" +id = "/=A/" + +[[system.children.children]] +name = "B" +format = "/=A//#ID//*Name/" +id = "/=A/./=ID/" +allow_arbitrary_contents = true + +[[system.children]] +name = "C" +format = "B/#B/" +id = "/=B/" + +[[system.children.children]] +name = "D" +format = "/=B//#ID//*Name/" +id = "/=B/./=ID/" +allow_arbitrary_contents = true diff --git a/tests/DUPLICATE_ID/result.json b/tests/DUPLICATE_ID/result.json new file mode 100644 index 0000000..e4cd213 --- /dev/null +++ b/tests/DUPLICATE_ID/result.json @@ -0,0 +1,27 @@ +{ + "errors": { + "JDex": [ + { + "type": "DUPLICATE_ID", + "file": "files/1", + "files": [ + "files/1", + "files/B1" + ], + "id": "1" + }, + { + "type": "DUPLICATE_ID", + "file": "files/1/11 A", + "files": [ + "files/1/11 A", + "files/1/11 B", + "files/B1/11 A", + "files/B1/11 B" + ], + "id": "1.1" + } + ] + }, + "jdex_errors": [] +} \ No newline at end of file From 3c84a29f709a0c08376ba7a7ef208c72816ed5c7 Mon Sep 17 00:00:00 2001 From: SiriusStarr <2049163+SiriusStarr@users.noreply.github.com> Date: Fri, 17 Jul 2026 16:25:03 -0700 Subject: [PATCH 09/23] Add checking of system IDs/match to JDex --- jdlint.py | 840 +++--------------- .../11 Me, Myself, & I/11.11 Me/Content | 0 .../11 Me, Myself, & I/11.12 Myslef/Content | 0 .../10.00 Life Admin.md | 0 .../11.00 Me, Myself, & I.md | 0 .../11 Me, Myself, & I/11.11 Me.md | 0 .../11 Me, Myself, & I/11.12 Myself.md | 0 tests/ID_DIFFERENT_FROM_JDEX/jdlint.toml | 59 ++ tests/ID_DIFFERENT_FROM_JDEX/result.json | 16 + .../11 Me, Myself, & I/11.11 Me/Content | 0 .../11 Me, Myself, & I/11.12 Myself/Content | 0 .../11 Me, Myself, & I/11.13 I/Content | 0 .../10.00 Life Admin.md | 0 .../11.00 Me, Myself, & I.md | 0 .../11 Me, Myself, & I/11.11 Me.md | 0 .../11 Me, Myself, & I/11.12 Myself.md | 0 tests/ID_NOT_IN_JDEX/jdlint.toml | 59 ++ tests/ID_NOT_IN_JDEX/result.json | 12 + .../files/1/1A/1.md | 0 .../jdlint.toml | 9 +- .../result.json | 16 + tests/JDEX_DUPLICATE_ID/files/1/1.md | 0 tests/JDEX_DUPLICATE_ID/jdlint.toml | 7 +- tests/JDEX_EMPTY_FOLDER/files/1/1B/X.md | 0 tests/JDEX_EMPTY_FOLDER/jdlint.toml | 4 +- .../files/1/1A/1.md | 0 .../jdlint.toml | 7 +- .../files/1/1A/1.md | 0 .../jdlint.toml | 7 +- 29 files changed, 313 insertions(+), 723 deletions(-) create mode 100644 tests/ID_DIFFERENT_FROM_JDEX/files/10-19 Life Admin/11 Me, Myself, & I/11.11 Me/Content create mode 100644 tests/ID_DIFFERENT_FROM_JDEX/files/10-19 Life Admin/11 Me, Myself, & I/11.12 Myslef/Content create mode 100644 tests/ID_DIFFERENT_FROM_JDEX/jdex/10-19 Life Admin/10 Management of area 10-19/10.00 Life Admin.md create mode 100644 tests/ID_DIFFERENT_FROM_JDEX/jdex/10-19 Life Admin/11 Me, Myself, & I/11.00 Me, Myself, & I.md create mode 100644 tests/ID_DIFFERENT_FROM_JDEX/jdex/10-19 Life Admin/11 Me, Myself, & I/11.11 Me.md create mode 100644 tests/ID_DIFFERENT_FROM_JDEX/jdex/10-19 Life Admin/11 Me, Myself, & I/11.12 Myself.md create mode 100644 tests/ID_DIFFERENT_FROM_JDEX/jdlint.toml create mode 100644 tests/ID_DIFFERENT_FROM_JDEX/result.json create mode 100644 tests/ID_NOT_IN_JDEX/files/10-19 Life Admin/11 Me, Myself, & I/11.11 Me/Content create mode 100644 tests/ID_NOT_IN_JDEX/files/10-19 Life Admin/11 Me, Myself, & I/11.12 Myself/Content create mode 100644 tests/ID_NOT_IN_JDEX/files/10-19 Life Admin/11 Me, Myself, & I/11.13 I/Content create mode 100644 tests/ID_NOT_IN_JDEX/jdex/10-19 Life Admin/10 Management of area 10-19/10.00 Life Admin.md create mode 100644 tests/ID_NOT_IN_JDEX/jdex/10-19 Life Admin/11 Me, Myself, & I/11.00 Me, Myself, & I.md create mode 100644 tests/ID_NOT_IN_JDEX/jdex/10-19 Life Admin/11 Me, Myself, & I/11.11 Me.md create mode 100644 tests/ID_NOT_IN_JDEX/jdex/10-19 Life Admin/11 Me, Myself, & I/11.12 Myself.md create mode 100644 tests/ID_NOT_IN_JDEX/jdlint.toml create mode 100644 tests/ID_NOT_IN_JDEX/result.json create mode 100644 tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/1A/1.md create mode 100644 tests/JDEX_DUPLICATE_ID/files/1/1.md create mode 100644 tests/JDEX_EMPTY_FOLDER/files/1/1B/X.md create mode 100644 tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/files/1/1A/1.md create mode 100644 tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/files/1/1A/1.md diff --git a/jdlint.py b/jdlint.py index 80b3d12..1f75f74 100755 --- a/jdlint.py +++ b/jdlint.py @@ -13,7 +13,7 @@ import typing from dataclasses import dataclass from pathlib import Path, PurePath -from typing import TYPE_CHECKING, Literal, TypeAlias, TypeVar +from typing import TYPE_CHECKING, Literal, TypeVar if TYPE_CHECKING: from collections.abc import Callable @@ -199,7 +199,7 @@ def __init__(self, from_file: dict) -> None: type(self.disable_rules).__name__, ) for r in self.disable_rules: - if r in [e.type for e in typing.get_args(ErrorType)]: + if r in [e.type for e in typing.get_args(AnyIssueType)]: continue raise ConfigValueError("linter.disable_rules", "not a valid rule name", r) @@ -304,12 +304,12 @@ def __init__( from_file, ) - build_id = [] + build = [] for i, v in enumerate(from_file.split("/")): if i % 2 == 0: # Literal segment - build_id.append(lambda _, v=v: v) + build.append(lambda _, v=v: v) else: # Variable segment match = ConfigStaticFormat.variable_static_segment_re.fullmatch(v) @@ -321,7 +321,7 @@ def __init__( ) if match.group(1) in ancestors.segments: p = match.group(1) - build_id.append(lambda d, p=p: d[p]) + build.append(lambda d, p=p: d[p]) else: raise ConfigValueError( @@ -330,7 +330,7 @@ def __init__( v, ) - self.build_id = lambda d: "".join([f(d) for f in build_id]) + self.build = lambda d: "".join([f(d) for f in build]) class ConfigJDexNotes: @@ -351,13 +351,16 @@ def __init__( ancestors, from_file, ) - if "id" not in from_file: - raise (ConfigMissingKeyError(f"{at}.id")) - self.id = ConfigStaticFormat( - f"{at}.id", - self.format, - from_file.pop("id"), - ) + if "ids" not in from_file: + raise (ConfigMissingKeyError(f"{at}.ids")) + self.ids = [ + ConfigStaticFormat( + f"{at}.ids[{i}]", + self.format, + v, + ) + for i, v in enumerate(from_file.pop("ids", [])) + ] # Ensure no extra fields for key in from_file: @@ -726,7 +729,7 @@ def explain(self) -> _Explanation: @dataclass(frozen=True) -class IssueDuplicateId(Issue): +class IssueDuplicateID(Issue): """An ID that has been used multiple times.""" files: tuple[PurePath, ...] @@ -745,6 +748,46 @@ def explain(self) -> _Explanation: ) +@dataclass(frozen=True) +class IssueIDNotInJDex(Issue): + """An ID without a corresponding JDex entry.""" + + id: str + type: Literal["ID_NOT_IN_JDEX"] = "ID_NOT_IN_JDEX" + + def display(self) -> str: + """Display this particular instance of an error.""" + return f"{self.file} [ID: {self.id}]" + + def explain(self) -> _Explanation: + """Explain what this error is.""" + return _Explanation( + explanation="An ID was found in files that is missing from the JDex.", + fix="Go add a corresponding entry to your JDex.", + ) + + +@dataclass(frozen=True) +class IssueIDDifferentFromJDex(Issue): + """An ID with a differently-named JDex entry.""" + + id: str + expected_jdex_note: str + known_jdex_notes: list[PurePath] + type: Literal["ID_DIFFERENT_FROM_JDEX"] = "ID_DIFFERENT_FROM_JDEX" + + def display(self) -> str: + """Display this particular instance of an error.""" + return f"{self.id}: {self.file} [Expected JDex: {self.expected_jdex_note}i; actual JDex: {self.known_jdex_notes}]" + + def explain(self) -> _Explanation: + """Explain what this error is.""" + return _Explanation( + explanation="An ID was found, the name of which is different from its corresponding JDex entry.", + fix="Update the one that is incorrect.", + ) + + @dataclass(frozen=True) class AreaDifferentFromJDex: """An area with a differently-named JDex entry.""" @@ -843,67 +886,6 @@ def explain(self) -> _Explanation: ) -@dataclass(frozen=True) -class DuplicateArea: - """An area that has been used multiple times.""" - - area: str - type: Literal["DUPLICATE_AREA"] = "DUPLICATE_AREA" - - def display(self, files: list[File]) -> str: - """Display this particular instance of an error.""" - return f"Area {_print_area(self.area)}:\n " + "\n ".join( - [_print_nest(f) for f in files], - ) - - def explain(self) -> _Explanation: - """Explain what this error is.""" - return _Explanation( - explanation="Duplicate areas were used.", - fix="Assign a new area to one of them.", - ) - - -@dataclass(frozen=True) -class DuplicateCategory: - """A category that has been used multiple times.""" - - category: str - type: Literal["DUPLICATE_CATEGORY"] = "DUPLICATE_CATEGORY" - - def display(self, files: list[File]) -> str: - """Display this particular instance of an error.""" - return f"Category {self.category}:\n " + "\n ".join( - [_print_nest(f) for f in files], - ) - - def explain(self) -> _Explanation: - """Explain what this error is.""" - return _Explanation( - explanation="Duplicate categories were used.", - fix="Assign a new category to one of them.", - ) - - -@dataclass(frozen=True) -class DuplicateId: - """An ID that has been used multiple times.""" - - id: str - type: Literal["DUPLICATE_ID"] = "DUPLICATE_ID" - - def display(self, files: list[File]) -> str: - """Display this particular instance of an error.""" - return f"ID {self.id}:\n " + "\n ".join([_print_nest(f) for f in files]) - - def explain(self) -> _Explanation: - """Explain what this error is.""" - return _Explanation( - explanation="Duplicate IDs were used.", - fix="Assign a new ID to one of them.", - ) - - @dataclass(frozen=True) class FileOutsideId: """A file was encountered not in a terminal ID folder.""" @@ -922,67 +904,6 @@ def explain(self) -> _Explanation: ) -@dataclass(frozen=True) -class IdDifferentFromJDex: - """An ID with a differently-named JDex entry.""" - - id: str - jdex_name: str - type: Literal["ID_DIFFERENT_FROM_JDEX"] = "ID_DIFFERENT_FROM_JDEX" - - def display(self, files: list[File]) -> str: - """Display this particular instance of an error.""" - return f"{_print_nest(files[0])} [JDex name: {self.jdex_name}]" - - def explain(self) -> _Explanation: - """Explain what this error is.""" - return _Explanation( - explanation="An ID was found, the name of which is different from its corresponding JDex entry.", - fix="Update the one that is incorrect.", - ) - - -@dataclass(frozen=True) -class IdInWrongCategory: - """An ID that, by its number, has been put in the wrong category.""" - - id_ac: str - file_ac: str - type: Literal["ID_IN_WRONG_CATEGORY"] = "ID_IN_WRONG_CATEGORY" - - def display(self, files: list[File]) -> str: - """Display this particular instance of an error.""" - return ( - f"{_print_nest(files[0])} [in {self.file_ac} but should be in {self.id_ac}]" - ) - - def explain(self) -> _Explanation: - """Explain what this error is.""" - return _Explanation( - explanation="Some IDs are in the wrong category.", - fix="Move them into the correct category folder.", - ) - - -@dataclass(frozen=True) -class IdNotInJDex: - """An ID without a corresponding JDex entry.""" - - id: str - type: Literal["ID_NOT_IN_JDEX"] = "ID_NOT_IN_JDEX" - - def display(self, files: list[File]) -> str: - """Display this particular instance of an error.""" - return f"{_print_nest(files[0])} [ID: {self.id}]" - - def explain(self) -> _Explanation: - """Explain what this error is.""" - return _Explanation( - explanation="An ID was found in the files that is missing from the JDex.", - fix="Go add a corresponding entry to your JDex.", - ) - - @dataclass(frozen=True) class InvalidAreaName: """A folder at the area level that doesn't match the normal format.""" @@ -1056,26 +977,6 @@ def explain(self) -> _Explanation: ) -ErrorType = ( - AreaDifferentFromJDex - | AreaNotInJDex - | CategoryDifferentFromJDex - | CategoryInWrongArea - | CategoryNotInJDex - | DuplicateArea - | DuplicateCategory - | DuplicateId - | FileOutsideId - | IdDifferentFromJDex - | IdInWrongCategory - | IdNotInJDex - | InvalidAreaName - | InvalidCategoryName - | InvalidIDName - | NonemptyInbox -) - - @dataclass(frozen=True) class File: """A file or folder that has been detected by jdlint.""" @@ -1085,7 +986,7 @@ class File: @dataclass(frozen=True) -class JDexIssueDuplicateId(JDexIssue): +class JDexIssueDuplicateID(JDexIssue): """A JDex ID that has been used multiple times.""" files: tuple[PurePath, ...] @@ -1189,6 +1090,23 @@ def explain(self) -> _Explanation: ) +JDexIssueType = ( + JDexIssueArbitraryContentWhereNotAllowed + | JDexIssueDuplicateID + | JDexIssueEmptyFolder + | JDexIssueFileWhereFolderExpected + | JDexIssueFolderWhereNoteExpected +) +IssueType = ( + IssueArbitraryContentWhereNotAllowed + | IssueDuplicateID + | IssueEmptyFolder + | IssueFileWhereFolderExpected +) + +AnyIssueType = JDexIssueType | IssueType + + @dataclass(frozen=True) class _Explanation: explanation: str @@ -1272,37 +1190,6 @@ def _sort_error(e: Issue) -> tuple[str, tuple[tuple[str, ...], str]]: ) -# Any valid area folder name -valid_area_re = re.compile("([0-9])0-(?:\\1)9 (.+)") -# Any valid category folder name -generic_category_re = re.compile("([0-9])[0-9] .+") -# Any valid ID folder name -generic_id_re = re.compile("([0-9][0-9])\\.([0-9][0-9]) (.+)") -# Matches only IDs that are inboxes -inbox_re = re.compile("[0-9][0-9]\\.01 .+") - - -def _valid_category_re(a: str) -> re.Pattern: - """Match only valid categories for a given area.""" - return re.compile("(" + a + "[0-9]) (.+)") - - -def _valid_id_re(ac: str) -> re.Pattern: - """Match only valid IDs for a given area and category.""" - return re.compile("(" + ac + "\\.[0-9][0-9]) (.+)") - - -# Match area header JDex notes -jdex_note_header_re = re.compile("([0-9])0\\. (.+?)(\\.md)?") -# Match any valid ID JDex note -jdex_note_generic_id_re = re.compile("([0-9][0-9])\\.([0-9][0-9]) (.+?)(\\.md)?") - - -def _jdex_note_id_re(ac: str) -> re.Pattern: - """Match only valid JDex note IDs for a given area and category.""" - return re.compile("(" + ac + "\\.[0-9][0-9]) (.+?)(\\.md)?") - - def _entry_is_ignored( ignored: tuple[str] | None, nested_under: list[str], @@ -1388,503 +1275,6 @@ def _process_single_file_jdex(path: Path) -> _JDexResults: ) -# def _process_flat_jdex_structure( -# files: list[os.DirEntry], -# jdex: _JDexAccumulator, -# *, -# ignored: list[str] | None, -# alt_zeros: bool = False, -# ) -> None: -# """Process a JDex that is a series of flat files.""" -# area_re = re.compile( -# "0([0-9])\\.00 (.+?)( area management)?( index)?(\\.md)?" -# if alt_zeros -# else "([0-9])0\\.00 (.+?)( area management)?( index)?(\\.md)?", -# flags=re.IGNORECASE, -# ) -# category_re = re.compile( -# # We need to tolerate the "area management" suffix for a category as well, to create categories from e.g. `01.00 Life Admin Area Management` -# "([0-9][0-9])\\.00 (.+?)( (category|area) management)?( index)?(\\.md)?" -# if alt_zeros -# else "([0-9][1-9])\\.00 (.+?)( category management)?( index)?(\\.md)?", -# flags=re.IGNORECASE, -# ) - -# for jid in files: -# if _entry_is_ignored(ignored, [], jid): -# continue - -# file = File(name=jid.name, full_path=jid.path, nested_under=[]) - -# # Check if the file matches an area -# area_match = area_re.fullmatch(jid.name) -# if area_match: -# _insert_append( -# area_match.group(1), -# (area_match.group(2), file), -# jdex.areas, -# ) - -# # Check if the file matches a category -# cat_match = category_re.fullmatch(jid.name) -# if cat_match: -# _insert_append( -# cat_match.group(1), -# (cat_match.group(2), file), -# jdex.categories, -# ) - -# # Check if it's a header match for alt zeros -# header_match = jdex_note_header_re.fullmatch(jid.name) -# if header_match: -# _insert_append( -# header_match.group(1), -# (header_match.group(2), file), -# jdex.headers, -# ) -# continue - -# # The file should also be a valid ID (or is bad) -# id_match = jdex_note_generic_id_re.fullmatch(jid.name) -# if id_match: -# _insert_append( -# f"{id_match.group(1)}.{id_match.group(2)}", -# (id_match.group(3), file), -# jdex.ids, -# ) -# else: -# jdex.errors.append( -# JDexIssue(error=JDexInvalidIDName(), files=[file]), -# ) - - -# def _process_nested_jdex_structure( -# path: Path, -# jdex: _JDexAccumulator, -# root_level_files: list[os.DirEntry], -# *, -# ignored: list[str] | None, -# ) -> None: -# for area in os.scandir(path): -# if _entry_is_ignored(ignored, [], area): -# continue -# if area.is_file(): -# # Maybe we have a flat structure -# root_level_files.append(area) -# continue - -# # Otherwise, a directory, so nested structure -# area_file = File(name=area.name, full_path=area.path, nested_under=[]) -# area_match = valid_area_re.fullmatch(area.name) -# if not area_match: -# jdex.errors.append( -# JDexIssue(error=JDexInvalidAreaName(), files=[area_file]), -# ) -# continue -# _insert_append( -# area_match.group(1), -# (area_match.group(2), area_file), -# jdex.areas, -# ) -# cat_re = _valid_category_re(area_match.group(1)) -# with os.scandir(area.path) as cats_it: -# for cat in cats_it: -# if _entry_is_ignored(ignored, [area.name], cat): -# continue -# cat_file = File( -# name=cat.name, -# full_path=cat.path, -# nested_under=[area.name], -# ) -# if cat.is_file(): -# jdex.errors.append( -# JDexIssue( -# error=JDexFileOutsideCategory(), -# files=[cat_file], -# ), -# ) -# continue - -# if cat_match := cat_re.fullmatch(cat.name): -# _insert_append( -# cat_match.group(1), -# (cat_match.group(2), cat_file), -# jdex.categories, -# ) -# id_re = _jdex_note_id_re(cat_match.group(1)) -# with os.scandir(cat.path) as ids_it: -# nested_under = [area.name, cat.name] -# for jid in ids_it: -# if _entry_is_ignored(ignored, nested_under, jid): -# continue -# id_file = File( -# name=jid.name, -# full_path=jid.path, -# nested_under=nested_under, -# ) -# if id_match := id_re.fullmatch(jid.name): -# _insert_append( -# id_match.group(1), -# (id_match.group(2), id_file), -# jdex.ids, -# ) -# elif gen_match := jdex_note_generic_id_re.fullmatch( -# jid.name, -# ): -# jdex.errors.append( -# JDexIssue( -# error=JDexIdInWrongCategory( -# id_ac=gen_match.group(1), -# file_ac=cat_match.group(1), -# ), -# files=[id_file], -# ), -# ) -# else: -# jdex.errors.append( -# JDexIssue( -# error=JDexInvalidIDName(), -# files=[id_file], -# ), -# ) - -# elif gen_match := generic_category_re.fullmatch(cat.name): -# jdex.errors.append( -# JDexIssue( -# error=JDexCategoryInWrongArea( -# category_area=gen_match.group(1), -# file_area=area_match.group(1), -# ), -# files=[cat_file], -# ), -# ) -# else: -# jdex.errors.append( -# JDexIssue(error=JDexInvalidCategoryName(), files=[cat_file]), -# ) - - -# def _get_jdex_entries( -# jdex_dir: Path, -# *, -# ignored: list[str] | None, -# alt_zeros: bool = False, -# ) -> _JDexResults | list[JDexIssue]: -# """Return canonical JDex information or a list of errors for it.""" -# if jdex_dir.is_file(): -# # Single file JDex -# return _process_single_file_jdex(jdex_dir) - -# jdex: _JDexAccumulator = _JDexAccumulator() -# root_level_files: list[os.DirEntry] = [] - -# _process_nested_jdex_structure(jdex_dir, jdex, root_level_files, ignored=ignored) - -# if jdex.ids or jdex.errors: -# # Not a flat structure, so we need to add all root level files as invalid -# jdex.errors.extend( -# [ -# JDexIssue( -# error=JDexFileOutsideCategory(), -# files=[ -# File( -# name=f.name, -# full_path=f.path, -# nested_under=[], -# ), -# ], -# ) -# for f in root_level_files -# ], -# ) - -# else: -# # Nothing nested, and not a file, so assume a flat structure -# _process_flat_jdex_structure( -# root_level_files, -# jdex, -# ignored=ignored, -# alt_zeros=alt_zeros, -# ) - -# # These duplicate errors apply regardless of JDex type -# jdex.errors.extend(_error_if_dups(JDexDuplicateArea, JDexIssue, jdex.areas)) -# jdex.errors.extend( -# _error_if_dups(JDexDuplicateCategory, JDexIssue, jdex.categories), -# ) -# jdex.errors.extend(_error_if_dups(JDexDuplicateId, JDexIssue, jdex.ids)) -# jdex.errors.extend(_error_if_dups(JDexDuplicateAreaHeader, JDexIssue, jdex.headers)) - -# for header, files in jdex.headers.items(): -# if header not in jdex.areas: -# jdex.errors.append( -# JDexIssue( -# error=JDexAreaHeaderWithoutArea(area=header), -# files=[f for (_, f) in files], -# ), -# ) -# elif len(files) == 1 and files[0][0] != jdex.areas[header][0][0]: -# jdex.errors.append( -# JDexIssue( -# error=JDexAreaHeaderDifferentFromArea( -# area=header, -# jdex_name=f"{_print_area(header)} {jdex.areas[header][0][0]}", -# ), -# files=[f for (_, f) in files], -# ), -# ) - -# if jdex.errors: -# return jdex.errors -# return _JDexResults( -# areas={k: f"{_print_area(k)} {v[0][0]}" for k, v in jdex.areas.items()}, -# categories={k: f"{k} {v[0][0]}" for k, v in jdex.categories.items()}, -# ids={k: f"{k} {v[0][0]}" for k, v in jdex.ids.items()}, -# ) - - -# def lint_dir( -# path: Path, -# ignored: list[str] | None = None, -# ) -> LintResults: -# """Check a root of a JD system for issues.""" -# errors: list[Error] = [] -# used_areas: dict[str, list[tuple[str, File]]] = {} -# used_categories: dict[str, list[tuple[str, File]]] = {} -# used_ids: dict[str, list[tuple[str, File]]] = {} - -# def check_inbox(nested_under: list[str], f: os.DirEntry) -> None: -# if inbox_re.fullmatch(f.name): -# entries = len(os.listdir(f.path)) -# if entries: -# errors.append( -# Error( -# error=NonemptyInbox(num_items=entries), -# files=[ -# File( -# name=f.name, -# full_path=f.path, -# nested_under=nested_under, -# ), -# ], -# ), -# ) - -# def check_if_out_of_id(file: os.DirEntry, nested_under: list[str]) -> bool: -# if file.is_file(): -# errors.append( -# Error( -# error=FileOutsideId(), -# files=[ -# File( -# name=file.name, -# full_path=file.path, -# nested_under=nested_under, -# ), -# ], -# ), -# ) -# return True -# return False - -# with os.scandir(path) as areas_it: -# for area in areas_it: -# if _entry_is_ignored(ignored, [], area) or check_if_out_of_id(area, []): -# continue -# area_file = File( -# name=area.name, -# full_path=area.path, -# nested_under=[], -# ) -# area_match = valid_area_re.fullmatch(area.name) -# if not area_match: -# errors.append( -# Error( -# error=InvalidAreaName(), -# files=[area_file], -# ), -# ) -# continue -# # Valid area -# _insert_append( -# area_match.group(1), -# (area_match.group(2), area_file), -# used_areas, -# ) -# cat_re = _valid_category_re(area_match.group(1)) -# with os.scandir(area.path) as cats_it: -# for cat in cats_it: -# if _entry_is_ignored( -# ignored, -# [area.name], -# cat, -# ) or check_if_out_of_id(cat, [area.name]): -# continue -# cat_file = File( -# name=cat.name, -# full_path=cat.path, -# nested_under=[area.name], -# ) -# if cat_match := cat_re.fullmatch(cat.name): -# _insert_append( -# cat_match.group(1), -# (cat_match.group(2), cat_file), -# used_categories, -# ) -# id_re = _valid_id_re(cat_match.group(1)) -# with os.scandir(cat.path) as ids_it: -# nested_under = [area.name, cat.name] - -# for jid in ids_it: -# if _entry_is_ignored( -# ignored, -# nested_under, -# jid, -# ) or check_if_out_of_id(jid, nested_under): -# continue -# id_file = File( -# name=jid.name, -# full_path=jid.path, -# nested_under=nested_under, -# ) -# if id_match := id_re.fullmatch(jid.name): -# _insert_append( -# id_match.group(1), -# (id_match.group(2), id_file), -# used_ids, -# ) - -# check_inbox(nested_under, jid) -# elif gen_match := generic_id_re.fullmatch(jid.name): -# errors.append( -# Error( -# error=IdInWrongCategory( -# id_ac=gen_match.group( -# 1, -# ), -# file_ac=cat_match.group( -# 1, -# ), -# ), -# files=[id_file], -# ), -# ) - -# else: -# errors.append( -# Error( -# error=InvalidIDName(), -# files=[id_file], -# ), -# ) -# elif gen_match := generic_category_re.fullmatch(cat.name): -# errors.append( -# Error( -# error=CategoryInWrongArea( -# category_area=gen_match.group(1), -# file_area=area_match.group(1), -# ), -# files=[cat_file], -# ), -# ) -# else: -# errors.append( -# Error( -# error=InvalidCategoryName(), -# files=[cat_file], -# ), -# ) - -# errors.extend(_error_if_dups(DuplicateArea, Error, used_areas)) -# errors.extend(_error_if_dups(DuplicateCategory, Error, used_categories)) -# errors.extend(_error_if_dups(DuplicateId, Error, used_ids)) - -# return LintResults( -# errors=sorted(errors, key=_sort_error), -# used_areas=used_areas, -# used_categories=used_categories, -# used_ids=used_ids, -# ) - - -# def lint_dir_and_jdex( -# *, -# path: Path, -# jdex_path: Path, -# ignored: list[str] | None = None, -# alt_zeros: bool = False, -# ) -> tuple[list[Error], list[JDexIssue]]: -# """Check a root of a JD system and its JDex for issues.""" -# results = lint_dir(path, ignored) -# jdex = _get_jdex_entries(jdex_path, ignored=ignored, alt_zeros=alt_zeros) -# if isinstance(jdex, list): -# return (results.errors, sorted(jdex, key=_sort_error)) - -# errors = results.errors - -# for area, files in results.used_areas.items(): -# if area not in jdex.areas: -# errors.append( -# Error( -# error=AreaNotInJDex(area=area), -# files=[f for (_, f) in files], -# ), -# ) -# elif len(files) == 1 and files[0][1].name != jdex.areas[area]: -# errors.append( -# Error( -# error=AreaDifferentFromJDex( -# area=area, -# jdex_name=jdex.areas[area], -# ), -# files=[f for (_, f) in files], -# ), -# ) -# for category, files in results.used_categories.items(): -# if category not in jdex.categories: -# errors.append( -# Error( -# error=CategoryNotInJDex(category=category), -# files=[f for (_, f) in files], -# ), -# ) -# elif len(files) == 1 and files[0][1].name != jdex.categories[category]: -# errors.append( -# Error( -# error=CategoryDifferentFromJDex( -# category=category, -# jdex_name=jdex.categories[category], -# ), -# files=[f for (_, f) in files], -# ), -# ) -# for jid, files in results.used_ids.items(): -# if jid not in jdex.ids: -# errors.append( -# Error(error=IdNotInJDex(id=jid), files=[f for (_, f) in files]), -# ) -# elif len(files) == 1 and files[0][1].name != jdex.ids[jid]: -# errors.append( -# Error( -# error=IdDifferentFromJDex(id=jid, jdex_name=jdex.ids[jid]), -# files=[f for (_, f) in files], -# ), -# ) -# return (sorted(errors, key=_sort_error), []) - - -def _print_area(d: str) -> str: - """Given the number of an area, pretty-print it.""" - return f"{d}0-{d}9" - - -def _print_nest(f: File) -> str: - """Pretty-print a nested file.""" - if f.nested_under: - return str(PurePath(*f.nested_under, f.name)) - return f.name - - def _get_jdex_notes_here_or_children( ignored: tuple[str], bound_segments: dict[str, str], @@ -1955,11 +1345,12 @@ def _get_jdex_notes_here_or_children( break # Create note entry - _insert_append( - note.id.build_id({**bound_segments, **match.groupdict()}), - PurePath(x), - accumulated_notes, - ) + for id in note.ids: + _insert_append( + id.build({**bound_segments, **match.groupdict()}), + PurePath(x), + accumulated_notes, + ) break else: # If we got here, it matched no known child/note @@ -1997,7 +1388,7 @@ def _process_jdex( # Check for duplicate ids duplicate_id_errors = [ - JDexIssueDuplicateId(ns[0], tuple(ns), id) + JDexIssueDuplicateID(ns[0], tuple(ns), id) for id, ns in jdex_notes_by_id.items() if len(ns) != 1 ] @@ -2013,6 +1404,7 @@ def _process_system_level_and_children( path: os.PathLike, tier: ConfigSystem | ConfigSystemTier, jdex: None | dict[str, list[PurePath]], + by_id_dict: dict[str, list[tuple[str | None, PurePath]]], ) -> tuple[dict[str, list[SystemFolder]], list[Issue]]: # Compile regexes for children valid_children = [ @@ -2053,14 +1445,28 @@ def _process_system_level_and_children( PurePath(x), child, jdex, + by_id_dict, ) ) + child_id = child.id.build({**bound_segments, **match.groupdict()}) _insert_append( - child.id.build_id({**bound_segments, **match.groupdict()}), + child_id, SystemFolder(PurePath(x), child_structure), accumulated_structure, ) + _insert_append( + child_id, + ( + child.jdex_note.build( + {**bound_segments, **match.groupdict()} + ) + if child.jdex_note + else None, + PurePath(x), + ), + by_id_dict, + ) accumulated_errors.extend(child_errors) break else: @@ -2082,39 +1488,41 @@ def _process_system_level_and_children( return (accumulated_structure, accumulated_errors) -def _flatten_tree( - acc: dict[str, list[SystemFolder]], xs: dict[str, list[SystemFolder]] -) -> None: - for id, clashes in xs.items(): - for folder in clashes: - if folder.children: - _flatten_tree(acc, folder.children) - _insert_append(id, folder, acc) - - def _process_system_root( ignored: tuple[str], root: ConfigSystemRoot, system: ConfigSystem, jdex: None | dict[str, list[PurePath]], ) -> tuple[dict[str, list[SystemFolder]], list[Issue]]: + by_id: dict[str, list[tuple[str | None, PurePath]]] = {} (root_structure, root_errors) = _process_system_level_and_children( - ignored + root.ignore, - {}, - root.path, - system, - jdex, + ignored + root.ignore, {}, root.path, system, jdex, by_id ) - # Check for duplicate ids - # First, we flatten, in case there are children of IDs with ID clashes (this would be stupid, but someone could set up their system that way) - flattened_by_id = {} - _flatten_tree(flattened_by_id, root_structure) + + # Check for duplicate IDs duplicate_id_errors = [ - IssueDuplicateId(ns[0].path, tuple([n.path for n in ns]), id) - for id, ns in flattened_by_id.items() - if len(ns) != 1 + IssueDuplicateID(fs[0][1], tuple([f[1] for f in fs]), id) + for id, fs in by_id.items() + if len(fs) != 1 ] - return (root_structure, root_errors + duplicate_id_errors) + + # If we have a JDex, we can do some additional checks + id_errors = [] + if jdex is not None: + for id, fs in by_id.items(): + if id not in jdex: + id_errors.append(IssueIDNotInJDex(fs[0][1], id)) + else: + jdex_notes = [n.name for n in jdex[id]] + for expected_jdex_note, f in fs: + if expected_jdex_note and expected_jdex_note not in jdex_notes: + id_errors.append( + IssueIDDifferentFromJDex( + f, id, expected_jdex_note, jdex[id] + ) + ) + + return (root_structure, root_errors + duplicate_id_errors + id_errors) def lint_system(config: Config) -> LintResults: @@ -2132,7 +1540,7 @@ def lint_system(config: Config) -> LintResults: config.linter.ignore, root, config.system, - jdex_notes, + jdex_notes if config.system.jdex else None, ) if root_errors: errors[root.name] = sorted(root_errors, key=_sort_error) diff --git a/tests/ID_DIFFERENT_FROM_JDEX/files/10-19 Life Admin/11 Me, Myself, & I/11.11 Me/Content b/tests/ID_DIFFERENT_FROM_JDEX/files/10-19 Life Admin/11 Me, Myself, & I/11.11 Me/Content new file mode 100644 index 0000000..e69de29 diff --git a/tests/ID_DIFFERENT_FROM_JDEX/files/10-19 Life Admin/11 Me, Myself, & I/11.12 Myslef/Content b/tests/ID_DIFFERENT_FROM_JDEX/files/10-19 Life Admin/11 Me, Myself, & I/11.12 Myslef/Content new file mode 100644 index 0000000..e69de29 diff --git a/tests/ID_DIFFERENT_FROM_JDEX/jdex/10-19 Life Admin/10 Management of area 10-19/10.00 Life Admin.md b/tests/ID_DIFFERENT_FROM_JDEX/jdex/10-19 Life Admin/10 Management of area 10-19/10.00 Life Admin.md new file mode 100644 index 0000000..e69de29 diff --git a/tests/ID_DIFFERENT_FROM_JDEX/jdex/10-19 Life Admin/11 Me, Myself, & I/11.00 Me, Myself, & I.md b/tests/ID_DIFFERENT_FROM_JDEX/jdex/10-19 Life Admin/11 Me, Myself, & I/11.00 Me, Myself, & I.md new file mode 100644 index 0000000..e69de29 diff --git a/tests/ID_DIFFERENT_FROM_JDEX/jdex/10-19 Life Admin/11 Me, Myself, & I/11.11 Me.md b/tests/ID_DIFFERENT_FROM_JDEX/jdex/10-19 Life Admin/11 Me, Myself, & I/11.11 Me.md new file mode 100644 index 0000000..e69de29 diff --git a/tests/ID_DIFFERENT_FROM_JDEX/jdex/10-19 Life Admin/11 Me, Myself, & I/11.12 Myself.md b/tests/ID_DIFFERENT_FROM_JDEX/jdex/10-19 Life Admin/11 Me, Myself, & I/11.12 Myself.md new file mode 100644 index 0000000..e69de29 diff --git a/tests/ID_DIFFERENT_FROM_JDEX/jdlint.toml b/tests/ID_DIFFERENT_FROM_JDEX/jdlint.toml new file mode 100644 index 0000000..e7918df --- /dev/null +++ b/tests/ID_DIFFERENT_FROM_JDEX/jdlint.toml @@ -0,0 +1,59 @@ +[linter] +json_output = true +ignore = [".placeholder"] + +[[system.roots]] +name = "Files" +path = "files" + +[system.jdex] +path = "jdex" + +## ########################################################### +# Partially Nested +## ########################################################### +[[system.jdex.children]] +name = "JDex Area Folder" +format = "/#A/0-/=A/9 /*Area/" + +[[system.jdex.children.children]] +name = "JDex Category Folder" +format = "/=A//#C/ /*Category/" + +[[system.jdex.children.children.notes]] +name = "JDex Area Note" +format = "/=A/0.00 /*Name/.md" +ids = ["/=A/0-/=A/9", "/=A/0.00"] + +[[system.jdex.children.children.notes]] +name = "JDex Category Note" +format = "/=A//=C/.00 /*Name/.md" +ids = ["/=A//=C/", "/=A//=C/.00"] + +[[system.jdex.children.children.notes]] +name = "JDex Id Note" +format = "/=A//=C/./##ID/ /*Name/.md" +ids = ["/=A//=C/./=ID/"] + +## ########################################################### +# Standard +## ########################################################### +[[system.children]] +name = "Area" +format = "/#A/0-/=A/9 /*Area/" +jdex_note = "/=A/0.00 /=Area/.md" +id = "/=A/0-/=A/9" + +[[system.children.children]] +name = "Category" +format = "/=A//#C/ /*Category/" +jdex_note = "/=A//=C/.00 /=Category/.md" +id = "/=A//=C/" + +[[system.children.children.children]] +name = "ID" +format = "/=A//=C/./##ID/ /*IDName/" +jdex_note = "/=A//=C/./=ID/ /=IDName/.md" +id = "/=A//=C/./=ID/" +can_be_file = true +allow_arbitrary_contents = true diff --git a/tests/ID_DIFFERENT_FROM_JDEX/result.json b/tests/ID_DIFFERENT_FROM_JDEX/result.json new file mode 100644 index 0000000..a18df65 --- /dev/null +++ b/tests/ID_DIFFERENT_FROM_JDEX/result.json @@ -0,0 +1,16 @@ +{ + "errors": { + "Files": [ + { + "type": "ID_DIFFERENT_FROM_JDEX", + "id": "11.12", + "file": "files/10-19 Life Admin/11 Me, Myself, & I/11.12 Myslef", + "expected_jdex_note": "11.12 Myslef.md", + "known_jdex_notes": [ + "jdex/10-19 Life Admin/11 Me, Myself, & I/11.12 Myself.md" + ] + } + ] + }, + "jdex_errors": [] +} \ No newline at end of file diff --git a/tests/ID_NOT_IN_JDEX/files/10-19 Life Admin/11 Me, Myself, & I/11.11 Me/Content b/tests/ID_NOT_IN_JDEX/files/10-19 Life Admin/11 Me, Myself, & I/11.11 Me/Content new file mode 100644 index 0000000..e69de29 diff --git a/tests/ID_NOT_IN_JDEX/files/10-19 Life Admin/11 Me, Myself, & I/11.12 Myself/Content b/tests/ID_NOT_IN_JDEX/files/10-19 Life Admin/11 Me, Myself, & I/11.12 Myself/Content new file mode 100644 index 0000000..e69de29 diff --git a/tests/ID_NOT_IN_JDEX/files/10-19 Life Admin/11 Me, Myself, & I/11.13 I/Content b/tests/ID_NOT_IN_JDEX/files/10-19 Life Admin/11 Me, Myself, & I/11.13 I/Content new file mode 100644 index 0000000..e69de29 diff --git a/tests/ID_NOT_IN_JDEX/jdex/10-19 Life Admin/10 Management of area 10-19/10.00 Life Admin.md b/tests/ID_NOT_IN_JDEX/jdex/10-19 Life Admin/10 Management of area 10-19/10.00 Life Admin.md new file mode 100644 index 0000000..e69de29 diff --git a/tests/ID_NOT_IN_JDEX/jdex/10-19 Life Admin/11 Me, Myself, & I/11.00 Me, Myself, & I.md b/tests/ID_NOT_IN_JDEX/jdex/10-19 Life Admin/11 Me, Myself, & I/11.00 Me, Myself, & I.md new file mode 100644 index 0000000..e69de29 diff --git a/tests/ID_NOT_IN_JDEX/jdex/10-19 Life Admin/11 Me, Myself, & I/11.11 Me.md b/tests/ID_NOT_IN_JDEX/jdex/10-19 Life Admin/11 Me, Myself, & I/11.11 Me.md new file mode 100644 index 0000000..e69de29 diff --git a/tests/ID_NOT_IN_JDEX/jdex/10-19 Life Admin/11 Me, Myself, & I/11.12 Myself.md b/tests/ID_NOT_IN_JDEX/jdex/10-19 Life Admin/11 Me, Myself, & I/11.12 Myself.md new file mode 100644 index 0000000..e69de29 diff --git a/tests/ID_NOT_IN_JDEX/jdlint.toml b/tests/ID_NOT_IN_JDEX/jdlint.toml new file mode 100644 index 0000000..e7918df --- /dev/null +++ b/tests/ID_NOT_IN_JDEX/jdlint.toml @@ -0,0 +1,59 @@ +[linter] +json_output = true +ignore = [".placeholder"] + +[[system.roots]] +name = "Files" +path = "files" + +[system.jdex] +path = "jdex" + +## ########################################################### +# Partially Nested +## ########################################################### +[[system.jdex.children]] +name = "JDex Area Folder" +format = "/#A/0-/=A/9 /*Area/" + +[[system.jdex.children.children]] +name = "JDex Category Folder" +format = "/=A//#C/ /*Category/" + +[[system.jdex.children.children.notes]] +name = "JDex Area Note" +format = "/=A/0.00 /*Name/.md" +ids = ["/=A/0-/=A/9", "/=A/0.00"] + +[[system.jdex.children.children.notes]] +name = "JDex Category Note" +format = "/=A//=C/.00 /*Name/.md" +ids = ["/=A//=C/", "/=A//=C/.00"] + +[[system.jdex.children.children.notes]] +name = "JDex Id Note" +format = "/=A//=C/./##ID/ /*Name/.md" +ids = ["/=A//=C/./=ID/"] + +## ########################################################### +# Standard +## ########################################################### +[[system.children]] +name = "Area" +format = "/#A/0-/=A/9 /*Area/" +jdex_note = "/=A/0.00 /=Area/.md" +id = "/=A/0-/=A/9" + +[[system.children.children]] +name = "Category" +format = "/=A//#C/ /*Category/" +jdex_note = "/=A//=C/.00 /=Category/.md" +id = "/=A//=C/" + +[[system.children.children.children]] +name = "ID" +format = "/=A//=C/./##ID/ /*IDName/" +jdex_note = "/=A//=C/./=ID/ /=IDName/.md" +id = "/=A//=C/./=ID/" +can_be_file = true +allow_arbitrary_contents = true diff --git a/tests/ID_NOT_IN_JDEX/result.json b/tests/ID_NOT_IN_JDEX/result.json new file mode 100644 index 0000000..af5d922 --- /dev/null +++ b/tests/ID_NOT_IN_JDEX/result.json @@ -0,0 +1,12 @@ +{ + "errors": { + "Files": [ + { + "type": "ID_NOT_IN_JDEX", + "id": "11.13", + "file": "files/10-19 Life Admin/11 Me, Myself, & I/11.13 I" + } + ] + }, + "jdex_errors": [] +} \ No newline at end of file diff --git a/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/1A/1.md b/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/files/1/1A/1.md new file mode 100644 index 0000000..e69de29 diff --git a/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml b/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml index f7970f6..349c081 100644 --- a/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml +++ b/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml @@ -20,16 +20,21 @@ allow_arbitrary_contents = true [[system.jdex.children.children.notes]] name = "C1" format = "X.md" -id = "/=A/./=Name/" +ids = ["/=A/./=Name/"] [[system.jdex.children.children]] name = "B2" format = "/=A//*Name/" +[[system.jdex.children.children.notes]] +name = "C3" +format = "/=A/.md" +ids = ["/=A/"] + [[system.jdex.children.children.notes]] name = "C2" format = "/=A//=Name/.md" -id = "/=A/./=Name/" +ids = ["/=A/./=Name/"] [[system.children]] name = "Area" diff --git a/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/result.json b/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/result.json index 52777c2..55e6539 100644 --- a/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/result.json +++ b/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/result.json @@ -45,6 +45,14 @@ "type": "JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED", "file": "files/1/1A/File", "possible_formats": [ + { + "name": [ + "A", + "B2", + "C3" + ], + "format": "/=A/.md" + }, { "name": [ "A", @@ -59,6 +67,14 @@ "type": "JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED", "file": "files/1/1A/Folder", "possible_formats": [ + { + "name": [ + "A", + "B2", + "C3" + ], + "format": "/=A/.md" + }, { "name": [ "A", diff --git a/tests/JDEX_DUPLICATE_ID/files/1/1.md b/tests/JDEX_DUPLICATE_ID/files/1/1.md new file mode 100644 index 0000000..e69de29 diff --git a/tests/JDEX_DUPLICATE_ID/jdlint.toml b/tests/JDEX_DUPLICATE_ID/jdlint.toml index 332d633..fee11b6 100644 --- a/tests/JDEX_DUPLICATE_ID/jdlint.toml +++ b/tests/JDEX_DUPLICATE_ID/jdlint.toml @@ -12,10 +12,15 @@ path = "files" name = "A" format = "/#A/" +[[system.jdex.children.notes]] +name = "C" +format = "/=A/.md" +ids = ["/=A/"] + [[system.jdex.children.notes]] name = "B" format = "/=A//#ID//*Name/.md" -id = "/=A/./=ID/" +ids = ["/=A/./=ID/"] [[system.children]] name = "Area" diff --git a/tests/JDEX_EMPTY_FOLDER/files/1/1B/X.md b/tests/JDEX_EMPTY_FOLDER/files/1/1B/X.md new file mode 100644 index 0000000..e69de29 diff --git a/tests/JDEX_EMPTY_FOLDER/jdlint.toml b/tests/JDEX_EMPTY_FOLDER/jdlint.toml index 403c771..db4bc87 100644 --- a/tests/JDEX_EMPTY_FOLDER/jdlint.toml +++ b/tests/JDEX_EMPTY_FOLDER/jdlint.toml @@ -21,11 +21,11 @@ allow_arbitrary_contents = true [[system.jdex.children.children.notes]] name = "C1" format = "X.md" -id = "/=A/./=Name/" +ids = ["/=A/"] [[system.children]] name = "Area" format = "/#A/" -jdex_note = "/=A/.md" +jdex_note = "X.md" id = "/=A/" allow_arbitrary_contents = true diff --git a/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/files/1/1A/1.md b/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/files/1/1A/1.md new file mode 100644 index 0000000..e69de29 diff --git a/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/jdlint.toml b/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/jdlint.toml index b7f4085..a518154 100644 --- a/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/jdlint.toml +++ b/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/jdlint.toml @@ -16,10 +16,15 @@ format = "/#A/" name = "B" format = "/=A//*Name/" +[[system.jdex.children.children.notes]] +name = "C" +format = "/=A/.md" +ids = ["/=A/"] + [[system.jdex.children.children.notes]] name = "C" format = "/=A//=Name/.md" -id = "/=A/./=Name/" +ids = ["/=A/./=Name/"] [[system.children]] name = "Area" diff --git a/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/files/1/1A/1.md b/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/files/1/1A/1.md new file mode 100644 index 0000000..e69de29 diff --git a/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/jdlint.toml b/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/jdlint.toml index 1641385..210bff3 100644 --- a/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/jdlint.toml +++ b/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/jdlint.toml @@ -16,10 +16,15 @@ format = "/#A/" name = "B" format = "/=A//*Name/" +[[system.jdex.children.children.notes]] +name = "JDex Note" +format = "/=A/.md" +ids = ["/=A/"] + [[system.jdex.children.children.notes]] name = "JDex Note" format = "/=A//=Name/.md" -id = "/=A/./=Name/" +ids = ["/=A/./=Name/"] [[system.children]] name = "Area" From 407ea7fca56c6fd6fb7305ca5140ee2807457fc2 Mon Sep 17 00:00:00 2001 From: SiriusStarr <2049163+SiriusStarr@users.noreply.github.com> Date: Sat, 18 Jul 2026 14:36:52 -0700 Subject: [PATCH 10/23] Support can_be_file --- jdlint.py | 19 ++++++++++--------- tests/can_be_file/files/1/1A/1A | 0 tests/can_be_file/jdlint.toml | 22 ++++++++++++++++++++++ tests/can_be_file/result.json | 4 ++++ 4 files changed, 36 insertions(+), 9 deletions(-) create mode 100644 tests/can_be_file/files/1/1A/1A create mode 100644 tests/can_be_file/jdlint.toml create mode 100644 tests/can_be_file/result.json diff --git a/jdlint.py b/jdlint.py index 1f75f74..4246f09 100755 --- a/jdlint.py +++ b/jdlint.py @@ -1425,16 +1425,17 @@ def _process_system_level_and_children( if match: # Is a valid child folder if x.is_file(): - # This is an error - accumulated_errors.append( - IssueFileWhereFolderExpected( - PurePath(x), - ContentPattern( - child.format.name, - child.format.raw_format, + if not child.can_be_file: + # This is an error + accumulated_errors.append( + IssueFileWhereFolderExpected( + PurePath(x), + ContentPattern( + child.format.name, + child.format.raw_format, + ), ), - ), - ) + ) break # Walk child diff --git a/tests/can_be_file/files/1/1A/1A b/tests/can_be_file/files/1/1A/1A new file mode 100644 index 0000000..e69de29 diff --git a/tests/can_be_file/jdlint.toml b/tests/can_be_file/jdlint.toml new file mode 100644 index 0000000..e6efbe3 --- /dev/null +++ b/tests/can_be_file/jdlint.toml @@ -0,0 +1,22 @@ +[linter] +json_output = true + +[[system.roots]] +name = "JDex" +path = "files" + +[[system.children]] +name = "A" +format = "/#A/" +id = "/=A/" + +[[system.children.children]] +name = "B" +format = "/=A//*Name/" +id = "/=A/./=Name/" + +[[system.children.children.children]] +name = "C" +format = "/=A//=Name/" +id = "/=A/./=Name/" +can_be_file = true diff --git a/tests/can_be_file/result.json b/tests/can_be_file/result.json new file mode 100644 index 0000000..da0ee54 --- /dev/null +++ b/tests/can_be_file/result.json @@ -0,0 +1,4 @@ +{ + "errors": {}, + "jdex_errors": [] +} \ No newline at end of file From bafc1f6f68553de127303504c76e1b6aa5d061e2 Mon Sep 17 00:00:00 2001 From: SiriusStarr <2049163+SiriusStarr@users.noreply.github.com> Date: Sat, 18 Jul 2026 15:01:00 -0700 Subject: [PATCH 11/23] Improve errors for folders that must be empty --- jdlint.py | 243 ++++-------------- .../11 Me, Myself, & I/11.01 Inbox/Content | 0 .../11.10 \342\226\240 Selves/Content" | 0 .../11 Me, Myself, & I/11.11 Me/Content | 0 .../11 Me, Myself, & I/11.12 Myslef/Content | 0 tests/FOLDER_SHOULD_BE_EMPTY/jdlint.toml | 42 +++ tests/FOLDER_SHOULD_BE_EMPTY/result.json | 21 ++ 7 files changed, 106 insertions(+), 200 deletions(-) create mode 100644 tests/FOLDER_SHOULD_BE_EMPTY/files/10-19 Life Admin/11 Me, Myself, & I/11.01 Inbox/Content create mode 100644 "tests/FOLDER_SHOULD_BE_EMPTY/files/10-19 Life Admin/11 Me, Myself, & I/11.10 \342\226\240 Selves/Content" create mode 100644 tests/FOLDER_SHOULD_BE_EMPTY/files/10-19 Life Admin/11 Me, Myself, & I/11.11 Me/Content create mode 100644 tests/FOLDER_SHOULD_BE_EMPTY/files/10-19 Life Admin/11 Me, Myself, & I/11.12 Myslef/Content create mode 100644 tests/FOLDER_SHOULD_BE_EMPTY/jdlint.toml create mode 100644 tests/FOLDER_SHOULD_BE_EMPTY/result.json diff --git a/jdlint.py b/jdlint.py index 4246f09..0e9517d 100755 --- a/jdlint.py +++ b/jdlint.py @@ -728,6 +728,25 @@ def explain(self) -> _Explanation: ) +@dataclass(frozen=True) +class IssueFolderShouldBeEmpty(Issue): + """Content was found in a folder that should be empty.""" + + children: tuple[PurePath, ...] + type: Literal["FOLDER_SHOULD_BE_EMPTY"] = "FOLDER_SHOULD_BE_EMPTY" + + def display(self) -> str: + """Display this particular instance of an error.""" + return f"{self.file!s} (has {len(self.children)} children)" + + def explain(self) -> _Explanation: + """Explain what this error is.""" + return _Explanation( + explanation="Files or folders were found in a folder that should be empty.", + fix="Either the folder in question should have allow_arbitrary_content set to true, or you should remove the content.", + ) + + @dataclass(frozen=True) class IssueDuplicateID(Issue): """An ID that has been used multiple times.""" @@ -788,195 +807,6 @@ def explain(self) -> _Explanation: ) -@dataclass(frozen=True) -class AreaDifferentFromJDex: - """An area with a differently-named JDex entry.""" - - area: str - jdex_name: str - type: Literal["AREA_DIFFERENT_FROM_JDEX"] = "AREA_DIFFERENT_FROM_JDEX" - - def display(self, files: list[File]) -> str: - """Display this particular instance of an error.""" - return f"{_print_nest(files[0])} [JDex name: {self.jdex_name}]" - - def explain(self) -> _Explanation: - """Explain what this error is.""" - return _Explanation( - explanation="An area was found, the name of which is different from its corresponding JDex entry.", - fix="Update the one that is incorrect.", - ) - - -@dataclass(frozen=True) -class AreaNotInJDex: - """An area without a corresponding JDex entry.""" - - area: str - type: Literal["AREA_NOT_IN_JDEX"] = "AREA_NOT_IN_JDEX" - - def display(self, files: list[File]) -> str: - """Display this particular instance of an error.""" - return f"{_print_nest(files[0])} [area: {_print_area(self.area)}]" - - def explain(self) -> _Explanation: - """Explain what this error is.""" - return _Explanation( - explanation="An area was found in your files that is missing from your JDex.", - fix="Go add a corresponding entry to your JDex, or delete this if it's unused.", - ) - - -@dataclass(frozen=True) -class CategoryDifferentFromJDex: - """A category with a differently-named JDex entry.""" - - category: str - jdex_name: str - type: Literal["CATEGORY_DIFFERENT_FROM_JDEX"] = "CATEGORY_DIFFERENT_FROM_JDEX" - - def display(self, files: list[File]) -> str: - """Display this particular instance of an error.""" - return f"{_print_nest(files[0])} [JDex name: {self.jdex_name}]" - - def explain(self) -> _Explanation: - """Explain what this error is.""" - return _Explanation( - explanation="A category was found, the name of which is different from its corresponding JDex entry.", - fix="Update the one that is incorrect.", - ) - - -@dataclass(frozen=True) -class CategoryInWrongArea: - """A category that, by its number, has been put in the wrong area.""" - - category_area: str - file_area: str - type: Literal["CATEGORY_IN_WRONG_AREA"] = "CATEGORY_IN_WRONG_AREA" - - def display(self, files: list[File]) -> str: - """Given the file's name, print the error message for it.""" - return f"{_print_nest(files[0])} [in {_print_area(self.file_area)} but should be in {_print_area(self.category_area)}]" - - def explain(self) -> _Explanation: - """Explain what this error is.""" - return _Explanation( - explanation="Some categories are in the wrong area.", - fix="Move them into the correct area folder.", - ) - - -@dataclass(frozen=True) -class CategoryNotInJDex: - """An category without a corresponding JDex entry.""" - - category: str - type: Literal["CATEGORY_NOT_IN_JDEX"] = "CATEGORY_NOT_IN_JDEX" - - def display(self, files: list[File]) -> str: - """Display this particular instance of an error.""" - return f"{_print_nest(files[0])} [category: {self.category}]" - - def explain(self) -> _Explanation: - """Explain what this error is.""" - return _Explanation( - explanation="A category was found in the files that is missing from the JDex.", - fix="Go add a corresponding entry to your JDex.", - ) - - -@dataclass(frozen=True) -class FileOutsideId: - """A file was encountered not in a terminal ID folder.""" - - type: Literal["FILE_OUTSIDE_ID"] = "FILE_OUTSIDE_ID" - - def display(self, files: list[File]) -> str: - """Display this particular instance of an error.""" - return _print_nest(files[0]) - - def explain(self) -> _Explanation: - """Explain what this error is.""" - return _Explanation( - explanation="Files were found outside of IDs.", - fix="Files should only be kept in IDs and not higher in the hierarchy.", - ) - - -@dataclass(frozen=True) -class InvalidAreaName: - """A folder at the area level that doesn't match the normal format.""" - - type: Literal["INVALID_AREA_NAME"] = "INVALID_AREA_NAME" - - def display(self, files: list[File]) -> str: - """Display this particular instance of an error.""" - return _print_nest(files[0]) - - def explain(self) -> _Explanation: - """Explain what this error is.""" - return _Explanation( - explanation="Some areas have invalid names.", - fix='Valid area names look like "10-19 Life Admin", so edit the names to match that format.', - ) - - -@dataclass(frozen=True) -class InvalidCategoryName: - """A folder at the category level that doesn't match the normal format.""" - - type: Literal["INVALID_CATEGORY_NAME"] = "INVALID_CATEGORY_NAME" - - def display(self, files: list[File]) -> str: - """Display this particular instance of an error.""" - return _print_nest(files[0]) - - def explain(self) -> _Explanation: - """Explain what this error is.""" - return _Explanation( - explanation="Some categories have invalid names.", - fix='Valid category names look like "11 Me, Myself, & I", so edit the names to match that format.', - ) - - -@dataclass(frozen=True) -class InvalidIDName: - """A folder at the ID level that doesn't match the normal format.""" - - type: Literal["INVALID_ID_NAME"] = "INVALID_ID_NAME" - - def display(self, files: list[File]) -> str: - """Display this particular instance of an error.""" - return _print_nest(files[0]) - - def explain(self) -> _Explanation: - """Explain what this error is.""" - return _Explanation( - explanation="Some IDs have invalid names.", - fix='Valid ID names look like "11.11 A Cool Project", so edit the names to match that format.', - ) - - -@dataclass(frozen=True) -class NonemptyInbox: - """An inbox (AC.01) that contains items.""" - - num_items: int - type: Literal["NONEMPTY_INBOX"] = "NONEMPTY_INBOX" - - def display(self, files: list[File]) -> str: - """Display this particular instance of an error.""" - return f"{_print_nest(files[0])} [{self.num_items} items]" - - def explain(self) -> _Explanation: - """Explain what this error is.""" - return _Explanation( - explanation="Files were found in an inbox.", - fix="Go sort them into the appropriate IDs.", - ) - - @dataclass(frozen=True) class File: """A file or folder that has been detected by jdlint.""" @@ -1415,6 +1245,8 @@ def _process_system_level_and_children( accumulated_structure = {} has_content = False + children_that_should_not_be = [] + with os.scandir(path) as contents: for x in contents: if _entry_is_ignored(ignored, [], x): @@ -1473,18 +1305,29 @@ def _process_system_level_and_children( else: # If we got here, it matched no known child/note if not getattr(tier, "allow_arbitrary_contents", False): - # This is an error - accumulated_errors.append( - IssueArbitraryContentWhereNotAllowed( - PurePath(x), - tuple( - ContentPattern(c.format.name, c.format.raw_format) - for c in tier.children + # If the tier has no children specified, it should be empty + if not tier.children: + children_that_should_not_be.append(PurePath(x)) + else: + accumulated_errors.append( + IssueArbitraryContentWhereNotAllowed( + PurePath(x), + tuple( + ContentPattern(c.format.name, c.format.raw_format) + for c in tier.children + ), ), - ), - ) - if not has_content: - # We have a fully empty folder; it shouldn't exist if it's doing nothing. + ) + if children_that_should_not_be: + accumulated_errors.append( + IssueFolderShouldBeEmpty( + PurePath(path), tuple(children_that_should_not_be) + ), + ) + if not has_content and ( + tier.children or getattr(tier, "allow_arbitrary_contents", False) + ): + # We have a fully empty folder; it shouldn't exist if it's doing nothing (unless it should be empty). accumulated_errors.append(IssueEmptyFolder(PurePath(path))) return (accumulated_structure, accumulated_errors) diff --git a/tests/FOLDER_SHOULD_BE_EMPTY/files/10-19 Life Admin/11 Me, Myself, & I/11.01 Inbox/Content b/tests/FOLDER_SHOULD_BE_EMPTY/files/10-19 Life Admin/11 Me, Myself, & I/11.01 Inbox/Content new file mode 100644 index 0000000..e69de29 diff --git "a/tests/FOLDER_SHOULD_BE_EMPTY/files/10-19 Life Admin/11 Me, Myself, & I/11.10 \342\226\240 Selves/Content" "b/tests/FOLDER_SHOULD_BE_EMPTY/files/10-19 Life Admin/11 Me, Myself, & I/11.10 \342\226\240 Selves/Content" new file mode 100644 index 0000000..e69de29 diff --git a/tests/FOLDER_SHOULD_BE_EMPTY/files/10-19 Life Admin/11 Me, Myself, & I/11.11 Me/Content b/tests/FOLDER_SHOULD_BE_EMPTY/files/10-19 Life Admin/11 Me, Myself, & I/11.11 Me/Content new file mode 100644 index 0000000..e69de29 diff --git a/tests/FOLDER_SHOULD_BE_EMPTY/files/10-19 Life Admin/11 Me, Myself, & I/11.12 Myslef/Content b/tests/FOLDER_SHOULD_BE_EMPTY/files/10-19 Life Admin/11 Me, Myself, & I/11.12 Myslef/Content new file mode 100644 index 0000000..e69de29 diff --git a/tests/FOLDER_SHOULD_BE_EMPTY/jdlint.toml b/tests/FOLDER_SHOULD_BE_EMPTY/jdlint.toml new file mode 100644 index 0000000..37181a3 --- /dev/null +++ b/tests/FOLDER_SHOULD_BE_EMPTY/jdlint.toml @@ -0,0 +1,42 @@ +[linter] +json_output = true +ignore = [".placeholder"] + +[[system.roots]] +name = "Files" +path = "files" + +## ########################################################### +# Standard +## ########################################################### +[[system.children]] +name = "Area" +format = "/#A/0-/=A/9 /*Area/" +jdex_note = "/=A/0.00 /=Area/.md" +id = "/=A/0-/=A/9" + +[[system.children.children]] +name = "Category" +format = "/=A//#C/ /*Category/" +jdex_note = "/=A//=C/.00 /=Category/.md" +id = "/=A//=C/" + +[[system.children.children.children]] +name = "Header" +format = "/=A//=C/./#I/0 ■ /*Header/" +jdex_note = "/=A//=C/./=I/0 ■ /=Header/.md" +id = "/=A//=C/./=I/0" + +[[system.children.children.children]] +name = "Inbox" +format = "/=A//=C/.01 /*Inbox/" +jdex_note = "/=A//=C/.01 /=Inbox/.md" +id = "/=A//=C/.01" + +[[system.children.children.children]] +name = "ID" +format = "/=A//=C/./##ID/ /*IDName/" +jdex_note = "/=A//=C/./=ID/ /=IDName/.md" +id = "/=A//=C/./=ID/" +can_be_file = true +allow_arbitrary_contents = true diff --git a/tests/FOLDER_SHOULD_BE_EMPTY/result.json b/tests/FOLDER_SHOULD_BE_EMPTY/result.json new file mode 100644 index 0000000..0229d72 --- /dev/null +++ b/tests/FOLDER_SHOULD_BE_EMPTY/result.json @@ -0,0 +1,21 @@ +{ + "errors": { + "Files": [ + { + "type": "FOLDER_SHOULD_BE_EMPTY", + "file": "files/10-19 Life Admin/11 Me, Myself, & I/11.01 Inbox", + "children": [ + "files/10-19 Life Admin/11 Me, Myself, & I/11.01 Inbox/Content" + ] + }, + { + "type": "FOLDER_SHOULD_BE_EMPTY", + "file": "files/10-19 Life Admin/11 Me, Myself, & I/11.10 ■ Selves", + "children": [ + "files/10-19 Life Admin/11 Me, Myself, & I/11.10 ■ Selves/Content" + ] + } + ] + }, + "jdex_errors": [] +} \ No newline at end of file From b78e83f083f4880bbc1fca1cf6cbc4e041ea1430 Mon Sep 17 00:00:00 2001 From: SiriusStarr <2049163+SiriusStarr@users.noreply.github.com> Date: Sat, 18 Jul 2026 15:02:37 -0700 Subject: [PATCH 12/23] Add missing issue types --- jdlint.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/jdlint.py b/jdlint.py index 0e9517d..cccaf4e 100755 --- a/jdlint.py +++ b/jdlint.py @@ -932,6 +932,9 @@ def explain(self) -> _Explanation: | IssueDuplicateID | IssueEmptyFolder | IssueFileWhereFolderExpected + | IssueFolderShouldBeEmpty + | IssueIDDifferentFromJDex + | IssueIDNotInJDex ) AnyIssueType = JDexIssueType | IssueType From 3e76771ec29cdd9e9a63a312de4fe272d36c44f3 Mon Sep 17 00:00:00 2001 From: SiriusStarr <2049163+SiriusStarr@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:02:00 -0700 Subject: [PATCH 13/23] Add forbidden patterns --- jdlint.py | 130 +++++++++++++++++- .../11.01 Inbxo/.placeholder | 0 .../11.10 \342\226\240Selves.md" | 0 .../11 Me, Myself, & I/11.11 Me.md | 0 .../11 Me, Myself, & I/11.12 Myself.md | 0 .../ENCOUNTERED_FORBIDDEN_FOLDER/jdlint.toml | 49 +++++++ .../ENCOUNTERED_FORBIDDEN_FOLDER/result.json | 31 +++++ .../11 Me, Myself, & I/11.11 Me/Content | 0 .../11 Me, Myself, & I/11.12 Myself/Content | 0 .../10.00 Life Admin.md | 0 .../10.00 Life Admin.md | 0 .../11.00 Me, Myself, & I.md | 0 .../11 Me, Myself, & I/11.11 Me.md | 0 .../11 Me, Myself, & I/11.12 Myself.md | 0 .../jdlint.toml | 73 ++++++++++ .../result.json | 16 +++ .../11 Me, Myself, & I/11.11 Me/Content | 0 .../11 Me, Myself, & I/11.12 Myself/Content | 0 .../10.00 Life Admin.md | 0 .../11.00 Me, Myself, & I.md | 0 .../11 Me, Myself, & I/11.01 Inbxo.md | 0 .../11 Me, Myself, & I/11.11 Me.md | 0 .../11 Me, Myself, & I/11.12 Myself.md | 0 .../jdlint.toml | 70 ++++++++++ .../result.json | 17 +++ 25 files changed, 383 insertions(+), 3 deletions(-) create mode 100644 tests/ENCOUNTERED_FORBIDDEN_FOLDER/files/10-19 Life Admin/11 Me, Myself, & I/11.01 Inbxo/.placeholder create mode 100644 "tests/ENCOUNTERED_FORBIDDEN_FOLDER/files/10-19 Life Admin/11 Me, Myself, & I/11.10 \342\226\240Selves.md" create mode 100644 tests/ENCOUNTERED_FORBIDDEN_FOLDER/files/10-19 Life Admin/11 Me, Myself, & I/11.11 Me.md create mode 100644 tests/ENCOUNTERED_FORBIDDEN_FOLDER/files/10-19 Life Admin/11 Me, Myself, & I/11.12 Myself.md create mode 100644 tests/ENCOUNTERED_FORBIDDEN_FOLDER/jdlint.toml create mode 100644 tests/ENCOUNTERED_FORBIDDEN_FOLDER/result.json create mode 100644 tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/files/10-19 Life Admin/11 Me, Myself, & I/11.11 Me/Content create mode 100644 tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/files/10-19 Life Admin/11 Me, Myself, & I/11.12 Myself/Content create mode 100644 tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/jdex/10-19 Life Admin/10 Management of area 10-19/10.00 Life Admin.md create mode 100644 tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/jdex/10-19 Life Admin/10 Managemnt of area 10-19/10.00 Life Admin.md create mode 100644 tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/jdex/10-19 Life Admin/11 Me, Myself, & I/11.00 Me, Myself, & I.md create mode 100644 tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/jdex/10-19 Life Admin/11 Me, Myself, & I/11.11 Me.md create mode 100644 tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/jdex/10-19 Life Admin/11 Me, Myself, & I/11.12 Myself.md create mode 100644 tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/jdlint.toml create mode 100644 tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/result.json create mode 100644 tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/files/10-19 Life Admin/11 Me, Myself, & I/11.11 Me/Content create mode 100644 tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/files/10-19 Life Admin/11 Me, Myself, & I/11.12 Myself/Content create mode 100644 tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/jdex/10-19 Life Admin/10 Management of area 10-19/10.00 Life Admin.md create mode 100644 tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/jdex/10-19 Life Admin/11 Me, Myself, & I/11.00 Me, Myself, & I.md create mode 100644 tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/jdex/10-19 Life Admin/11 Me, Myself, & I/11.01 Inbxo.md create mode 100644 tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/jdex/10-19 Life Admin/11 Me, Myself, & I/11.11 Me.md create mode 100644 tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/jdex/10-19 Life Admin/11 Me, Myself, & I/11.12 Myself.md create mode 100644 tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/jdlint.toml create mode 100644 tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/result.json diff --git a/jdlint.py b/jdlint.py index cccaf4e..4c3be4d 100755 --- a/jdlint.py +++ b/jdlint.py @@ -408,6 +408,16 @@ def __init__( at, "If children are specified, allow_arbitrary_contents must be false.", ) + if self.format.forbidden and self.children: + raise ConfigConflictError( + at, + "If forbidden, children must not be specified.", + ) + if self.format.forbidden and self.allow_arbitrary_contents: + raise ConfigConflictError( + at, + "If forbidden, allow_arbitrary_contents must be false.", + ) class ConfigSystemTier(ConfigFolderTier): @@ -451,11 +461,18 @@ def __init__( type(self.can_be_file).__name__, ) - if self.children and (self.can_be_file): + if self.children and self.can_be_file: raise ConfigConflictError( at, "If children are specified, can_be_file must be false.", ) + + if self.format.forbidden and self.can_be_file: + raise ConfigConflictError( + at, + "If forbidden, can_be_file must be false.", + ) + # Ensure no extra fields for key in from_file: raise ConfigExtraKeyError(f"{at}.{key}", tuple(self.__dict__.keys())) @@ -483,10 +500,10 @@ def __init__( for i, v in enumerate(from_file.pop("notes", [])) ] - if not self.notes and not self.children: + if not self.notes and not self.children and not self.format.forbidden: raise ConfigConflictError( at, - "A JDex tier must specify either notes or children.", + "A JDex tier must specify either notes or children, or be forbidden.", ) if self.notes and self.children: @@ -494,6 +511,11 @@ def __init__( at, "Only one of notes and children may be specified.", ) + if self.notes and self.format.forbidden: + raise ConfigConflictError( + at, + "If forbidden, notes cannot be speciefed.", + ) # Ensure no extra fields for key in from_file: @@ -523,6 +545,7 @@ def __init__( raise (ConfigMissingKeyError(f"{at}.format")) self.raw_format = from_file.pop("format") + self.forbidden = from_file.pop("forbidden", False) if "name" not in from_file: raise (ConfigMissingKeyError(f"{at}.name")) @@ -540,6 +563,12 @@ def __init__( "str", type(self.raw_format).__name__, ) + if not isinstance(self.forbidden, bool): + raise ConfigTypeError( + f"{at}.forbidden", + "bool", + type(self.forbidden).__name__, + ) if from_file["name"] == "": raise ConfigValueError( @@ -807,6 +836,25 @@ def explain(self) -> _Explanation: ) +@dataclass(frozen=True) +class IssueEncounteredForbiddenFolder(Issue): + """A file that matched a forbidden format was found.""" + + matched_pattern: ContentPattern + type: Literal["ENCOUNTERED_FORBIDDEN_FOLDER"] = "ENCOUNTERED_FORBIDDEN_FOLDER" + + def display(self) -> str: + """Display this particular instance of an error.""" + return f'{self.file!s} (matched "{self.matched_pattern}")' + + def explain(self) -> _Explanation: + """Explain what this error is.""" + return _Explanation( + explanation="A file was found that matched the format of a forbidden folder.", + fix="You should remove/rename the file in question.", + ) + + @dataclass(frozen=True) class File: """A file or folder that has been detected by jdlint.""" @@ -920,12 +968,54 @@ def explain(self) -> _Explanation: ) +@dataclass(frozen=True) +class JDexIssueEncounteredForbiddenFolder(JDexIssue): + """A JDex file that matched a forbidden format was found.""" + + matched_pattern: ContentPattern + type: Literal["JDEX_ENCOUNTERED_FORBIDDEN_FOLDER"] = ( + "JDEX_ENCOUNTERED_FORBIDDEN_FOLDER" + ) + + def display(self) -> str: + """Display this particular instance of an error.""" + return f'{self.file!s} (matched "{self.matched_pattern}")' + + def explain(self) -> _Explanation: + """Explain what this error is.""" + return _Explanation( + explanation="A JDex file was found that matched the format of a forbidden folder.", + fix="You should remove/rename the file in question.", + ) + + +@dataclass(frozen=True) +class JDexIssueEncounteredForbiddenNote(JDexIssue): + """A JDex file that matched a forbidden format was found.""" + + matched_pattern: ContentPattern + type: Literal["JDEX_ENCOUNTERED_FORBIDDEN_NOTE"] = "JDEX_ENCOUNTERED_FORBIDDEN_NOTE" + + def display(self) -> str: + """Display this particular instance of an error.""" + return f'{self.file!s} (matched "{self.matched_pattern}")' + + def explain(self) -> _Explanation: + """Explain what this error is.""" + return _Explanation( + explanation="A JDex file was found that matched the format of a forbidden note.", + fix="You should remove/rename the file in question.", + ) + + JDexIssueType = ( JDexIssueArbitraryContentWhereNotAllowed | JDexIssueDuplicateID | JDexIssueEmptyFolder | JDexIssueFileWhereFolderExpected | JDexIssueFolderWhereNoteExpected + | JDexIssueEncounteredForbiddenFolder + | JDexIssueEncounteredForbiddenNote ) IssueType = ( IssueArbitraryContentWhereNotAllowed @@ -935,6 +1025,7 @@ def explain(self) -> _Explanation: | IssueFolderShouldBeEmpty | IssueIDDifferentFromJDex | IssueIDNotInJDex + | IssueEncounteredForbiddenFolder ) AnyIssueType = JDexIssueType | IssueType @@ -1134,6 +1225,17 @@ def _get_jdex_notes_here_or_children( for child_format, child in valid_children: match = child_format.fullmatch(x.name) if match: + if child.format.forbidden: + accumulated_errors.append( + JDexIssueEncounteredForbiddenFolder( + PurePath(x), + ContentPattern( + child.format.name, + child.format.raw_format, + ), + ), + ) + break # Is a valid child folder if x.is_file(): # This is an error @@ -1163,6 +1265,17 @@ def _get_jdex_notes_here_or_children( for note_format, note in valid_notes: match = note_format.fullmatch(x.name) if match: + if note.format.forbidden: + accumulated_errors.append( + JDexIssueEncounteredForbiddenNote( + PurePath(x), + ContentPattern( + note.format.name, + note.format.raw_format, + ), + ), + ) + break # Is a valid JDex note if x.is_dir(): # This is an error @@ -1258,6 +1371,17 @@ def _process_system_level_and_children( for child_format, child in valid_children: match = child_format.fullmatch(x.name) if match: + if child.format.forbidden: + accumulated_errors.append( + IssueEncounteredForbiddenFolder( + PurePath(x), + ContentPattern( + child.format.name, + child.format.raw_format, + ), + ), + ) + break # Is a valid child folder if x.is_file(): if not child.can_be_file: diff --git a/tests/ENCOUNTERED_FORBIDDEN_FOLDER/files/10-19 Life Admin/11 Me, Myself, & I/11.01 Inbxo/.placeholder b/tests/ENCOUNTERED_FORBIDDEN_FOLDER/files/10-19 Life Admin/11 Me, Myself, & I/11.01 Inbxo/.placeholder new file mode 100644 index 0000000..e69de29 diff --git "a/tests/ENCOUNTERED_FORBIDDEN_FOLDER/files/10-19 Life Admin/11 Me, Myself, & I/11.10 \342\226\240Selves.md" "b/tests/ENCOUNTERED_FORBIDDEN_FOLDER/files/10-19 Life Admin/11 Me, Myself, & I/11.10 \342\226\240Selves.md" new file mode 100644 index 0000000..e69de29 diff --git a/tests/ENCOUNTERED_FORBIDDEN_FOLDER/files/10-19 Life Admin/11 Me, Myself, & I/11.11 Me.md b/tests/ENCOUNTERED_FORBIDDEN_FOLDER/files/10-19 Life Admin/11 Me, Myself, & I/11.11 Me.md new file mode 100644 index 0000000..e69de29 diff --git a/tests/ENCOUNTERED_FORBIDDEN_FOLDER/files/10-19 Life Admin/11 Me, Myself, & I/11.12 Myself.md b/tests/ENCOUNTERED_FORBIDDEN_FOLDER/files/10-19 Life Admin/11 Me, Myself, & I/11.12 Myself.md new file mode 100644 index 0000000..e69de29 diff --git a/tests/ENCOUNTERED_FORBIDDEN_FOLDER/jdlint.toml b/tests/ENCOUNTERED_FORBIDDEN_FOLDER/jdlint.toml new file mode 100644 index 0000000..0c43e6b --- /dev/null +++ b/tests/ENCOUNTERED_FORBIDDEN_FOLDER/jdlint.toml @@ -0,0 +1,49 @@ +[linter] +json_output = true +ignore = [".placeholder"] + +[[system.roots]] +name = "Files" +path = "files" + +## ########################################################### +# Standard +## ########################################################### +[[system.children]] +name = "Area" +format = "/#A/0-/=A/9 /*Area/" +id = "/=A/0-/=A/9" + +[[system.children.children]] +name = "Category" +format = "/=A//#C/ /*Category/" +id = "/=A//=C/" + +[[system.children.children.children]] +name = "Inbox" +format = "/=A//=C/.01 Inbox" +id = "/=A//=C/.01" + +[[system.children.children.children]] +name = "Bad Inbox" +format = "/=A//=C/.01 /*Inbox/" +id = "/=A//=C/.01" +forbidden = true + +[[system.children.children.children]] +name = "Header" +format = "/=A//=C/./#I/0 ■ /*Header/" +id = "/=A//=C/./=I/0" + +[[system.children.children.children]] +name = "Bad Header" +format = "/=A//=C/./#I/0 /*Header/" +id = "/=A//=C/./=I/0" +forbidden = true + +[[system.children.children.children]] +name = "ID" +format = "/=A//=C/./##ID/ /*IDName/" +id = "/=A//=C/./=ID/" +can_be_file = true +allow_arbitrary_contents = true diff --git a/tests/ENCOUNTERED_FORBIDDEN_FOLDER/result.json b/tests/ENCOUNTERED_FORBIDDEN_FOLDER/result.json new file mode 100644 index 0000000..56970ea --- /dev/null +++ b/tests/ENCOUNTERED_FORBIDDEN_FOLDER/result.json @@ -0,0 +1,31 @@ +{ + "errors": { + "Files": [ + { + "type": "ENCOUNTERED_FORBIDDEN_FOLDER", + "matched_pattern": { + "name": [ + "Area", + "Category", + "Bad Inbox" + ], + "format": "/=A//=C/.01 /*Inbox/" + }, + "file": "files/10-19 Life Admin/11 Me, Myself, & I/11.01 Inbxo" + }, + { + "type": "ENCOUNTERED_FORBIDDEN_FOLDER", + "matched_pattern": { + "name": [ + "Area", + "Category", + "Bad Header" + ], + "format": "/=A//=C/./#I/0 /*Header/" + }, + "file": "files/10-19 Life Admin/11 Me, Myself, & I/11.10 ■Selves.md" + } + ] + }, + "jdex_errors": [] +} \ No newline at end of file diff --git a/tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/files/10-19 Life Admin/11 Me, Myself, & I/11.11 Me/Content b/tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/files/10-19 Life Admin/11 Me, Myself, & I/11.11 Me/Content new file mode 100644 index 0000000..e69de29 diff --git a/tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/files/10-19 Life Admin/11 Me, Myself, & I/11.12 Myself/Content b/tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/files/10-19 Life Admin/11 Me, Myself, & I/11.12 Myself/Content new file mode 100644 index 0000000..e69de29 diff --git a/tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/jdex/10-19 Life Admin/10 Management of area 10-19/10.00 Life Admin.md b/tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/jdex/10-19 Life Admin/10 Management of area 10-19/10.00 Life Admin.md new file mode 100644 index 0000000..e69de29 diff --git a/tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/jdex/10-19 Life Admin/10 Managemnt of area 10-19/10.00 Life Admin.md b/tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/jdex/10-19 Life Admin/10 Managemnt of area 10-19/10.00 Life Admin.md new file mode 100644 index 0000000..e69de29 diff --git a/tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/jdex/10-19 Life Admin/11 Me, Myself, & I/11.00 Me, Myself, & I.md b/tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/jdex/10-19 Life Admin/11 Me, Myself, & I/11.00 Me, Myself, & I.md new file mode 100644 index 0000000..e69de29 diff --git a/tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/jdex/10-19 Life Admin/11 Me, Myself, & I/11.11 Me.md b/tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/jdex/10-19 Life Admin/11 Me, Myself, & I/11.11 Me.md new file mode 100644 index 0000000..e69de29 diff --git a/tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/jdex/10-19 Life Admin/11 Me, Myself, & I/11.12 Myself.md b/tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/jdex/10-19 Life Admin/11 Me, Myself, & I/11.12 Myself.md new file mode 100644 index 0000000..e69de29 diff --git a/tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/jdlint.toml b/tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/jdlint.toml new file mode 100644 index 0000000..2214a20 --- /dev/null +++ b/tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/jdlint.toml @@ -0,0 +1,73 @@ +[linter] +json_output = true +ignore = [".placeholder"] + +[[system.roots]] +name = "Files" +path = "files" + +[system.jdex] +path = "jdex" + +## ########################################################### +# Partially Nested +## ########################################################### +[[system.jdex.children]] +name = "JDex Area Folder" +format = "/#A/0-/=A/9 /*Area/" + +[[system.jdex.children.children]] +name = "JDex Area Management Folder" +format = "/=A/0 Management of area /=A/0-/=A/9" + +[[system.jdex.children.children.notes]] +name = "JDex Area Note" +format = "/=A/0.00 /*Name/.md" +ids = ["/=A/0-/=A/9", "/=A/0.00"] + +[[system.jdex.children.children]] +name = "JDex Bad Area Management Folder" +format = "/=A/0 /*AreaManagement/" +forbidden = true + +[[system.jdex.children.children]] +name = "JDex Category Folder" +format = "/=A//#C/ /*Category/" + +[[system.jdex.children.children.notes]] +name = "JDex Area Note" +format = "/=A/0.00 /*Name/.md" +ids = ["/=A/0-/=A/9", "/=A/0.00"] + +[[system.jdex.children.children.notes]] +name = "JDex Category Note" +format = "/=A//=C/.00 /*Name/.md" +ids = ["/=A//=C/", "/=A//=C/.00"] + +[[system.jdex.children.children.notes]] +name = "JDex Id Note" +format = "/=A//=C/./##ID/ /*Name/.md" +ids = ["/=A//=C/./=ID/"] + +## ########################################################### +# Standard +## ########################################################### +[[system.children]] +name = "Area" +format = "/#A/0-/=A/9 /*Area/" +jdex_note = "/=A/0.00 /=Area/.md" +id = "/=A/0-/=A/9" + +[[system.children.children]] +name = "Category" +format = "/=A//#C/ /*Category/" +jdex_note = "/=A//=C/.00 /=Category/.md" +id = "/=A//=C/" + +[[system.children.children.children]] +name = "ID" +format = "/=A//=C/./##ID/ /*IDName/" +jdex_note = "/=A//=C/./=ID/ /=IDName/.md" +id = "/=A//=C/./=ID/" +can_be_file = true +allow_arbitrary_contents = true diff --git a/tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/result.json b/tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/result.json new file mode 100644 index 0000000..0ae1e23 --- /dev/null +++ b/tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/result.json @@ -0,0 +1,16 @@ +{ + "errors": {}, + "jdex_errors": [ + { + "type": "JDEX_ENCOUNTERED_FORBIDDEN_FOLDER", + "matched_pattern": { + "name": [ + "JDex Area Folder", + "JDex Bad Area Management Folder" + ], + "format": "/=A/0 /*AreaManagement/" + }, + "file": "jdex/10-19 Life Admin/10 Managemnt of area 10-19" + } + ] +} \ No newline at end of file diff --git a/tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/files/10-19 Life Admin/11 Me, Myself, & I/11.11 Me/Content b/tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/files/10-19 Life Admin/11 Me, Myself, & I/11.11 Me/Content new file mode 100644 index 0000000..e69de29 diff --git a/tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/files/10-19 Life Admin/11 Me, Myself, & I/11.12 Myself/Content b/tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/files/10-19 Life Admin/11 Me, Myself, & I/11.12 Myself/Content new file mode 100644 index 0000000..e69de29 diff --git a/tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/jdex/10-19 Life Admin/10 Management of area 10-19/10.00 Life Admin.md b/tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/jdex/10-19 Life Admin/10 Management of area 10-19/10.00 Life Admin.md new file mode 100644 index 0000000..e69de29 diff --git a/tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/jdex/10-19 Life Admin/11 Me, Myself, & I/11.00 Me, Myself, & I.md b/tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/jdex/10-19 Life Admin/11 Me, Myself, & I/11.00 Me, Myself, & I.md new file mode 100644 index 0000000..e69de29 diff --git a/tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/jdex/10-19 Life Admin/11 Me, Myself, & I/11.01 Inbxo.md b/tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/jdex/10-19 Life Admin/11 Me, Myself, & I/11.01 Inbxo.md new file mode 100644 index 0000000..e69de29 diff --git a/tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/jdex/10-19 Life Admin/11 Me, Myself, & I/11.11 Me.md b/tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/jdex/10-19 Life Admin/11 Me, Myself, & I/11.11 Me.md new file mode 100644 index 0000000..e69de29 diff --git a/tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/jdex/10-19 Life Admin/11 Me, Myself, & I/11.12 Myself.md b/tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/jdex/10-19 Life Admin/11 Me, Myself, & I/11.12 Myself.md new file mode 100644 index 0000000..e69de29 diff --git a/tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/jdlint.toml b/tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/jdlint.toml new file mode 100644 index 0000000..f69e2e7 --- /dev/null +++ b/tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/jdlint.toml @@ -0,0 +1,70 @@ +[linter] +json_output = true +ignore = [".placeholder"] + +[[system.roots]] +name = "Files" +path = "files" + +[system.jdex] +path = "jdex" + +## ########################################################### +# Partially Nested +## ########################################################### +[[system.jdex.children]] +name = "JDex Area Folder" +format = "/#A/0-/=A/9 /*Area/" + +[[system.jdex.children.children]] +name = "JDex Category Folder" +format = "/=A//#C/ /*Category/" + +[[system.jdex.children.children.notes]] +name = "JDex Area Note" +format = "/=A/0.00 /*Name/.md" +ids = ["/=A/0-/=A/9", "/=A/0.00"] + +[[system.jdex.children.children.notes]] +name = "JDex Category Note" +format = "/=A//=C/.00 /*Name/.md" +ids = ["/=A//=C/", "/=A//=C/.00"] + +[[system.jdex.children.children.notes]] +name = "JDex Inbox" +format = "/=A//=C/.01 Inbox.md" +ids = ["/=A//=C/.01"] + +[[system.jdex.children.children.notes]] +name = "JDex Bad Inbox" +format = "/=A//=C/.01 /*Name/" +ids = ["/=A//=C/.01"] +forbidden = true + +[[system.jdex.children.children.notes]] +name = "JDex Id Note" +format = "/=A//=C/./##ID/ /*Name/.md" +ids = ["/=A//=C/./=ID/"] + +## ########################################################### +# Standard +## ########################################################### +[[system.children]] +name = "Area" +format = "/#A/0-/=A/9 /*Area/" +jdex_note = "/=A/0.00 /=Area/.md" +id = "/=A/0-/=A/9" + +[[system.children.children]] +name = "Category" +format = "/=A//#C/ /*Category/" +jdex_note = "/=A//=C/.00 /=Category/.md" +id = "/=A//=C/" + +[[system.children.children.children]] +name = "ID" +format = "/=A//=C/./##ID/ /*IDName/" +jdex_note = "/=A//=C/./=ID/ /=IDName/.md" +id = "/=A//=C/./=ID/" +can_be_file = true +allow_arbitrary_contents = true diff --git a/tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/result.json b/tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/result.json new file mode 100644 index 0000000..713deef --- /dev/null +++ b/tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/result.json @@ -0,0 +1,17 @@ +{ + "errors": {}, + "jdex_errors": [ + { + "type": "JDEX_ENCOUNTERED_FORBIDDEN_NOTE", + "matched_pattern": { + "name": [ + "JDex Area Folder", + "JDex Category Folder", + "JDex Bad Inbox" + ], + "format": "/=A//=C/.01 /*Name/" + }, + "file": "jdex/10-19 Life Admin/11 Me, Myself, & I/11.01 Inbxo.md" + } + ] +} \ No newline at end of file From ef02a28fd9800fbd7b8b386f47101f507039d72a Mon Sep 17 00:00:00 2001 From: SiriusStarr <2049163+SiriusStarr@users.noreply.github.com> Date: Tue, 21 Jul 2026 16:44:38 -0700 Subject: [PATCH 14/23] Report ignored errors and allow specific structure --- jdlint.py | 261 +++++++++++------- run_tests.py | 10 +- .../jdlint.toml | 8 +- tests/DUPLICATE_ID/jdlint.toml | 8 +- tests/EMPTY_FOLDER/jdlint.toml | 4 +- .../ENCOUNTERED_FORBIDDEN_FOLDER/jdlint.toml | 14 +- tests/FILE_WHERE_FOLDER_EXPECTED/jdlint.toml | 6 +- tests/FOLDER_SHOULD_BE_EMPTY/jdlint.toml | 10 +- tests/ID_DIFFERENT_FROM_JDEX/jdlint.toml | 6 +- tests/ID_NOT_IN_JDEX/jdlint.toml | 6 +- .../jdlint.toml | 2 +- tests/JDEX_DUPLICATE_ID/jdlint.toml | 2 +- tests/JDEX_EMPTY_FOLDER/jdlint.toml | 2 +- .../jdlint.toml | 6 +- .../jdlint.toml | 6 +- .../jdlint.toml | 2 +- .../jdlint.toml | 2 +- tests/can_be_file/jdlint.toml | 6 +- 18 files changed, 216 insertions(+), 145 deletions(-) diff --git a/jdlint.py b/jdlint.py index 4c3be4d..701ff38 100755 --- a/jdlint.py +++ b/jdlint.py @@ -7,6 +7,7 @@ import argparse import dataclasses import json +import textwrap import os import re import sys @@ -65,7 +66,7 @@ def __init__(self, key: str, expected: str, got: str) -> None: class ConfigValueError(ConfigError): """A bad value in the jdlint config.""" - def __init__(self, key, issue, got): + def __init__(self, key: str, issue: str, got: str) -> None: """Create a value error, given the key it occurs at, the issue with the value, and the actual value.""" super().__init__(key, f"Bad value. Got: {got} Issue: {issue}") @@ -73,7 +74,7 @@ def __init__(self, key, issue, got): class ConfigConflictError(ConfigError): """A conflict in the jdlint config.""" - def __init__(self, key, issue): + def __init__(self, key: str, issue: str) -> None: """Create a conflict error, given the key it occurs at and the issue.""" super().__init__(key, f"Conflict in config. Issue: {issue}") @@ -86,7 +87,9 @@ def __init__(self, key, issue): class ConfigSystemRoot: """A root (base folder) of a JD system to check for correctness, e.g. ~/Documents.""" - def __init__(self, at: str, from_file: dict) -> None: + def __init__( + self, at: str, default_structure: list[ConfigSystemTier], from_file: dict + ) -> None: """Create a valid configuration given a loaded section of a config file.""" # Acquire and set defaults if "name" not in from_file: @@ -109,7 +112,7 @@ def __init__(self, at: str, from_file: dict) -> None: raise ConfigValueError( f"{at}.path", "Root path isn't a folder that exists!", - self.path, + str(self.path), ) if not isinstance(self.ignore, list): raise ConfigTypeError( @@ -121,6 +124,26 @@ def __init__(self, at: str, from_file: dict) -> None: if not isinstance(r, str): raise ConfigTypeError(f"{at}.ignore", "str", type(r).__name__) + # Load specialized structure, if any + if "children" in from_file: + self.children = [ + ConfigSystemTier( + f"{at}.children[{i}]", + ConfigFormatAncestorInfo((), ()), + v, + ) + for i, v in enumerate(from_file.pop("children", [])) + ] + elif default_structure: + self.children = default_structure + else: + raise ( + ConfigConflictError( + f"{at}", + "Either system.default.children must be specified or every root must specify its own children.", + ) + ) + # Ensure no extra fields for key in from_file: raise ConfigExtraKeyError(f"{at}.{key}", tuple(self.__dict__.keys())) @@ -142,7 +165,7 @@ def __init__(self, at: str, from_file: dict) -> None: raise ConfigValueError( f"{at}.path", "JDex path isn't a folder that exists!", - self.path, + str(self.path), ) if not isinstance(self.ignore, list): raise ConfigTypeError( @@ -170,11 +193,6 @@ def __init__(self, at: str, from_file: dict) -> None: ) for i, v in enumerate(from_file.pop("notes", [])) ] - if self.notes and self.children: - raise ConfigConflictError( - at, - "Only one of notes and children may be specified.", - ) # Ensure no extra fields for key in from_file: @@ -230,9 +248,20 @@ class ConfigSystem: def __init__(self, from_file: dict) -> None: """Create a valid configuration given a loaded system section of a config file.""" + + default_structure = [ + ConfigSystemTier( + f"system.default.children[{i}]", + ConfigFormatAncestorInfo((), ()), + v, + ) + for i, v in enumerate(from_file.pop("default", {}).pop("children", [])) + ] + self.roots = [ ConfigSystemRoot( f"system.roots[{i}]", + default_structure, v, ) for i, v in enumerate(from_file.pop("roots", [])) @@ -257,15 +286,6 @@ def __init__(self, from_file: dict) -> None: else: self.jdex = None - self.children = [ - ConfigSystemTier( - f"system.children[{i}]", - ConfigFormatAncestorInfo((), ()), - v, - ) - for i, v in enumerate(from_file.pop("children", [])) - ] - # Ensure no extra fields for key in from_file: raise ConfigExtraKeyError(f"system.{key}", tuple(self.__dict__.keys())) @@ -500,17 +520,6 @@ def __init__( for i, v in enumerate(from_file.pop("notes", [])) ] - if not self.notes and not self.children and not self.format.forbidden: - raise ConfigConflictError( - at, - "A JDex tier must specify either notes or children, or be forbidden.", - ) - - if self.notes and self.children: - raise ConfigConflictError( - at, - "Only one of notes and children may be specified.", - ) if self.notes and self.format.forbidden: raise ConfigConflictError( at, @@ -574,19 +583,19 @@ def __init__( raise ConfigValueError( f"{at}.name", "Malformed name; must not be empty.", - from_file, + str(from_file), ) if self.raw_format.count("/") % 2 != 0: raise ConfigValueError( f"{at}.format", "Malformed format; there must be an even number of / characters. You have an extra one/are missing one.", - from_file, + str(from_file), ) if self.raw_format == "": raise ConfigValueError( f"{at}.format", "Malformed format; must not be empty.", - from_file, + str(from_file), ) regex = [] @@ -654,7 +663,10 @@ def __init__( class Config: - def __init__(self, from_file): + """Valid config for jdlint.""" + + def __init__(self, from_file: dict) -> None: + """Attempt to create a valid config from loaded TOML.""" self.linter = ConfigLinter(from_file.get("linter", {})) if "system" not in from_file: @@ -674,7 +686,7 @@ class Issue: file: PurePath type = None - def display(self) -> str: + def display(self, base_path: PurePath) -> str: """Display this particular instance of an error.""" raise NotImplementedError @@ -690,7 +702,7 @@ class JDexIssue: file: PurePath type = None - def display(self) -> str: + def display(self, base_path: PurePath) -> str: """Display this particular instance of an error.""" raise NotImplementedError @@ -705,9 +717,9 @@ class IssueEmptyFolder(Issue): type: Literal["EMPTY_FOLDER"] = "EMPTY_FOLDER" - def display(self) -> str: + def display(self, base_path: PurePath) -> str: """Display this particular instance of an error.""" - return f"{self.file!s}" + return f"{self.file.relative_to(base_path)!s}" def explain(self) -> _Explanation: """Explain what this error is.""" @@ -724,9 +736,9 @@ class IssueFileWhereFolderExpected(Issue): matched_pattern: ContentPattern type: Literal["FILE_WHERE_FOLDER_EXPECTED"] = "FILE_WHERE_FOLDER_EXPECTED" - def display(self) -> str: + def display(self, base_path: PurePath) -> str: """Display this particular instance of an error.""" - return f'{self.file!s} (matched "{self.matched_pattern}")' + return f"{self.file.relative_to(base_path)!s}\n (matched {_print_pattern(self.matched_pattern)})" def explain(self) -> _Explanation: """Explain what this error is.""" @@ -745,9 +757,9 @@ class IssueArbitraryContentWhereNotAllowed(Issue): "ARBITRARY_CONTENT_WHERE_NOT_ALLOWED" ) - def display(self) -> str: + def display(self, base_path: PurePath) -> str: """Display this particular instance of an error.""" - return f'{self.file!s} (matched none of "{self.possible_formats}")' + return f"{self.file.relative_to(base_path)!s}\n{textwrap.indent(_print_unmatched_patterns(self.possible_formats), ' ')}" def explain(self) -> _Explanation: """Explain what this error is.""" @@ -764,9 +776,11 @@ class IssueFolderShouldBeEmpty(Issue): children: tuple[PurePath, ...] type: Literal["FOLDER_SHOULD_BE_EMPTY"] = "FOLDER_SHOULD_BE_EMPTY" - def display(self) -> str: + def display(self, base_path: PurePath) -> str: """Display this particular instance of an error.""" - return f"{self.file!s} (has {len(self.children)} children)" + return ( + f"{self.file.relative_to(base_path)!s}\n has {len(self.children)} children" + ) def explain(self) -> _Explanation: """Explain what this error is.""" @@ -784,7 +798,7 @@ class IssueDuplicateID(Issue): id: str type: Literal["DUPLICATE_ID"] = "DUPLICATE_ID" - def display(self) -> str: + def display(self, base_path: PurePath) -> str: """Display this particular instance of an error.""" return f"{self.id}:\n " + "\n ".join([str(f.name) for f in self.files]) @@ -803,9 +817,9 @@ class IssueIDNotInJDex(Issue): id: str type: Literal["ID_NOT_IN_JDEX"] = "ID_NOT_IN_JDEX" - def display(self) -> str: + def display(self, base_path: PurePath) -> str: """Display this particular instance of an error.""" - return f"{self.file} [ID: {self.id}]" + return f"{self.file.relative_to(base_path)!s} [ID: {self.id}]" def explain(self) -> _Explanation: """Explain what this error is.""" @@ -824,9 +838,10 @@ class IssueIDDifferentFromJDex(Issue): known_jdex_notes: list[PurePath] type: Literal["ID_DIFFERENT_FROM_JDEX"] = "ID_DIFFERENT_FROM_JDEX" - def display(self) -> str: + def display(self, base_path: PurePath) -> str: """Display this particular instance of an error.""" - return f"{self.id}: {self.file} [Expected JDex: {self.expected_jdex_note}i; actual JDex: {self.known_jdex_notes}]" + known = "\n".join((n.name for n in self.known_jdex_notes)) + return f"{self.file.relative_to(base_path)!s}\n ID: {self.id}\n Expected:\n {self.expected_jdex_note}\n Actual:\n{textwrap.indent(known, ' ')}]" def explain(self) -> _Explanation: """Explain what this error is.""" @@ -843,9 +858,11 @@ class IssueEncounteredForbiddenFolder(Issue): matched_pattern: ContentPattern type: Literal["ENCOUNTERED_FORBIDDEN_FOLDER"] = "ENCOUNTERED_FORBIDDEN_FOLDER" - def display(self) -> str: + def display(self, base_path: PurePath) -> str: """Display this particular instance of an error.""" - return f'{self.file!s} (matched "{self.matched_pattern}")' + return ( + f'{self.file.relative_to(base_path)!s} (matched "{self.matched_pattern}")' + ) def explain(self) -> _Explanation: """Explain what this error is.""" @@ -871,7 +888,7 @@ class JDexIssueDuplicateID(JDexIssue): id: str type: Literal["JDEX_DUPLICATE_ID"] = "JDEX_DUPLICATE_ID" - def display(self) -> str: + def display(self, base_path: PurePath) -> str: """Display this particular instance of an error.""" return f"{self.id}:\n " + "\n ".join([str(f.name) for f in self.files]) @@ -890,9 +907,11 @@ class JDexIssueFileWhereFolderExpected(JDexIssue): matched_pattern: ContentPattern type: Literal["JDEX_FILE_WHERE_FOLDER_EXPECTED"] = "JDEX_FILE_WHERE_FOLDER_EXPECTED" - def display(self) -> str: + def display(self, base_path: PurePath) -> str: """Display this particular instance of an error.""" - return f'{self.file!s} (matched "{self.matched_pattern}")' + return ( + f'{self.file.relative_to(base_path)!s} (matched "{self.matched_pattern}")' + ) def explain(self) -> _Explanation: """Explain what this error is.""" @@ -909,9 +928,11 @@ class JDexIssueFolderWhereNoteExpected(JDexIssue): matched_pattern: ContentPattern type: Literal["JDEX_FOLDER_WHERE_NOTE_EXPECTED"] = "JDEX_FOLDER_WHERE_NOTE_EXPECTED" - def display(self) -> str: + def display(self, base_path: PurePath) -> str: """Display this particular instance of an error.""" - return f'{self.file!s} (matched "{self.matched_pattern}")' + return ( + f'{self.file.relative_to(base_path)!s} (matched "{self.matched_pattern}")' + ) def explain(self) -> _Explanation: """Explain what this error is.""" @@ -938,9 +959,9 @@ class JDexIssueArbitraryContentWhereNotAllowed(JDexIssue): "JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED" ) - def display(self) -> str: + def display(self, base_path: PurePath) -> str: """Display this particular instance of an error.""" - return f'{self.file!s} (matched none of "{self.possible_formats}")' + return f"{self.file.relative_to(base_path)!s}\n{textwrap.indent(_print_unmatched_patterns(self.possible_formats), ' ')}" def explain(self) -> _Explanation: """Explain what this error is.""" @@ -956,9 +977,9 @@ class JDexIssueEmptyFolder(JDexIssue): type: Literal["JDEX_EMPTY_FOLDER"] = "JDEX_EMPTY_FOLDER" - def display(self) -> str: + def display(self, base_path: PurePath) -> str: """Display this particular instance of an error.""" - return f"{self.file!s}" + return f"{self.file.relative_to(base_path)!s}" def explain(self) -> _Explanation: """Explain what this error is.""" @@ -977,9 +998,11 @@ class JDexIssueEncounteredForbiddenFolder(JDexIssue): "JDEX_ENCOUNTERED_FORBIDDEN_FOLDER" ) - def display(self) -> str: + def display(self, base_path: PurePath) -> str: """Display this particular instance of an error.""" - return f'{self.file!s} (matched "{self.matched_pattern}")' + return ( + f'{self.file.relative_to(base_path)!s} (matched "{self.matched_pattern}")' + ) def explain(self) -> _Explanation: """Explain what this error is.""" @@ -996,9 +1019,11 @@ class JDexIssueEncounteredForbiddenNote(JDexIssue): matched_pattern: ContentPattern type: Literal["JDEX_ENCOUNTERED_FORBIDDEN_NOTE"] = "JDEX_ENCOUNTERED_FORBIDDEN_NOTE" - def display(self) -> str: + def display(self, base_path: PurePath) -> str: """Display this particular instance of an error.""" - return f'{self.file!s} (matched "{self.matched_pattern}")' + return ( + f'{self.file.relative_to(base_path)!s} (matched "{self.matched_pattern}")' + ) def explain(self) -> _Explanation: """Explain what this error is.""" @@ -1045,14 +1070,31 @@ class SystemFolder: children: dict[str, list[SystemFolder]] +@dataclass(frozen=True) +class JDexLintResults: + """All errors returned from linting the JDex.""" + + errors: list[JDexIssue] + path: PurePath + entries: dict[str, list[PurePath]] + + +@dataclass(frozen=True) +class RootLintResults: + """All errors returned from linting a system root.""" + + errors: list[Issue] + path: PurePath + structure: dict[str, list[SystemFolder]] + + @dataclass(frozen=True) class LintResults: """All errors returned from linting files, as well as the JDex and filesystems structures.""" - errors: dict[str, list[Issue]] - jdex_errors: list[JDexIssue] - jdex: dict[str, list[PurePath]] - structure: dict[str, dict[str, list[SystemFolder]]] + jdex: None | JDexLintResults + roots: dict[str, RootLintResults] + ignored_errs: int @dataclass @@ -1092,6 +1134,15 @@ def default(self, o: object) -> object: return super().default(o) +def _print_pattern(p: ContentPattern) -> str: + return f"{'/'.join(p.name)}: {p.format}" + + +def _print_unmatched_patterns(ps: tuple[ContentPattern, ...]) -> str: + formats = "\n".join(_print_pattern(p) for p in ps) + return f"matched none of:\n{textwrap.indent(formats, ' ')}" + + def _sort_jdex_error(e: JDexIssue) -> tuple[str, tuple[tuple[str, ...], str]]: # Sort errors alphabetically by type, then by file affected # This is split from _sort_error for type-checking nonsense @@ -1130,21 +1181,6 @@ def _entry_is_ignored( E = TypeVar("E") -# def _error_if_dups( # Python's types are horrid and it just is awful to try to type this better -# make_error_type: Callable[[str], Any], -# make_error: Callable[[Any, list[File]], E], -# d: dict[str, list[tuple[Any, File]]], -# ) -> list[E]: -# return [ -# make_error( -# make_error_type(k), -# sorted([file for (_, file) in v], key=_sort_file), -# ) -# for k, v in d.items() -# if len(v) > 1 -# ] - - def _insert_append(k, v, d) -> None: # noqa: ANN001 """Add value as a singleton if it's not already in the dict, else append it to the list.""" if k not in d: @@ -1348,7 +1384,7 @@ def _process_system_level_and_children( ignored: tuple[str], bound_segments: dict[str, str], path: os.PathLike, - tier: ConfigSystem | ConfigSystemTier, + tier: ConfigSystemRoot | ConfigSystemTier, jdex: None | dict[str, list[PurePath]], by_id_dict: dict[str, list[tuple[str | None, PurePath]]], ) -> tuple[dict[str, list[SystemFolder]], list[Issue]]: @@ -1419,7 +1455,7 @@ def _process_system_level_and_children( child_id, ( child.jdex_note.build( - {**bound_segments, **match.groupdict()} + {**bound_segments, **match.groupdict()}, ) if child.jdex_note else None, @@ -1448,7 +1484,8 @@ def _process_system_level_and_children( if children_that_should_not_be: accumulated_errors.append( IssueFolderShouldBeEmpty( - PurePath(path), tuple(children_that_should_not_be) + PurePath(path), + tuple(children_that_should_not_be), ), ) if not has_content and ( @@ -1467,7 +1504,12 @@ def _process_system_root( ) -> tuple[dict[str, list[SystemFolder]], list[Issue]]: by_id: dict[str, list[tuple[str | None, PurePath]]] = {} (root_structure, root_errors) = _process_system_level_and_children( - ignored + root.ignore, {}, root.path, system, jdex, by_id + ignored + root.ignore, + {}, + root.path, + root, + jdex, + by_id, ) # Check for duplicate IDs @@ -1489,8 +1531,11 @@ def _process_system_root( if expected_jdex_note and expected_jdex_note not in jdex_notes: id_errors.append( IssueIDDifferentFromJDex( - f, id, expected_jdex_note, jdex[id] - ) + f, + id, + expected_jdex_note, + jdex[id], + ), ) return (root_structure, root_errors + duplicate_id_errors + id_errors) @@ -1504,8 +1549,8 @@ def lint_system(config: Config) -> LintResults: config.linter.ignore, config.system.jdex, ) - errors = {} - structure = {} + roots = {} + ignored_errors = 0 for root in config.system.roots: (root_structure, root_errors) = _process_system_root( config.linter.ignore, @@ -1513,15 +1558,35 @@ def lint_system(config: Config) -> LintResults: config.system, jdex_notes if config.system.jdex else None, ) - if root_errors: - errors[root.name] = sorted(root_errors, key=_sort_error) - structure[root.name] = root_structure + ignored_errors += sum( + (1 for e in root_errors if e.type in config.linter.disable_rules) + ) + root_errors = [ + e for e in root_errors if e.type not in config.linter.disable_rules + ] + roots[root.name] = RootLintResults( + sorted(root_errors, key=_sort_error), + root.path, + root_structure, + ) + + ignored_jdex_errors = sum( + (1 for e in jdex_errors if e.type in config.linter.disable_rules) + ) return LintResults( - errors, - sorted(jdex_errors, key=_sort_jdex_error), - jdex_notes, - structure, + JDexLintResults( + sorted( + [e for e in jdex_errors if e.type not in config.linter.disable_rules], + key=_sort_jdex_error, + ), + config.system.jdex.path, + jdex_notes, + ) + if config.system.jdex + else None, + roots, + ignored_errors + ignored_jdex_errors, ) diff --git a/run_tests.py b/run_tests.py index ac580db..f73d1fe 100755 --- a/run_tests.py +++ b/run_tests.py @@ -47,8 +47,14 @@ def tests(self) -> None: actual = json.loads( json.dumps( { - "errors": results.errors, - "jdex_errors": results.jdex_errors, + "errors": { + root_name: root.errors + for root_name, root in results.roots.items() + if root.errors + }, + "jdex_errors": results.jdex.errors + if results.jdex + else [], }, cls=jdlint._EnhancedJSONEncoder, ) diff --git a/tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml b/tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml index f848783..890c8c6 100644 --- a/tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml +++ b/tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml @@ -5,23 +5,23 @@ json_output = true name = "JDex" path = "files" -[[system.children]] +[[system.default.children]] name = "A" format = "/#A/" id = "/=A/" -[[system.children.children]] +[[system.default.children.children]] name = "B1" format = "/*Name//=A/" id = "/=A/./=Name/" allow_arbitrary_contents = true -[[system.children.children]] +[[system.default.children.children]] name = "B2" id = "/=A/./=Name/" format = "/=A//*Name/" -[[system.children.children.children]] +[[system.default.children.children.children]] name = "C2" format = "/=A//=Name/" id = "/=A/./=Name/ Child" diff --git a/tests/DUPLICATE_ID/jdlint.toml b/tests/DUPLICATE_ID/jdlint.toml index 760db43..045500d 100644 --- a/tests/DUPLICATE_ID/jdlint.toml +++ b/tests/DUPLICATE_ID/jdlint.toml @@ -5,23 +5,23 @@ json_output = true name = "JDex" path = "files" -[[system.children]] +[[system.default.children]] name = "A" format = "/#A/" id = "/=A/" -[[system.children.children]] +[[system.default.children.children]] name = "B" format = "/=A//#ID//*Name/" id = "/=A/./=ID/" allow_arbitrary_contents = true -[[system.children]] +[[system.default.children]] name = "C" format = "B/#B/" id = "/=B/" -[[system.children.children]] +[[system.default.children.children]] name = "D" format = "/=B//#ID//*Name/" id = "/=B/./=ID/" diff --git a/tests/EMPTY_FOLDER/jdlint.toml b/tests/EMPTY_FOLDER/jdlint.toml index 294f8dd..aa6004d 100644 --- a/tests/EMPTY_FOLDER/jdlint.toml +++ b/tests/EMPTY_FOLDER/jdlint.toml @@ -6,12 +6,12 @@ ignore = [".placeholder"] name = "JDex" path = "files" -[[system.children]] +[[system.default.children]] name = "A" format = "/#A/" id = "/=A/" -[[system.children.children]] +[[system.default.children.children]] name = "B" format = "/=A//*Name/" id = "/=A/./=Name/" diff --git a/tests/ENCOUNTERED_FORBIDDEN_FOLDER/jdlint.toml b/tests/ENCOUNTERED_FORBIDDEN_FOLDER/jdlint.toml index 0c43e6b..9fffbc9 100644 --- a/tests/ENCOUNTERED_FORBIDDEN_FOLDER/jdlint.toml +++ b/tests/ENCOUNTERED_FORBIDDEN_FOLDER/jdlint.toml @@ -9,39 +9,39 @@ path = "files" ## ########################################################### # Standard ## ########################################################### -[[system.children]] +[[system.default.children]] name = "Area" format = "/#A/0-/=A/9 /*Area/" id = "/=A/0-/=A/9" -[[system.children.children]] +[[system.default.children.children]] name = "Category" format = "/=A//#C/ /*Category/" id = "/=A//=C/" -[[system.children.children.children]] +[[system.default.children.children.children]] name = "Inbox" format = "/=A//=C/.01 Inbox" id = "/=A//=C/.01" -[[system.children.children.children]] +[[system.default.children.children.children]] name = "Bad Inbox" format = "/=A//=C/.01 /*Inbox/" id = "/=A//=C/.01" forbidden = true -[[system.children.children.children]] +[[system.default.children.children.children]] name = "Header" format = "/=A//=C/./#I/0 ■ /*Header/" id = "/=A//=C/./=I/0" -[[system.children.children.children]] +[[system.default.children.children.children]] name = "Bad Header" format = "/=A//=C/./#I/0 /*Header/" id = "/=A//=C/./=I/0" forbidden = true -[[system.children.children.children]] +[[system.default.children.children.children]] name = "ID" format = "/=A//=C/./##ID/ /*IDName/" id = "/=A//=C/./=ID/" diff --git a/tests/FILE_WHERE_FOLDER_EXPECTED/jdlint.toml b/tests/FILE_WHERE_FOLDER_EXPECTED/jdlint.toml index 830f230..2a7cc92 100644 --- a/tests/FILE_WHERE_FOLDER_EXPECTED/jdlint.toml +++ b/tests/FILE_WHERE_FOLDER_EXPECTED/jdlint.toml @@ -5,17 +5,17 @@ json_output = true name = "JDex" path = "files" -[[system.children]] +[[system.default.children]] name = "A" format = "/#A/" id = "/=A/" -[[system.children.children]] +[[system.default.children.children]] name = "B" format = "/=A//*Name/" id = "/=A/./=Name/" -[[system.children.children.children]] +[[system.default.children.children.children]] name = "C" format = "/=A//=Name/" id = "/=A/./=Name/" diff --git a/tests/FOLDER_SHOULD_BE_EMPTY/jdlint.toml b/tests/FOLDER_SHOULD_BE_EMPTY/jdlint.toml index 37181a3..3ad276a 100644 --- a/tests/FOLDER_SHOULD_BE_EMPTY/jdlint.toml +++ b/tests/FOLDER_SHOULD_BE_EMPTY/jdlint.toml @@ -9,31 +9,31 @@ path = "files" ## ########################################################### # Standard ## ########################################################### -[[system.children]] +[[system.default.children]] name = "Area" format = "/#A/0-/=A/9 /*Area/" jdex_note = "/=A/0.00 /=Area/.md" id = "/=A/0-/=A/9" -[[system.children.children]] +[[system.default.children.children]] name = "Category" format = "/=A//#C/ /*Category/" jdex_note = "/=A//=C/.00 /=Category/.md" id = "/=A//=C/" -[[system.children.children.children]] +[[system.default.children.children.children]] name = "Header" format = "/=A//=C/./#I/0 ■ /*Header/" jdex_note = "/=A//=C/./=I/0 ■ /=Header/.md" id = "/=A//=C/./=I/0" -[[system.children.children.children]] +[[system.default.children.children.children]] name = "Inbox" format = "/=A//=C/.01 /*Inbox/" jdex_note = "/=A//=C/.01 /=Inbox/.md" id = "/=A//=C/.01" -[[system.children.children.children]] +[[system.default.children.children.children]] name = "ID" format = "/=A//=C/./##ID/ /*IDName/" jdex_note = "/=A//=C/./=ID/ /=IDName/.md" diff --git a/tests/ID_DIFFERENT_FROM_JDEX/jdlint.toml b/tests/ID_DIFFERENT_FROM_JDEX/jdlint.toml index e7918df..2e63069 100644 --- a/tests/ID_DIFFERENT_FROM_JDEX/jdlint.toml +++ b/tests/ID_DIFFERENT_FROM_JDEX/jdlint.toml @@ -38,19 +38,19 @@ ids = ["/=A//=C/./=ID/"] ## ########################################################### # Standard ## ########################################################### -[[system.children]] +[[system.default.children]] name = "Area" format = "/#A/0-/=A/9 /*Area/" jdex_note = "/=A/0.00 /=Area/.md" id = "/=A/0-/=A/9" -[[system.children.children]] +[[system.default.children.children]] name = "Category" format = "/=A//#C/ /*Category/" jdex_note = "/=A//=C/.00 /=Category/.md" id = "/=A//=C/" -[[system.children.children.children]] +[[system.default.children.children.children]] name = "ID" format = "/=A//=C/./##ID/ /*IDName/" jdex_note = "/=A//=C/./=ID/ /=IDName/.md" diff --git a/tests/ID_NOT_IN_JDEX/jdlint.toml b/tests/ID_NOT_IN_JDEX/jdlint.toml index e7918df..2e63069 100644 --- a/tests/ID_NOT_IN_JDEX/jdlint.toml +++ b/tests/ID_NOT_IN_JDEX/jdlint.toml @@ -38,19 +38,19 @@ ids = ["/=A//=C/./=ID/"] ## ########################################################### # Standard ## ########################################################### -[[system.children]] +[[system.default.children]] name = "Area" format = "/#A/0-/=A/9 /*Area/" jdex_note = "/=A/0.00 /=Area/.md" id = "/=A/0-/=A/9" -[[system.children.children]] +[[system.default.children.children]] name = "Category" format = "/=A//#C/ /*Category/" jdex_note = "/=A//=C/.00 /=Category/.md" id = "/=A//=C/" -[[system.children.children.children]] +[[system.default.children.children.children]] name = "ID" format = "/=A//=C/./##ID/ /*IDName/" jdex_note = "/=A//=C/./=ID/ /=IDName/.md" diff --git a/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml b/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml index 349c081..5f75ea0 100644 --- a/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml +++ b/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml @@ -36,7 +36,7 @@ name = "C2" format = "/=A//=Name/.md" ids = ["/=A/./=Name/"] -[[system.children]] +[[system.default.children]] name = "Area" format = "/#A/" jdex_note = "/=A/.md" diff --git a/tests/JDEX_DUPLICATE_ID/jdlint.toml b/tests/JDEX_DUPLICATE_ID/jdlint.toml index fee11b6..f9a7637 100644 --- a/tests/JDEX_DUPLICATE_ID/jdlint.toml +++ b/tests/JDEX_DUPLICATE_ID/jdlint.toml @@ -22,7 +22,7 @@ name = "B" format = "/=A//#ID//*Name/.md" ids = ["/=A/./=ID/"] -[[system.children]] +[[system.default.children]] name = "Area" format = "/#A/" jdex_note = "/=A/.md" diff --git a/tests/JDEX_EMPTY_FOLDER/jdlint.toml b/tests/JDEX_EMPTY_FOLDER/jdlint.toml index db4bc87..ad7d35e 100644 --- a/tests/JDEX_EMPTY_FOLDER/jdlint.toml +++ b/tests/JDEX_EMPTY_FOLDER/jdlint.toml @@ -23,7 +23,7 @@ name = "C1" format = "X.md" ids = ["/=A/"] -[[system.children]] +[[system.default.children]] name = "Area" format = "/#A/" jdex_note = "X.md" diff --git a/tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/jdlint.toml b/tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/jdlint.toml index 2214a20..e85ce8d 100644 --- a/tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/jdlint.toml +++ b/tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/jdlint.toml @@ -52,19 +52,19 @@ ids = ["/=A//=C/./=ID/"] ## ########################################################### # Standard ## ########################################################### -[[system.children]] +[[system.default.children]] name = "Area" format = "/#A/0-/=A/9 /*Area/" jdex_note = "/=A/0.00 /=Area/.md" id = "/=A/0-/=A/9" -[[system.children.children]] +[[system.default.children.children]] name = "Category" format = "/=A//#C/ /*Category/" jdex_note = "/=A//=C/.00 /=Category/.md" id = "/=A//=C/" -[[system.children.children.children]] +[[system.default.children.children.children]] name = "ID" format = "/=A//=C/./##ID/ /*IDName/" jdex_note = "/=A//=C/./=ID/ /=IDName/.md" diff --git a/tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/jdlint.toml b/tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/jdlint.toml index f69e2e7..e86d83a 100644 --- a/tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/jdlint.toml +++ b/tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/jdlint.toml @@ -49,19 +49,19 @@ ids = ["/=A//=C/./=ID/"] ## ########################################################### # Standard ## ########################################################### -[[system.children]] +[[system.default.children]] name = "Area" format = "/#A/0-/=A/9 /*Area/" jdex_note = "/=A/0.00 /=Area/.md" id = "/=A/0-/=A/9" -[[system.children.children]] +[[system.default.children.children]] name = "Category" format = "/=A//#C/ /*Category/" jdex_note = "/=A//=C/.00 /=Category/.md" id = "/=A//=C/" -[[system.children.children.children]] +[[system.default.children.children.children]] name = "ID" format = "/=A//=C/./##ID/ /*IDName/" jdex_note = "/=A//=C/./=ID/ /=IDName/.md" diff --git a/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/jdlint.toml b/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/jdlint.toml index a518154..6c0a100 100644 --- a/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/jdlint.toml +++ b/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/jdlint.toml @@ -26,7 +26,7 @@ name = "C" format = "/=A//=Name/.md" ids = ["/=A/./=Name/"] -[[system.children]] +[[system.default.children]] name = "Area" format = "/#A/" jdex_note = "/=A/.md" diff --git a/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/jdlint.toml b/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/jdlint.toml index 210bff3..ded3294 100644 --- a/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/jdlint.toml +++ b/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/jdlint.toml @@ -26,7 +26,7 @@ name = "JDex Note" format = "/=A//=Name/.md" ids = ["/=A/./=Name/"] -[[system.children]] +[[system.default.children]] name = "Area" format = "/#A/" jdex_note = "/=A/.md" diff --git a/tests/can_be_file/jdlint.toml b/tests/can_be_file/jdlint.toml index e6efbe3..9cde5f6 100644 --- a/tests/can_be_file/jdlint.toml +++ b/tests/can_be_file/jdlint.toml @@ -5,17 +5,17 @@ json_output = true name = "JDex" path = "files" -[[system.children]] +[[system.default.children]] name = "A" format = "/#A/" id = "/=A/" -[[system.children.children]] +[[system.default.children.children]] name = "B" format = "/=A//*Name/" id = "/=A/./=Name/" -[[system.children.children.children]] +[[system.default.children.children.children]] name = "C" format = "/=A//=Name/" id = "/=A/./=Name/" From 3e1ab41de41579f9292bdcc210f9c4582ae5be53 Mon Sep 17 00:00:00 2001 From: SiriusStarr <2049163+SiriusStarr@users.noreply.github.com> Date: Tue, 21 Jul 2026 19:26:12 -0700 Subject: [PATCH 15/23] Update README and add args --- .github/workflows/test.yml | 2 +- README.md | 624 +++++++++++-------------------------- images/header_note.png | Bin 3704 -> 0 bytes jdlint.py | 167 +++++----- 4 files changed, 253 insertions(+), 540 deletions(-) delete mode 100644 images/header_note.png diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d667af7..c10ab41 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ["3.10", "3.11", "3.12"] + python-version: [3.11", "3.12", "3.13", "3.14"] # Steps represent a sequence of tasks that will be executed as part of the job diff --git a/README.md b/README.md index ae77eb5..5e02d57 100644 --- a/README.md +++ b/README.md @@ -6,50 +6,36 @@ clean. * [jdlint \[N14.0001\]](#jdlint-n140001) * [Installation/Requirements](#installationrequirements) * [Usage](#usage) - * [With a JDex/Index](#with-a-jdexindex) - * [JDex Formats](#jdex-formats) - * [Alternative Layout for the Standard Zeros](#alternative-layout-for-the-standard-zeros) - * [Ignoring Files](#ignoring-files) - * [Disabling Specific Rules](#disabling-specific-rules) - * [I Am a Robot and Want Something Machine-Readable](#i-am-a-robot-and-want-something-machine-readable) - * [File Errors](#file-errors) - * [`AREA_DIFFERENT_FROM_JDEX`](#area_different_from_jdex) - * [`AREA_NOT_IN_JDEX`](#area_not_in_jdex) - * [`CATEGORY_DIFFERENT_FROM_JDEX`](#category_different_from_jdex) - * [`CATEGORY_IN_WRONG_AREA`](#category_in_wrong_area) - * [`CATEGORY_NOT_IN_JDEX`](#category_not_in_jdex) - * [`DUPLICATE_AREA`](#duplicate_area) - * [`DUPLICATE_CATEGORY`](#duplicate_category) + * [Config File](#config-file) + * [Ignoring Files](#ignoring-files) + * [Disabling Specific Rules](#disabling-specific-rules) + * [I Am a Robot and Want Something Machine-Readable](#i-am-a-robot-and-want-something-machine-readable) + * [Errors](#errors) + * [`ARBITRARY_CONTENT_WHERE_NOT_ALLOWED`](#arbitrary_content_where_not_allowed) * [`DUPLICATE_ID`](#duplicate_id) - * [`FILE_OUTSIDE_ID`](#file_outside_id) - * [`ID_DIFFERENT_FROM_JDEX`](#id_different_from_jdex) - * [`ID_IN_WRONG_CATEGORY`](#id_in_wrong_category) + * [`EMPTY_FOLDER`](#empty_folder) + * [`ENCOUNTERED_FORBIDDEN_FOLDER`](#encountered_forbidden_folder) + * [`FILE_WHERE_FOLDER_EXPECTED`](#file_where_folder_expected) + * [`FOLDER_SHOULD_BE_EMPTY`](#folder_should_be_empty) * [`ID_NOT_IN_JDEX`](#id_not_in_jdex) - * [`INVALID_AREA_NAME`](#invalid_area_name) - * [`INVALID_CATEGORY_NAME`](#invalid_category_name) - * [`INVALID_ID_NAME`](#invalid_id_name) - * [`NONEMPTY_INBOX`](#nonempty_inbox) + * [`ID_DIFFERENT_FROM_JDEX`](#id_different_from_jdex) * [JDex Errors](#jdex-errors) - * [`JDEX_AREA_HEADER_DIFFERENT_FROM_AREA`](#jdex_area_header_different_from_area) - * [`JDEX_AREA_HEADER_WITHOUT_AREA`](#jdex_area_header_without_area) - * [`JDEX_CATEGORY_IN_WRONG_AREA`](#jdex_category_in_wrong_area) - * [`JDEX_DUPLICATE_AREA`](#jdex_duplicate_area) - * [`JDEX_DUPLICATE_AREA_HEADER`](#jdex_duplicate_area_header) - * [`JDEX_DUPLICATE_CATEGORY`](#jdex_duplicate_category) + * [`JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED`](#jdex_arbitrary_content_where_not_allowed) * [`JDEX_DUPLICATE_ID`](#jdex_duplicate_id) - * [`JDEX_FILE_OUTSIDE_CATEGORY`](#jdex_file_outside_category) - * [`JDEX_ID_IN_WRONG_CATEGORY`](#jdex_id_in_wrong_category) - * [`JDEX_INVALID_AREA_NAME`](#jdex_invalid_area_name) - * [`JDEX_INVALID_CATEGORY_NAME`](#jdex_invalid_category_name) - * [`JDEX_INVALID_ID_NAME`](#jdex_invalid_id_name) + * [`JDEX_EMPTY_FOLDER`](#jdex_empty_folder) + * [`JDEX_ENCOUNTERED_FORBIDDEN_FOLDER`](#jdex_encountered_forbidden_folder) + * [`JDEX_ENCOUNTERED_FORBIDDEN_NOTE`](#jdex_encountered_forbidden_note) + * [`JDEX_FILE_WHERE_FOLDER_EXPECTED`](#jdex_file_where_folder_expected) + * [`JDEX_FOLDER_WHERE_NOTE_EXPECTED`](#jdex_folder_where_note_expected) * [Why Doesn't This Check For-](#why-doesnt-this-check-for-) + * [This Doesn't Work with My System Because-](#this-doesnt-work-with-my-system-because-) * [Acknowledgments](#acknowledgments) ## Installation/Requirements Install a fairly recent version of [Python 3](https://www.python.org/downloads/); `jdlint` is tested to work on -Python 3.10 and up. Python 3.9 and earlier is not supported. +Python 3.11 and up. That's it! There are no other dependencies. @@ -60,290 +46,176 @@ That's it! There are no other dependencies. The script itself is executable (you should be able to just run `./jdlint.py`), or you can explicitly point python at it, like `python3 jdlint.py`. -You'll need to pass the script the root folder of your JD-organized filesystem, -e.g. - -```bash -./jdlint.py ~/Documents -``` - -### With a JDex/Index - -If you have your JDex/Index stored as files, you can improve the number of -detected issues by passing that root path along as well, e.g. +You'll need to provide it wit a config file. By default, it looks for +`jdlint.toml` in the working directory; you may specify a different config file +with the `-c` flag, e.g. ```bash -./jdlint.py --jdex ~/"Knowledge/00.00 📇 System Index" ~/Documents +./jdlint.py -c ~/my_jdlint_config.toml ``` -#### JDex Formats - -This supports three possible JDex formats: - -* Single file, as specified [here](https://github.com/johnnydecimal/index-spec). - Note that strict adherence to the spec is not checked, because I personally do - not use this method and am lazy, so don't expect linter errors for your JDex - if you use this format. -* Nested files in folders, e.g. - -```text -. -└── 00-09 System - └── 01 System Stuff - ├── 01.02 A Name.md - └── 01.03 Another ID.md -``` - -* Flat files, e.g. - -```tree -. -├── 00.00 System Area Management.md -├── 01.00 System Stuff Category Management.md -├── 01.02 A Name.md -└── 01.03 Another ID.md -``` - -Note that in the flat case, `N0.00` is taken as the JDex entry for area `N0-N9`, -and `AC.00` is taken as the JDex entry for category `AC`. - -In all cases, any `.md` file extension will be stripped, should one exist. +Everything the script needs is specified in the config file. -A trailing `area management` or `category management` or `index` will also be -stripped, if one exists, to derive the "canonical" name for the area/category. +## Config File -For example: +Several config files are provided in this repository, matching the various +JDex/JD standards. You can of course customize it to your liking. -* `10.00 Life Admin` -* `10.00 Life Admin.md` -* `10.00 Life Admin Area Management` -* `10.00 Life Admin Index` -* `10.00 Life Admin Area Management Index.md` +## Ignoring Files -are all equivalent, and will lead to `jdlint` expecting an area named -`10-19 Life Admin` in your files. +If you wish to ignore files/folders, patterns can be added globally to +`linter.ignore`, to a single root's `.ignore`, or to `system.jdex.ignore`. -#### Alternative Layout for the Standard Zeros - -For a more complete treatment of this topic, see the original post -[here](https://forum.johnnydecimal.com/t/the-standard-zeros/1558/12). - -This moves the expected area management zeros into the system management area. -For example, the management category for area `10-19` will be category `01`, -instead of the typical `10`. This increases the available number of categories -per area and makes greater use of the reserved `00-09` area. - -To specify that you're using this format, pass `jdlint` the `--altzeros` flag. - -This causes two changes in behavior if you are using the flat files JDex -structure: - -* The linter will treat `10.00` as the note for category `10`, and `01.00` as - the note for area `10-19`, etc. -* The area management notes (`0N.00`) will additionally create categories in the - `00-09` area, for those standard zeros. -* The linter will check for optional "header" files. This is not an official - Johnny Decimal thing, just something I use to visually separate notes in the - JDex. They are named e.g. `10. Life Admin` for the `10-19 Life Admin` area. - - ![An image showing how `40. Improvement` nicely shows `20.00 Education` as nested under it.](images/header_note.png) - - If you don't use them, there will be no complaints from the linter; it just - ensures their names stay in sync with the area if they do already exist. - -### Ignoring Files - -You may have files outside of IDs that have to be there; if so, you can ignore -them, e.g. +Additionally, you can ignore them just for one run with: ```bash -./jdlint.py ~/Documents --ignore .st* +./jdlint.py --ignore .st* ``` +This option may be specified more than once. + This option supports some basic glob-style patterns. (It uses [`PurePath.match()`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.match).) -### Disabling Specific Rules +## Disabling Specific Rules -Maybe you disagree with a specific issue detected by this linter or have reason -to break the rules. If so, you can disable a rule, e.g. +If you wish to disable a specific rule, it can be added globally to +`linter.disable_rules`. -```bash -./jdlint.py ~/Documents --disable NONEMPTY_INBOX -``` - -### I Am a Robot and Want Something Machine-Readable - -Ask nicely for JSON output instead! +Additionally, you can ignore them just for one run with: ```bash -./jdlint.py ~/Documents --json +./jdlint.py --disable DUPLICATE_ID ``` -## File Errors - -These are errors that can be generated for your files. Some of them require you -passing your JDex via the `--jdex` argument to be detected. +This option may be specified more than once. -### `AREA_DIFFERENT_FROM_JDEX` +## I Am a Robot and Want Something Machine-Readable -An area with a differently-named JDex entry, e.g. +Either set `linter.json_output = true` in the config file, or force JSON output +just once with the flag: -```text -. -├── files -│   └── 00-09 Systme <-- This is a typo, oops! -│   └── 01 System Stuff -│   ├── 01.00 An ID -│   ├── 01.02 A Name -│   └── 01.03 Another ID -└── jdex -   └── 00-09 System -    └── 01 System Stuff -    ├── 01.00 An ID.md -    ├── 01.02 A Name.md -    └── 01.03 Another ID.md +```bash +./jdlint.py --json ``` -### `AREA_NOT_IN_JDEX` +## Errors -An area without a corresponding JDex entry, e.g. +These are errors that can be generated for your files; some of them may require +the JDex to determine. -```text -. -├── files -│   ├── 00-09 System -│   │   └── 01 System Stuff -│   │   ├── 01.02 A Name -│   │   └── 01.03 Another ID -│   └── 10-19 Oops <-- This area has no corresponding entry in the JDex/index -│   └── 11 Cat -│   └── 11.12 ID -└──jdex -    ├── 00.00 System.md -    ├── 01.00 System Stuff.md -    ├── 01.02 A Name.md -    ├── 01.03 Another ID.md -    ├── 11.00 Cat.md -    └── 11.12 ID.md -``` +### `ARBITRARY_CONTENT_WHERE_NOT_ALLOWED` -### `CATEGORY_DIFFERENT_FROM_JDEX` - -A category with a differently-named JDex entry, e.g. +A file or folder was found that didn't match you specified format, e.g. ```text . -├── files -│   └── 00-09 System -│   └── 01 System Stuf <-- This is a typo, oops! -│   ├── 01.00 An ID -│   │   └── Other File -│   ├── 01.02 A Name -│   └── 01.03 Another ID -└── jdex -   └── 00-09 System -    └── 01 System Stuff -    ├── 01.00 An ID.md -    ├── 01.02 A Name.md -    └── 01.03 Another ID.md +├── 10-19 Life +│ ├── 11 Me, Myself, & I +│ │   ├── 11.11 Me +│ │   ├── 11.12 Myself +│ │   └── Not An Id <-- These don't belong here +│ └── 21 You <-- These don't belong here +└── Stuff <-- These don't belong here ``` -### `CATEGORY_IN_WRONG_AREA` +### `DUPLICATE_ID` -A category that, by its number, has been put in the wrong area, e.g. +An ID that has been used multiple times, e.g. ```text . -└── 00-09 System - ├── 01 System Stuff - │   └── 01.00 An ID - └── 11 Whoops <-- This is in the wrong area - └── 11.01 Inbox +├── 00-09 System <-- 00-09 has been used twice! +│ ├── 01 System Stuff <-- 01 has been used twice! +│ │ ├── 01.11 An ID <-- 01.11 has been used twice! +│ │ └── 01.11 A Reuse <-- 01.11 has been used twice! +│ └── 01 A Reuse <-- 01 has been used twice! +│ └── 01.02 Another ID +└── 00-09 A Reuse <-- 00-09 has been used twice! + └── 02 Another Category + └── 02.00 An ID ``` -### `CATEGORY_NOT_IN_JDEX` +### `EMPTY_FOLDER` -A category without a corresponding JDex entry, e.g. +A completely empty folder was found; you shouldn't have folders that clutter +things up if they aren't actually serving a purpose. ```text . -├── files -│   └── 00-09 System -│   └── 01 System Stuff <-- This category has no corresponding entry in the JDex -│   ├── 01.02 An ID -│   └── 01.03 Another ID -└── jdex -    ├── 00.00 System.md -    ├── 01.00 An ID.md -    └── 01.03 Another ID.md +├── 00-09 System +│ ├── 01 System Stuff +│ │ └── 01.11 Empty <-- These are all empty folders (just suppose there's no content it here) +│ └── 02 Empty Category <-- These are all empty folders +└── 10-19 Empty Area <-- These are all empty folders ``` -### `DUPLICATE_AREA` +### `ENCOUNTERED_FORBIDDEN_FOLDER` -An area that has been used multiple times, e.g. +A folder that was specified as forbidden in the config was encountered. (These +can be used to ensure that naming schemes are followed.) ```text . -├── 00-09 An Area <-- 00-09 has been used twice! -│   └── 01 A Category -│   └── 01.00 An ID -└── 00-09 A Reuse <-- 00-09 has been used twice! - └── 02 Another Category - └── 02.00 An ID +└── 10-19 Life + └── 11 Me, Myself, & I +    ├── 11.01 Meh <-- This should be called AC.01 Inbox +    └── 11.11 Me ``` -### `DUPLICATE_CATEGORY` +### `FILE_WHERE_FOLDER_EXPECTED` -A category that has been used multiple times, e.g. +A file was found with the name of something that should have been a folder. ```text . -└── 00-09 System - ├── 01 A Category <-- 01 has been used twice! - │   └── 01.00 An ID - └── 01 A Reuse <-- 01 has been used twice! - └── 01.02 Another ID +└── 10-19 Life + ├── 11 Me, Myself, & I + │   ├── 11.11 Me + │   ├── 11.12 Myself + │   └── 11.13 I + └── 12 You <-- This is a file that looks like a category folder ``` -### `DUPLICATE_ID` +### `FOLDER_SHOULD_BE_EMPTY` -An ID that has been used multiple times, e.g. +A folder that should be empty per the config (e.g. inboxes and headers) wasn't. ```text . -└── 00-09 System - └── 01 System Stuff - ├── 01.11 An ID <-- 01.11 has been used twice! - └── 01.11 A Reuse <-- 01.11 has been used twice! +└── 10-19 Life + └── 11 Me, Myself, & I +    ├── 11.01 Inbox <-- These shouldn't have files in them + │   └── Some file +    ├── 11.10 ■ The Me's <-- These shouldn't have files in them + │   └── Some file +    └── 11.11 Me ``` -### `FILE_OUTSIDE_ID` +### `ID_NOT_IN_JDEX` -A file that is located somewhere higher up than in an ID folder, e.g. +An ID without a corresponding JDex entry, e.g. ```text . -├── 00-09 System -│   ├── 01 System Stuff -│   │   ├── 01.00 An ID -│   │   └── File Outside Id <-- -│   └── File Outside Category <-- All of these should only be in IDs -└── File Outside Area <-- +├── files +│   └── 00-09 System <-- This Area has no corresponding entry in the JDex +│   └── 01 System Stuff <-- This Category has no corresponding entry in the JDex +│   ├── 01.02 Missing ID <-- This ID has no corresponding entry in the JDex +│   └── 01.03 Another ID +└── jdex +    └── 01.03 Another ID.md ``` -Use the `--ignore FILE_NAME` option if you have files that *have* to be there, -e.g. `.stignore` is you use Syncthing. - ### `ID_DIFFERENT_FROM_JDEX` An ID with a differently-named JDex entry, e.g. ```text ├── files -│   └── 00-09 System -│   └── 01 System Stuff -│   ├── 01.02 A Naem <-- This is a typo, oops! +│   └── 00-09 Systm <-- This is a typo, oops! +│   └── 01 System Stuf <-- This is a typo, oops! +│   ├── 01.02 A Naem <-- This is a typo, oops! │   ├── 01.03 Another ID │   └── 01.04 An ID └── jdex @@ -354,262 +226,114 @@ An ID with a differently-named JDex entry, e.g.    └── 01.04 An ID.md ``` -### `ID_IN_WRONG_CATEGORY` +## JDex Errors -An ID that, by its number, has been put in the wrong category, e.g. +These are errors that are only generated about the state of your JDex, not your +files. -```text -. -└── 00-09 System - └── 01 System Stuff - ├── 01.00 An ID - └── 11.01 Whoops <-- This is in the wrong category -``` + | JDexIssueEncounteredForbiddenFolder + | JDexIssueEncounteredForbiddenNote -### `ID_NOT_IN_JDEX` +### `JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED` -An ID without a corresponding JDex entry, e.g. +A file or folder was found that didn't match you specified format, e.g. ```text . -├── files -│   └── 00-09 System -│   └── 01 System Stuff -│   ├── 01.00 An ID -│   ├── 01.02 Missing ID <-- This ID has no corresponding entry in the JDex/index -│   └── 01.03 Another ID -└── jdex -    ├── 01.00 An ID.md -    └── 01.03 Another ID.md +├── 10-19 Life +│ ├── 11 Me, Myself, & I +│ │   ├── 11.11 Me.md +│ │   ├── 11.12 Myself.md +│ │   └── Not An Id.md <-- These don't belong here +│ └── 21 You <-- These don't belong here +└── Stuff <-- These don't belong here ``` -### `INVALID_AREA_NAME` +### `JDEX_DUPLICATE_ID` -A directory was found at the area level that doesn't begin with `00-09` or the -like, e.g. +An ID that has been used multiple times, e.g. ```text . -├── 00-09 System -│   └── 01 System Stuff -│   └── 01.00 An ID -├── 10-18 Malformed <-- This has numbers that were typo'd. -└── No ID <-- This doesn't have numbers at all +├── 01.11 An ID.md <-- 01.11 has been used twice! +└── 01.11 A Reuse.md <-- 01.11 has been used twice! ``` -### `INVALID_CATEGORY_NAME` +### `JDEX_EMPTY_FOLDER` -A directory was found at the category level that doesn't begin with `11` or the -like, e.g. +A completely empty folder was found; you shouldn't have folders that clutter +things up if they aren't actually serving a purpose. ```text . -└── 00-09 System - ├── 01 System Stuff - │   └── 01.00 An ID - ├── 2 Malformed <-- This has numbers that were typo'd. - └── No ID <-- This doesn't have numbers at all +├── 00-09 System +│ ├── 01 System Stuff +│ │ └── 01.11 Me.md +│ └── 02 Empty Category <-- These are all empty folders +└── 10-19 Empty Area <-- These are all empty folders ``` -### `INVALID_ID_NAME` +### `JDEX_ENCOUNTERED_FORBIDDEN_FOLDER` -A directory was found at the ID level that doesn't begin with `11.12` or the -like, e.g. +A folder that was specified as forbidden in the config was encountered. (These +can be used to ensure that naming schemes are followed.) ```text . -└── 00-09 System - └── 01 System Stuff - ├── 01.00 An ID - ├── 01.1 Malformed <-- This has numbers that were typo'd. - └── No ID <-- This doesn't have numbers at all +└── 10-19 Life + └── 11 Me, Myself, & I +    └── 11.00 Meh <-- This should be called AC.00 Index +    └── 11.11 Me.md ``` -### `NONEMPTY_INBOX` +### `JDEX_ENCOUNTERED_FORBIDDEN_NOTE` -An inbox (AC.01) that contains items, e.g. +A note that was specified as forbidden in the config was encountered. (These can +be used to ensure that naming schemes are followed.) ```text . -└── 01 System Stuff - ├── 01.01 Inbox - │ └── This is something you meant to sort that you never got around to... - └── 01.11 An ID -``` - -## JDex Errors - -These are errors that are only generated with the `--jdex` argument and concern -the state of your JDex, not your organized files. - -### `JDEX_AREA_HEADER_DIFFERENT_FROM_AREA` - -An (optional) area header with a differently-named JDex entry, e.g. - -```text -jdex -├── 00.00 System Area Management.md -├── 01.00 Life Admin Area Management.md -├── 01.03 Area Standard Zero.md -├── 10. Life Adminn.md <-- This is a typo, oops! -├── 10.00 Me, Myself, and I.md -└── 10.02 An ID.md -``` - -### `JDEX_AREA_HEADER_WITHOUT_AREA` - -An (optional) area header without any corresponding JDex entry, e.g. - -```text -jdex -├── 00.00 System Area Management.md -├── 01.00 Life Admin Area Management.md -├── 01.03 Area Standard Zero.md -├── 10. Life Admin.md -├── 10.00 Me, Myself, and I.md -├── 10.02 An ID.md -└── 20. Digital Stuff.md <- There is no corresponding area -``` - -### `JDEX_CATEGORY_IN_WRONG_AREA` - -A JDex category that, by its number, has been put in the wrong area, e.g. - -```text -jdex -└── 00-09 System - ├── 01 System Stuff - │   └── 01.00 An ID - └── 11 Whoops <-- This is in the wrong area - └── 11.01 Inbox -``` - -### `JDEX_DUPLICATE_AREA` - -A JDex area that has been used multiple times, e.g. - -```text -jdex -├── 00-09 An Area <-- 00-09 has been used twice! -│   └── 01 A Category -│   └── 01.00 An ID -└── 00-09 A Reuse <-- 00-09 has been used twice! - └── 02 Another Category - └── 02.00 An ID -``` - -### `JDEX_DUPLICATE_AREA_HEADER` - -An (optional) area header that has been used multiple times, e.g. - -```text -jdex -├── 00.00 System Area Management.md -├── 01.00 An Area.md -├── 01.03 Area Standard Zero.md -├── 10. An Area.md <-- These are both for 10-19 -├── 10. A Reuse.md <-- These are both for 10-19 -├── 10.00 Me, Myself, and I.md -└── 10.02 An ID.md +└── 10-19 Life + └── 11 Me, Myself, & I +    ├── 11.01 Meh.md <-- This should be called AC.01 Inbox +    └── 11.11 Me.md ``` -### `JDEX_DUPLICATE_CATEGORY` +### `JDEX_FILE_WHERE_FOLDER_EXPECTED` -A JDex category that has been used multiple times, e.g. +A file was found with the name of something that should have been a folder. ```text -jdex -└── 00-09 System - ├── 01 A Category <-- 01 has been used twice! - │   └── 01.00 An ID - └── 01 A Reuse <-- 01 has been used twice! - └── 01.02 Another ID -``` - -### `JDEX_DUPLICATE_ID` - -A JDex ID that has been used multiple times, e.g. - -```text -jdex -└── 00-09 System - └── 01 System Stuff - ├── 01.11 An ID <-- 01.11 has been used twice! - └── 01.11 A Reuse <-- 01.11 has been used twice! -``` - -### `JDEX_FILE_OUTSIDE_CATEGORY` - -A file that is located at the area or category level in a nested JDex, e.g. - -```text -jdex -├── 00-09 System -│   ├── 01 System Stuff -│   │   ├── 01.02 A Name.md -│   │   └── 01.03 Another ID.md -│   └── Nor here <-- Both of these should only be in categories -└── Not here <-- -``` - -### `JDEX_ID_IN_WRONG_CATEGORY` - -A JDex ID that, by its number, has been put in the wrong category in a nested -structure, e.g. - -```text -jdex -└── 00-09 System - └── 01 System Stuff - ├── 01.00 An ID - └── 02.01 Whoops <-- This is in the wrong category -``` - -### `JDEX_INVALID_AREA_NAME` - -A directory was found at the area level in the JDex that doesn't begin with -`00-09` or the like, e.g. - -```text -jdex -├── 00-09 System -│   └── 01 System Stuff -│   └── 01.02 An ID -├── 10-18 Malformed <-- This has numbers that were typo'd. -└── No ID <-- This doesn't have numbers at all -``` - -### `JDEX_INVALID_CATEGORY_NAME` - -A directory was found at the category level in the JDex that doesn't begin with -`11` or the like, e.g. - -```text -jdex -└── 00-09 System - ├── 01 System Stuff - │   └── 01.02 An ID - ├── 2 Malformed <-- This has numbers that were typo'd. - └── No ID <-- This doesn't have numbers at all +. +└── 10-19 Life + ├── 11 Me, Myself, & I + │   ├── 11.11 Me.md + │   ├── 11.12 Myself.md + │   └── 11.13 I.md + └── 12 You <-- This is a file that looks like a category folder ``` -### `JDEX_INVALID_ID_NAME` +### `JDEX_FOLDER_WHERE_NOTE_EXPECTED` -A JDex note was found at the ID level that doesn't begin with `11.12` or the -like, e.g. +A folder was found with the name of something that should have been a file. ```text . -└── 00-09 System - └── 01 System Stuff - ├── 01.00 An ID - ├── 01.1 Malformed <-- This has numbers that were typo'd. - └── No ID <-- This doesn't have numbers at all +└── 10-19 Life + ├── 11 Me, Myself, & I +    ├── 11.11 Me.md +    ├── 11.12 Myself.md +    └── 11.13 I.md <-- This is a file that looks like a category folder +    └── Some file ``` ## Why Doesn't This Check For- -Because I didn't think of it. Open an issue and maybe it will get added! +Because I didn't think of it. Open an issue and maybe it will get added. + +## This Doesn't Work with My System Because- + +Open an issue, and if it's reasonable, we can try to support it. ## Acknowledgments diff --git a/images/header_note.png b/images/header_note.png deleted file mode 100644 index a737e02fefbf4bfd699ddbe6110460f0dc9644d9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3704 zcmai1S5%YT68)%#A_N7L-hv<^9fZ(JkkHF7T|hxVdhcBX1Qex86{3PtHAoFj5E7bz zks?K;1SHhZLJ4s5KitQ=?!%m!wa&wtHEW-}_dGK-zI&OTlO6zo%Wyp%b8<$ILkC1j zKA%4i$C4ABpPo%105E;~Hz;y$F>#R#F}RNAKVe0iMUTT+VVvly%DViLTe3=eU|~9e z0BsP3AR%ttnsE;hN47Is)B?#-eKx}xMtt$o>I@)tWx zu`-&sEq27eXp!{G>(}uiR?(pjGZ@yWCv+><@t=vkNQ^KyJru!HoChJRZn5g=;sRg>1pf|4{SoV z+Vb=b1~RkExvJw4Aj=W{M8*i7gqPPoY1UnZ^<(hV6bx)Q{tIsWmG|3oh2Mv~+nF4g z6HU#s8>>2DjNCsj(1TAyUk>qTCCJJmvF5S>5Wf07FFSiK*lKFL8xbQN-5Ar+vwjfX z-jO9@e$u=D*wEE1ThZZ<^h($jEeyl!rEp#bwHK^K#%jX(T8Xnw>WegHtXu}pz4P<> z2E%``O>U;9fAWiF+b@)M_J=-}J>a3QW36#)>h}p%kq@l=+EsAdwQ$(h!IJPR>2?UJ z$Aclv4VUSB_v)p~FCs&61wS&8{U(Mf&o&1Gqx1u&OMDM)#B(3rVjqc(HVY2E4E&Mw z{jMH$WW7jOE@^KyF}&_pZzIWWKi#&*kcU0kuYSx5%OC2)V0Mr{c8$22GI(W?p>KF4 zDk2A09ZLp>F9C^()JCzp&r&eo*>E{CjWPH97JmFWYJMP;^5DS@tW9D-FlwFc;PpUzMWeuEJtiolHBgDSXU=@GJm3_rlBbrk&FY|inHI{w8sav zQdZ~od6eE9D4nc__F?ePs~zf`)2mCyrFE~Y)!Dn+8!vq@qpct7!aq*+5X}1YX>`15 zqN5=U=HhacCaI2!nOh03yS=Em^-8*vunlJA-e0)Hck%f5A`-cqKVn#S%i3beW(R$z zjuaN$XJIl$S>r2iy?dBSY&}RhpPRb75rW1UsQ2w}GXNmQFHeIRhZT#*1>}~WUUQ2+ z8^2RGavT_L(>r8?B^~I5(9M62o+O2*RaJSicMzugzh$@7Nf^T6g;4J+Kpf)_pabIWu!}2YWu0zzXnWU4A8}MT zvBFd@x`(z8`2rM+K$@A5S=*0E77TUN$HQeIvyuKP$3O+^yb=V9|) zy`0vuq^{8UNEP08hil6GD^)xwRo)Ej!gNF6$djHV05H$QKD@L~1$<-ro0k~k{L99TT2oIshxdoTb*(9R zsQw6^xT(qyH@Ed@ow?7$!$Sjzc35VHsc<+)odlH+yS^_Vi8VoQqe=zgi^sp}2L=X+ z+sYu|hZV9-2YAy=yDYif+JJ> zy=&;r3UztzZHWPKy(TeFe<@v8+wh_znTeMc znDUy4k#4^@CPX$EVJIfzE=)AK8#*PC2n`TRfi^h; zVBux*tckD)r5%3?7i3UFGqqY2IqTu3*Qmh-blty)<9ixY#FU2+RVZwD_l^|+b6@%9 z-^TA$a_u{Ag_%q3i!*w(Xu}?e8(cFs)}K3Cn_!p62ej}ot#%*#jx#g8$Uq%z&C(AS ze-TJDQB!@2_+T>WTN~S-o&V}==fi?{Rg*0h?fe}nSs^aKsewjEH)-DbS@+&9L1;th z?SVF5*Xo{UNK{$#+{pqn1yX+d$3A{sND-3q;58vby&y02_c~$Xg4MaGC>tuUAe}p2 zI9%?(goc#p?Z(!V!P2J!6%!KTa%Kb+b28bL`OC><4=PJZ`EF5el(4g+JsLZ$5;3Hg z!0)B7{)K~%J+0+)vC<1>COVH~y0#Yd9YL8W%-Iv^zN*Q#&5Z2q>@)ARN>vomBwyk2 zuhd?_KDda5g~@v=;Gwf~#3BChD?S512?!)A{i1{yq7F17+96{9oZM8|5cuJ0A!P}Q z1LKgNGd|- z3Ny6|TlO#jU|jl?WWu6*AWRAy|F9KyK4 z%`&Z|A}58>nlG4Yk04q%M1*_3CmNIu*94FS%vI(1+Z-_{05vq_=HNJeU;IM4 zh3au&)ofsAtQc8e@S&5sFj$Ao>hWM+?bPOrasy2eDAUQNSh&kw3eQ5RTdak}ELkXO zKD-PqE~%~EZ7muvGHE89`v3jIj0%adXu1i%tSskSe4^Vlckz2YM7~w`tP$I2+0vz9 zSAp7&aO*uqh_2v@JFWd%OgbIZqhhPq%;k)d6KRpOoB|jbeHYOeTZwaO&~?FH=TcEX zz=ckVlM$NycG_W~RpK7ZtTro`e>4%ELFUZSB?4~zoLS3HZ*-R}%`svj_@Z*-UR9p& zk;`7oEXlYSYFu36XR>O{gxXs#jzr3ky{A9bsk)kf^;aj+wt(QhK5rZ@XOd&C+{i>R z7{AXv&xc)my&e^Y5S4e&@%(Jrq%mWQqpW7nK-;Z0`zsbr0E0#bEYuGa3ayJJ#KD)h z`c%1>E;podix8|Wv)|+@)<_Ig*GGN0UMMP8swYKU*;`}ZMC z$=*Q1=1hp%-fUKUeB+1OS6ElWbM)y&!6`n0jQW0y8wq{SeeRs>kO*8g0h@4g3+@HA z>L0~YwZ?uL8IWyI0)rKQEuL@T8D6xc)oFU)G4Nq+$FxD5?Gxl=PhK80Ep5Pe$Zz=r z%eq1}q4UO#bSi)`CGGfpbj)*7e^!DxCiKi`zt)cE4l`roA!)Mq2DThKdddQ~eymVY&bQlmfn>)X98@0Bn5Xy=898M<1n>MOd1Ezr|5VpO+5ec8{B< zD5S-!_!LMnS^XdYGlBwu>*u3XpfU+FmO#fy_y~TOJxg#9i2L)h^ee%i`edzi&OOMp zJ2^D+-~T7Fu3+>=`go3L^-oKc+RUf%1wTrFQ#_a_f|EM`5oAv#gM&5&0TO_K7Y_V* zGp;V_CSx#WLb@+F&6-;F>tfX-AkvDEqdJ3&p};83ufnnRnaj5eI}Kl}NIcj*8cOHB zz=8Y1hW{nmLOCDOcIE6nzMA`vmbNj=I|Qo{J{lXN5gT=aOrcU+F=$ z&z99PWSa8wd^6$h_C2Z-xH+$ZWrbg%2L#!}dG}_AhjuwHMtCCfUcb-L$f9H^eYL!| zTa=Lu7Nc*zdZWuxFtOjMeNIU(t{UKd^6=6CN+0S81~jll%Vup6@8=UZ<5;ctV`Bez e0V6Z$j(XolC8QOs_lmqq0&tkIPOX+x!hZnfpD9}a diff --git a/jdlint.py b/jdlint.py index 701ff38..5b9f850 100755 --- a/jdlint.py +++ b/jdlint.py @@ -1167,15 +1167,12 @@ def _sort_error(e: Issue) -> tuple[str, tuple[tuple[str, ...], str]]: def _entry_is_ignored( ignored: tuple[str] | None, - nested_under: list[str], f: os.DirEntry, ) -> bool: """Check if a given file/directory should be ignored.""" - # TODO this is now wrong with the nested under shit and needs fixing if not ignored: return False - p = PurePath(*nested_under, f.name) - return any(p.match(pattern) for pattern in ignored) + return any(PurePath(f).match(pattern) for pattern in ignored) E = TypeVar("E") @@ -1255,7 +1252,7 @@ def _get_jdex_notes_here_or_children( has_content = False with os.scandir(path) as contents: for x in contents: - if _entry_is_ignored(ignored, [], x): + if _entry_is_ignored(ignored, x): continue has_content = True for child_format, child in valid_children: @@ -1401,7 +1398,7 @@ def _process_system_level_and_children( with os.scandir(path) as contents: for x in contents: - if _entry_is_ignored(ignored, [], x): + if _entry_is_ignored(ignored, x): continue has_content = True for child_format, child in valid_children: @@ -1596,15 +1593,11 @@ def lint_system(config: Config) -> LintResults: description="Ensure that your Johnny Decimal system is neat and clean", ) parser.add_argument( - "path", - metavar="ROOT_PATH", - help='The root of a JD file structure; should contain folders called e.g. "10-19 Life Admin"', - ) - parser.add_argument( - "--jdex", - "--index", - metavar="JDEX_FILES", - help="Folder containing your JDex/index notes", + "-c", + "--config", + metavar="CONFIG_FILE_PATH", + default="./jdlint.toml", + help="Path to jdlint config file (default ./jdlint.toml)", ) parser.add_argument( "-i", @@ -1622,7 +1615,7 @@ def lint_system(config: Config) -> LintResults: action="append", metavar="RULE_TO_DISABLE", default=[], - help="A rule to disable by name, e.g. NONEMPTY_INBOX", + help="A rule to disable by name, e.g. DUPLICATE_ID", ) parser.add_argument( "-j", @@ -1630,98 +1623,94 @@ def lint_system(config: Config) -> LintResults: dest="json", action="store_const", const=True, - help="Output as machine-readable JSON", - ) - parser.add_argument( - "--altzeros", - dest="altzeros", - action="store_const", - const=True, - help="Specify use of the alternative standard zeros layout; see the README for more info", + help="Override config file to output machine-readable JSON", ) args = parser.parse_args() - # Get all errors - if args.jdex: - (errors, jdex_errors) = lint_dir_and_jdex( - path=Path(args.path), - jdex_path=Path(args.jdex), - ignored=args.ignored, - alt_zeros=args.altzeros, - ) - else: - errors = (lint_dir(args.path, args.ignored)).errors - jdex_errors = [] + with Path.open(args.config, "rb") as config_file: + config = Config(tomllib.load(config_file)) - # Filter disabled errors - errors = [e for e in errors if e.type() not in args.disable] - jdex_errors = [e for e in jdex_errors if e.type() not in args.disable] + if args.json: + config.linter.json_output = True + config.linter.ignore.extend(args.ignored) + for r in args.disable: + if r in [e.type for e in typing.get_args(AnyIssueType)]: + continue + raise ConfigValueError("--disable", "not a valid rule name", r) - # If there were issues - if errors or jdex_errors: - # Dump to JSON if asked - if args.json: - json.dump( - {"errors": errors, "jdex_errors": jdex_errors}, - sys.stdout, - cls=_EnhancedJSONEncoder, - ) + config.linter.disable_rules.extend(args.disable) - # Or print them out - else: - # Group errors by type and then by type details - # Since all explanations are identical, there's no reason to print them multiple times - jdex_errs_by_type: dict[str, dict[JDexErrorType, list[JDexError]]] = {} - for je in jdex_errors: - if je.error.type not in jdex_errs_by_type: - jdex_errs_by_type.update({je.error.type: {}}) - _insert_append(je.error, je, jdex_errs_by_type[je.error.type]) - - errs_by_type: dict[str, dict[ErrorType, list[Error]]] = {} - for e in errors: - if e.error.type not in errs_by_type: - errs_by_type.update({e.error.type: {}}) - _insert_append(e.error, e, errs_by_type[e.error.type]) + # We have a valid config; now run the linter + results = lint_system(config) + + # Dump to JSON if asked + if config.linter.json_output: + json.dump( + results, + sys.stdout, + cls=_EnhancedJSONEncoder, + ) + + any_errors = False + # If there were issues + if not config.linter.json_output: + if results.jdex: + jdex_errs_by_type: dict[JDexIssueType, list[JDexIssue]] = {} + for je in results.jdex.errors if results.jdex else []: + _insert_append(je.type, je, jdex_errs_by_type) # Print JDex errors if any - if jdex_errors: - print("JDex errors found:") - for je_type in jdex_errs_by_type.values(): - first_j_err = next(iter(je_type.keys())) # Just get the first error + if jdex_errs_by_type: + any_errors = True + print(f"{'':=^80}\n{'JDex Errors Found':^80}\n{'':=^80}") + for errs in jdex_errs_by_type.values(): + first_j_err = next(iter(errs)) # Just get the first error explanation = first_j_err.explain() print(f"\n{explanation.explanation} ({first_j_err.type})") print( - "\n".join( - [ - " " + e.display() - for jes in je_type.values() - for e in jes - ], + textwrap.indent( + "\n".join( + [e.display(results.jdex.path) for e in errs], + ), + " ", ), ) print(explanation.fix) print("\n") - # Print file errors if any - if errors: - print("Errors found:") - for e_type in errs_by_type.values(): - first_err = next(iter(e_type.keys())) # Just get the first error - explanation = first_err.explain() - print(f"\n{explanation.explanation} ({first_err.type})") - print( - "\n".join( - [" " + e.display() for es in e_type.values() for e in es], - ), - ) - print(explanation.fix) - print("\n") + # Print file errors if any + if any(r.errors for r in results.roots.values()): + any_errors = True + for location, root in results.roots.items(): + errs_by_type: dict[IssueType, list[Issue]] = {} + if root.errors: + errs_by_type = {} + for e in root.errors: + _insert_append(e.type, e, errs_by_type) + print(f"{'':=^80}\n{location + ' Errors Found:':^80}\n{'':=^80}") + for errs in errs_by_type.values(): + first_err = next(iter(errs)) # Just get the first error + explanation = first_err.explain() + print(f"\n{first_err.type:^80}\n{explanation.explanation}\n---") + print( + textwrap.indent( + "\n".join( + [e.display(root.path) for e in errs], + ), + " ", + ) + ) + print(f"---\n{explanation.fix}\n") + if results.ignored_errs: + print( + f"{'':=^80}\n{'Ignored Errors: ' + str(results.ignored_errs):^80}\n{'':=^80}" + ) + if any_errors: # Exit unhappily sys.exit(1) - # If we're here, there were no issues - print("Everything looks good!") - + if not config.linter.json_output: + print("Everything looks good!") sys.exit(0) From 222301a9f6ed08bbbb86d97b28ea95e887f6c525 Mon Sep 17 00:00:00 2001 From: SiriusStarr <2049163+SiriusStarr@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:48:38 -0700 Subject: [PATCH 16/23] Add example configs and update JDex structure --- README.md | 17 +- configs/README.md | 147 ++++++++ configs/flat_jdex.toml | 191 +++++++++++ configs/fully_nested_jdex.toml | 215 ++++++++++++ configs/no_jdex.toml | 125 +++++++ configs/partially_nested_jdex.toml | 203 +++++++++++ configs/siriusstarr.toml | 319 ++++++++++++++++++ jdlint.py | 199 +++++++---- .../ENCOUNTERED_FORBIDDEN_FOLDER/jdlint.toml | 3 - tests/FOLDER_SHOULD_BE_EMPTY/jdlint.toml | 8 - tests/ID_DIFFERENT_FROM_JDEX/jdlint.toml | 32 +- tests/ID_DIFFERENT_FROM_JDEX/result.json | 9 +- tests/ID_NOT_IN_JDEX/jdlint.toml | 32 +- .../jdlint.toml | 16 +- tests/JDEX_DUPLICATE_ID/jdlint.toml | 13 +- tests/JDEX_EMPTY_FOLDER/jdlint.toml | 6 +- .../jdlint.toml | 39 ++- .../jdlint.toml | 38 ++- .../jdlint.toml | 11 +- .../jdlint.toml | 11 +- .../10-19 Life Admin/10 No JDex/.placeholder | 0 .../11 Me, Myself, & I/11.11 Me/Content | 0 .../11 Me, Myself, & I/11.12 Myslef/Content | 0 .../10.00 Life Admin.md | 0 .../11.00 Me, Myself, & I.md | 0 .../11 Me, Myself, & I/11.11 Me.md | 0 .../11 Me, Myself, & I/11.12 Myself.md | 0 tests/jdex_entry/jdlint.toml | 81 +++++ tests/jdex_entry/result.json | 4 + 29 files changed, 1560 insertions(+), 159 deletions(-) create mode 100644 configs/README.md create mode 100644 configs/flat_jdex.toml create mode 100644 configs/fully_nested_jdex.toml create mode 100644 configs/no_jdex.toml create mode 100644 configs/partially_nested_jdex.toml create mode 100644 configs/siriusstarr.toml create mode 100644 tests/jdex_entry/files/10-19 Life Admin/10 No JDex/.placeholder create mode 100644 tests/jdex_entry/files/10-19 Life Admin/11 Me, Myself, & I/11.11 Me/Content create mode 100644 tests/jdex_entry/files/10-19 Life Admin/11 Me, Myself, & I/11.12 Myslef/Content create mode 100644 tests/jdex_entry/jdex/10-19 Life Admin/10 Management of area 10-19/10.00 Life Admin.md create mode 100644 tests/jdex_entry/jdex/10-19 Life Admin/11 Me, Myself, & I/11.00 Me, Myself, & I.md create mode 100644 tests/jdex_entry/jdex/10-19 Life Admin/11 Me, Myself, & I/11.11 Me.md create mode 100644 tests/jdex_entry/jdex/10-19 Life Admin/11 Me, Myself, & I/11.12 Myself.md create mode 100644 tests/jdex_entry/jdlint.toml create mode 100644 tests/jdex_entry/result.json diff --git a/README.md b/README.md index 5e02d57..6aebb8d 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ clean. * [`JDEX_ENCOUNTERED_FORBIDDEN_NOTE`](#jdex_encountered_forbidden_note) * [`JDEX_FILE_WHERE_FOLDER_EXPECTED`](#jdex_file_where_folder_expected) * [`JDEX_FOLDER_WHERE_NOTE_EXPECTED`](#jdex_folder_where_note_expected) + * [Does This Modify My Files?](#does-this-modify-my-files) * [Why Doesn't This Check For-](#why-doesnt-this-check-for-) * [This Doesn't Work with My System Because-](#this-doesnt-work-with-my-system-because-) * [Acknowledgments](#acknowledgments) @@ -61,6 +62,11 @@ Everything the script needs is specified in the config file. Several config files are provided in this repository, matching the various JDex/JD standards. You can of course customize it to your liking. +It's recommended you read the [Config README](./configs/README.md) for +information on the format, and check out the provided example configs in that +folder, which are (hopefully) well-documented. If you have questions/need help, +please feel free to poke me on the JD Discord. + ## Ignoring Files If you wish to ignore files/folders, patterns can be added globally to @@ -99,6 +105,10 @@ just once with the flag: ./jdlint.py --json ``` +Note that these JSON results additionally contain complete information about the +structure of your system that jdlint had to scan, if that information if of use +to you. + ## Errors These are errors that can be generated for your files; some of them may require @@ -327,9 +337,14 @@ A folder was found with the name of something that should have been a file.    └── Some file ``` +## Does This Modify My Files? + +No. jdlint makes no changes to your files; you have to fix the problems it finds +yourself. We also think that's a good thing. + ## Why Doesn't This Check For- -Because I didn't think of it. Open an issue and maybe it will get added. +Because we didn't think of it. Open an issue and maybe it will get added. ## This Doesn't Work with My System Because- diff --git a/configs/README.md b/configs/README.md new file mode 100644 index 0000000..80ed350 --- /dev/null +++ b/configs/README.md @@ -0,0 +1,147 @@ +# jdlint.toml + +* [jdlint.toml](#jdlinttoml) + * [Introduction](#introduction) + * [LAS/SBS](#lassbs) + * ["SiriusStarr"](#siriusstarr) + * [Formats](#formats) + * [Literal Segments](#literal-segments) + * [Variable Segments](#variable-segments) + * [Numeric Segments](#numeric-segments) + * [Wildcard Segments](#wildcard-segments) + * [Bound Segments](#bound-segments) + * [Segment Identifiers](#segment-identifiers) + +## Introduction + +This folder has some example configs for jdlint. They are (hopefully) +well-documented with comments and demonstrate the full range of capabilities. + +You're strongly encouraged to read through the entirety of one of the configs + +## LAS/SBS + +[Partially-nested](./partially_nested_jdex.toml) should work mostly +out-of-the-box with the Life Admin System or Small Business System. You'll need +to set your paths, and there are one or two features that are turned on to +demonstrate the existence of certain options. + +[Fully-nested](./fully_nested_jdex.toml) and [flat](./flat_jdex.toml) JDex's no +longer have "canon" downloads available for them, so they will likely need more +tweaking to make work. + +[No JDex](./no_jdex.toml) is exactly what it says on the label; this is +generally an inferior mode to run jdlint in, since it many checks are +impossible, but it may be necessary depending on how you store your JDex. + +## "SiriusStarr" + +[SiriusStarr](./SiriusStarr.toml) is an example config for a very non-standard +system that showcases the ability of jdlint to adapt to flexible system +structures. Probably don't look at it if you're running a very standard JD +system, but if you're trying to fit an odd system of your own, it might be +helpful to look at. + +## Formats + +This section provides an overview of the simple markup used for defining the +format of folders and notes by jdlint. A format can consist of three different +segments: literal segments, variable segments, and bound segments. + +### Literal Segments + +Literal segments match exactly what you type. For example, the format +`format = "My Note.md"` will match a file or folder called exactly `My Note.md` +and nothing else. + +### Variable Segments + +Variable segments, on the other hand, can match a range of values. They are +delimited with `/`, since the slash character cannot appear in filenames +anyways. There are two different kinds, numeric segments and wildcard segments. + +#### Numeric Segments + +Numeric Segments match an exact number of digits and are denoted by one or more +`#` symbols, followed by the name to bind the match to. + +For example, `format = "/##AC/./#Tens/0"` will match all of the following: + +* `00.10` +* `19.70` +* `99.20` + +It will *not* match the following: + +* `0.10` -- (Too few leading digits; matches exactly 2) +* `999.10` -- (Too many leading digits; matches exactly 2) +* `19.710` -- (Too many digits after the decimal; matches exactly 1 followed by + a 0) + +#### Wildcard Segments + +Wildcard segments match 1 or more of *any* character and are denoted by the `*` +symbol, followed by the name to bind the match to. + +For example, `format = "12.35 /*Name/.md"` will match all of the following: + +* `12.35` A.md +* `12.35` This is a long title woooooo.md +* `12.35` This title with 1946 digits and 🐦️ emoji.md + +It will *not* match the following: + +* `12.35A.md` -- (Missing the literal space before the variable segment) +* `12.35 A.m` -- (Missing the `d` on the end of the literal `.md`) +* `12.35 .md` -- (Wildcard segments must match at least 1 character, not + nothing) + +### Bound Segments + +Bound segments refer to variable segments that were defined earlier in the +format **or in an ancestor format** (e.g. a parent folder). They match exactly +the content that matched the variable segment the first time. They are delimited +with `/` and denoted by `=` followed by the alphabetic name of the variable +segment they reference. + +For example, you can define an area folder with +`format = "/#A/0-/=A/9 /*Name/"`, which will match all of the following: + +* `00-09 System` +* `90-99 Archive Stuff` +* `10-19 Life Admin` + +It will *not* match the following: + +* `00-19 System` -- (`A` is bound to be `0` by the first match, which means the + `1` does not then match it) + +Alternately, assuming we are defining a child folder of the above area format, +we could define a category folder with: `format = "/=A//#C/ /*CategoryName/"` +Note that this refers to the variable segment `A`, which isn't defined in this +format but rather in its ancestor. This would then match: + +```text +. +├── 00-09 System +│ └── 01 System Stuff +└── 10-19 Life + └── 11 Me, Myself, & I +``` + +It will *not* match: + +```text +. +├── 00-09 System +│ └── 11 Me, Myself, & I <--- "A" is bound to 0 +└── 10-19 Life + └── 01 System Stuff <--- "A" is bound to 1 +``` + +### Segment Identifiers + +The names used to define variable segments and refer to them in bound segments +must consist only of letters. They may not be reused, since it would be +ambiguous what a bound segment referred to then (this will throw an error if you +try). diff --git a/configs/flat_jdex.toml b/configs/flat_jdex.toml new file mode 100644 index 0000000..e317c17 --- /dev/null +++ b/configs/flat_jdex.toml @@ -0,0 +1,191 @@ +## ############################################################################# +# The linter section handles global settings +## ############################################################################# +[linter] +# Globally disable one or more rules by name, e.g. with `disable_rules = ["ID_DIFFERENT_FROM_JDEX"]` +disable_rules = [] +# Output machine-readable JSON instead of human-readable terminal output +json_output = false +# File patterns to ignore anywhere they appear; these support rudimentary globbing +ignore = [".stignore", ".stfolder", ".git*"] + +## ############################################################################# +# The system section handles defining your system format +## ############################################################################# +[system] +## ############################################################################# +# The system.jdex section defines how your JDex is stored on the disk +## ############################################################################# +[system.jdex] +# The path to begin JDex structure within; if you have a flat structure, this +# can just be whatever folder it's in. +path = "~/Obsidian/00-09 System/00 System Management/00.00 System Index" +# File patterns to ignore just in the JDex +ignore = ["Attachments"] + +# `notes` specifies files on the disk (generally they are going to be Markdown +# notes, but they don't have to be) that make up the JDex. +# +# Note that notes are always matched sequentially as they are defined in the +# config file, meaning you can define a more narrow case first and then a more +# broad case later, as is done with Area notes before Category notes here (as +# Area notes would match the Category format). +[[system.jdex.notes]] +# The name to refer to the note type as in errors/JSON output +name = "JDex Area Note" +# The expected format of the note +format = "/#A/0.00 /*Name/.md" + +# `ids` specifies what IDs a note creates, and what their full "name" is. Note +# that a note *can* allow more than one, e.g. 10.00 allows Area 10-19, Category +# 10, and ID 10.00 to exist. Note that you can use bound segments in this. +[[system.jdex.notes.ids]] +id = "/=A/0-/=A/9" +entry = "/=A/0-/=A/9 /=Name/" + +[[system.jdex.notes.ids]] +id = "/=A/0" +entry = "/=A/0 management for /=Name/" + +[[system.jdex.notes.ids]] +id = "/=A/0.00" +entry = "/=A/0.00 index for /=Name/" + +[[system.jdex.notes]] +name = "JDex Category Note" +format = "/#A//#C/.00 /*Name/.md" + +[[system.jdex.notes.ids]] +id = "/=A//=C/" +entry = "/=A//=C/ /=Name/" + +[[system.jdex.notes.ids]] +id = "/=A//=C/.00" +entry = "/=A//=C/.00 JDex for /=Name/" + +[[system.jdex.notes]] +name = "JDex Id Note" +format = "/#A//#C/./##ID/ /*Name/.md" + +[[system.jdex.notes.ids]] +id = "/=A//=C/./=ID/" +entry = "/=A//=C/./=ID/ /=Name/" + +[[system.jdex.notes]] +name = "JDex Work Package Id Note" +format = "W/####WPID/~/##AC/./##ID/ /*WPName/.md" + +[[system.jdex.notes.ids]] +id = "W/=WPID/" +entry = "W/=WPID/~/=AC/./=ID/ /=WPName/" + +## ############################################################################# +# system.default defines the *default* structure used by any roots to check +## ############################################################################# +[system.default] +# At any level, we can define ".children" to specify one or more formats of +# folders to be allowed within the current level; here, we are defining the +# topmost level. +# +# Note that children are always matched sequentially as they are defined in the +# config file, meaning you can define a more narrow case first and then a more +# broad case later. +[[system.default.children]] +# The name to refer to the folder type as in errors/JSON output +name = "Area" +# The expected format of the folder +format = "/#A/0-/=A/9 /*Area/" +# The ID of this entry; this will be checked for uniqueness and matched to the +# JDex. Note that you can use bound segments in this. +id = "/=A/0-/=A/9" + +# Here we are defining the child folders of the top-level area folders +[[system.default.children.children]] +name = "Category" +format = "/=A//#C/ /*Category/" # Note that we're using bound segments from the parent area +id = "/=A//=C/" + +# Here, we define a folder without any children; that tells jdlint that it +# should be empty. This is useful for Headers and Inboxes. +[[system.default.children.children.children]] +name = "Header" +format = "/=A//=C/./#I/0 ■ /*Header/" +id = "/=A//=C/./=I/0" + +[[system.default.children.children.children]] +name = "Inbox" +format = "00.01 Inbox for the /*System/ System 📥" +id = "00.01" + +[[system.default.children.children.children]] +name = "Inbox" +format = "/=A/0.01 Inbox for area /=A/0-/=A/9 📥" +id = "/=A/0.01" + +[[system.default.children.children.children]] +name = "Inbox" +format = "/=A//=C/.01 Inbox for category /=A//=C/" +id = "/=A//=C/.01" + +# Here, we're disallowing inboxes (AC.01) that don't match the exact naming scheme above. +[[system.default.children.children.children]] +name = "Invalid Inbox" +format = "/=A//=C/.01 /*Inbox/" +id = "/=A//=C/.01" +forbidden = true # This just causes jdlint to fail anything that matches the format + +# Here, we're defining our terminal ID folders +[[system.default.children.children.children]] +name = "ID" +format = "/=A//=C/./##ID/ /*IDName/" +id = "/=A//=C/./=ID/" +can_be_file = true # This allows us to have a file as a terminal folder; you might like this for notes, for example +allow_arbitrary_contents = true # This tells jdlint to tolerate absolutely anything in this folder + +# Defining a top-level folder for work packages +[[system.default.children]] +name = "Work Packages Area" +format = "W0000-W9999 /*Area/" +id = "W0000-W9999" + +# And terminal work package folders +[[system.default.children.children]] +name = "Work Package" +format = "W/####WPID/~/##AC/./##ID/ /*WPName/" +id = "W/=WPID/" +allow_arbitrary_contents = true + +## ############################################################################# +# system.roots define folders that should be organized according to your JD +# system; for example, you might have ~/Documents as one root and ~/Dropbox +# as another. +## ############################################################################# +[[system.roots]] +# The name to refer to the root as in errors/JSON output +name = "Documents" +# The path to the files +path = "~/Documents" +# Any ignore patterns that apply only to that set of files +ignore = [".trash"] + +[[system.roots]] +name = "Dropbox" +path = "~/Dropbox" +ignore = [] + +# You can override the default structure for just *one* location if you want, +# e.g. if you only wanted work packages and nothing else in your dropbox +[[system.roots.children]] +name = "Work Packages Area" +format = "W0000-W9999 /*Area/" +id = "W0000-W9999" +# This allows us to not need a JDex entry for something +no_jdex_entry = true + +[[system.roots.children.children]] +name = "Work Package" +format = "W/####WPID/~/##AC/./##ID/ /*WPName/" +# You can override an expected JDex entry with `jdex_entry` +jdex_entry = "Something wildly arbitrary for /=WPID/; you should change this" +id = "W/=WPID/" +allow_arbitrary_contents = true diff --git a/configs/fully_nested_jdex.toml b/configs/fully_nested_jdex.toml new file mode 100644 index 0000000..819f7b7 --- /dev/null +++ b/configs/fully_nested_jdex.toml @@ -0,0 +1,215 @@ +## ############################################################################# +# The linter section handles global settings +## ############################################################################# +[linter] +# Globally disable one or more rules by name, e.g. with `disable_rules = ["ID_DIFFERENT_FROM_JDEX"]` +disable_rules = [] +# Output machine-readable JSON instead of human-readable terminal output +json_output = false +# File patterns to ignore anywhere they appear; these support rudimentary globbing +ignore = [".stignore", ".stfolder", ".git*"] + +## ############################################################################# +# The system section handles defining your system format +## ############################################################################# +[system] +## ############################################################################# +# The system.jdex section defines how your JDex is stored on the disk +## ############################################################################# +[system.jdex] +# The path to begin JDex structure within. +path = "~/Obsidian" +# File patterns to ignore just in the JDex +ignore = ["Attachments"] + +# `children` specifies folders on the disk that are expected in the JDex. These +# may then define `notes`, which specify files on the disk (generally they are +# going to be Markdown notes, but they don't have to be) that make up the JDex. +[[system.jdex.children]] +# The name to refer to the note type as in errors/JSON output +name = "JDex Area Folder" +# The expected format of the note +format = "/#A/0-/=A/9 /*Area/" + +[[system.jdex.children.children]] +name = "JDex Category Folder" +format = "/=A//#C/ /*Category/" + +[[system.jdex.children.children.children]] +name = "JDex ID Folder" +format = "/=A//=C/./##ID/ /*IDName/" +# Since ID notes are alongside content notes, we allow arbitrary content to exist here +allow_arbitrary_contents = true + +# Note that notes are always matched sequentially as they are defined in the +# config file, meaning you can define a more narrow case first and then a more +# broad case later, as is done with Area notes before Category notes here (as +# Area notes would match the Category format). +[[system.jdex.children.children.children.notes]] +name = "JDex Area Note" +format = "/=A/0.00 /*Name/.md" + +# `ids` specifies what IDs a note creates, and what their full "name" is. Note +# that a note *can* allow more than one, e.g. 10.00 allows Area 10-19, Category +# 10, and ID 10.00 to exist. Note that you can use bound segments in this. +[[system.jdex.children.children.children.notes.ids]] +id = "/=A/0-/=A/9" +entry = "/=A/0-/=A/9 /=Area/" + +[[system.jdex.children.children.children.notes.ids]] +id = "/=A/0" +entry = "/=A/0 /=Category/" + +[[system.jdex.children.children.children.notes.ids]] +id = "/=A/0.00" +entry = "/=A/0.00 /=Name/" + +[[system.jdex.children.children.children.notes]] +name = "JDex Category Note" +format = "/=A//=C/.00 /*Name/.md" + +[[system.jdex.children.children.children.notes.ids]] +id = "/=A//=C/" +entry = "/=A//=C/ /=Category/" + +[[system.jdex.children.children.children.notes.ids]] +id = "/=A//=C/.00" +entry = "/=A//=C/.00 /=Name/" + +[[system.jdex.children.children.children.notes]] +name = "JDex Id Note" +format = "/=A//=C/./=ID/ /*Name/.md" + +[[system.jdex.children.children.children.notes.ids]] +id = "/=A//=C/./=ID/" +entry = "/=A//=C/./=ID/ /=Name/" + +# Defining a top-level folder for work packages +[[system.jdex.children]] +name = "JDex Work Packages Folder" +format = "W0000-W9999 /*Area/" + +[[system.jdex.children.children]] +name = "JDex Work Package Id Folder" +format = "W/####WPID/~/##AC/./##ID/ /*WPName/" +# Since ID notes are alongside content notes, we allow arbitrary content to exist here +allow_arbitrary_contents = true + +[[system.jdex.children.children.notes]] +name = "JDex Work Package Id Note" +format = "W/=WPID/~/=AC/./=ID/ /=WPName/.md" + +[[system.jdex.children.children.notes.ids]] +id = "W/=WPID/" +entry = "W/=WPID/~/=AC/./=ID/ /=WPName/" + +## ############################################################################# +# system.default defines the *default* structure used by any roots to check +## ############################################################################# +[system.default] +# At any level, we can define ".children" to specify one or more formats of +# folders to be allowed within the current level; here, we are defining the +# topmost level. +# +# Note that children are always matched sequentially as they are defined in the +# config file, meaning you can define a more narrow case first and then a more +# broad case later. +[[system.default.children]] +# The name to refer to the folder type as in errors/JSON output +name = "Area" +# The expected format of the folder +format = "/#A/0-/=A/9 /*Area/" +# The ID of this entry; this will be checked for uniqueness and matched to the +# JDex. Note that you can use bound segments in this. +id = "/=A/0-/=A/9" + +# Here we are defining the child folders of the top-level area folders +[[system.default.children.children]] +name = "Category" +format = "/=A//#C/ /*Category/" # Note that we're using bound segments from the parent area +id = "/=A//=C/" + +# Here, we define a folder without any children; that tells jdlint that it +# should be empty. This is useful for Headers and Inboxes. +[[system.default.children.children.children]] +name = "Header" +format = "/=A//=C/./#I/0 ■ /*Header/" +id = "/=A//=C/./=I/0" + +[[system.default.children.children.children]] +name = "Inbox" +format = "00.01 Inbox for the /*System/ System 📥" +id = "00.01" + +[[system.default.children.children.children]] +name = "Inbox" +format = "/=A/0.01 Inbox for area /=A/0-/=A/9 📥" +id = "/=A/0.01" + +[[system.default.children.children.children]] +name = "Inbox" +format = "/=A//=C/.01 Inbox for category /=A//=C/" +id = "/=A//=C/.01" + +# Here, we're disallowing inboxes (AC.01) that don't match the exact naming scheme above. +[[system.default.children.children.children]] +name = "Invalid Inbox" +format = "/=A//=C/.01 /*Inbox/" +id = "/=A//=C/.01" +forbidden = true # This just causes jdlint to fail anything that matches the format + +# Here, we're defining our terminal ID folders +[[system.default.children.children.children]] +name = "ID" +format = "/=A//=C/./##ID/ /*IDName/" +id = "/=A//=C/./=ID/" +can_be_file = true # This allows us to have a file as a terminal folder; you might like this for notes, for example +allow_arbitrary_contents = true # This tells jdlint to tolerate absolutely anything in this folder + +# Defining a top-level folder for work packages +[[system.default.children]] +name = "Work Packages Area" +format = "W0000-W9999 /*Area/" +id = "W0000-W9999" + +# And terminal work package folders +[[system.default.children.children]] +name = "Work Package" +format = "W/####WPID/~/##AC/./##ID/ /*WPName/" +id = "W/=WPID/" +allow_arbitrary_contents = true + +## ############################################################################# +# system.roots define folders that should be organized according to your JD +# system; for example, you might have ~/Documents as one root and ~/Dropbox +# as another. +## ############################################################################# +[[system.roots]] +# The name to refer to the root as in errors/JSON output +name = "Documents" +# The path to the files +path = "~/Documents" +# Any ignore patterns that apply only to that set of files +ignore = [".trash"] + +[[system.roots]] +name = "Dropbox" +path = "~/Dropbox" +ignore = [] + +# You can override the default structure for just *one* location if you want, +# e.g. if you only wanted work packages and nothing else in your dropbox +[[system.roots.children]] +name = "Work Packages Area" +format = "W0000-W9999 /*Area/" +id = "W0000-W9999" +# This allows us to not need a JDex entry for something +no_jdex_entry = true + +[[system.roots.children.children]] +name = "Work Package" +format = "W/####WPID/~/##AC/./##ID/ /*WPName/" +# You can override an expected JDex entry with `jdex_entry` +jdex_entry = "Something wildly arbitrary for /=WPID/; you should change this" +id = "W/=WPID/" +allow_arbitrary_contents = true diff --git a/configs/no_jdex.toml b/configs/no_jdex.toml new file mode 100644 index 0000000..decbf8c --- /dev/null +++ b/configs/no_jdex.toml @@ -0,0 +1,125 @@ +## ############################################################################# +# The linter section handles global settings +## ############################################################################# +[linter] +# Globally disable one or more rules by name, e.g. with `disable_rules = ["DUPLICATE_ID"]` +disable_rules = [] +# Output machine-readable JSON instead of human-readable terminal output +json_output = false +# File patterns to ignore anywhere they appear; these support rudimentary globbing +ignore = [".stignore", ".stfolder", ".git*"] + +## ############################################################################# +# The system section handles defining your system format +## ############################################################################# +[system] +## ############################################################################# +# system.default defines the *default* structure used by any roots to check +## ############################################################################# +[system.default] +# At any level, we can define ".children" to specify one or more formats of +# folders to be allowed within the current level; here, we are defining the +# topmost level. +# +# Note that children are always matched sequentially as they are defined in the +# config file, meaning you can define a more narrow case first and then a more +# broad case later. +[[system.default.children]] +# The name to refer to the folder type as in errors/JSON output +name = "Area" +# The expected format of the folder +format = "/#A/0-/=A/9 /*Area/" +# The ID of this entry; this will be checked for uniqueness and matched to the +# JDex. Note that you can use bound segments in this. +id = "/=A/0-/=A/9" + +# Here we are defining the child folders of the top-level area folders +[[system.default.children.children]] +name = "Category" +format = "/=A//#C/ /*Category/" # Note that we're using bound segments from the parent area +id = "/=A//=C/" + +# Here, we define a folder without any children; that tells jdlint that it +# should be empty. This is useful for Headers and Inboxes. +[[system.default.children.children.children]] +name = "Header" +format = "/=A//=C/./#I/0 ■ /*Header/" +id = "/=A//=C/./=I/0" + +[[system.default.children.children.children]] +name = "Inbox" +format = "00.01 Inbox for the /*System/ System 📥" +id = "00.01" + +[[system.default.children.children.children]] +name = "Inbox" +format = "/=A/0.01 Inbox for area /=A/0-/=A/9 📥" +id = "/=A/0.01" + +[[system.default.children.children.children]] +name = "Inbox" +format = "/=A//=C/.01 Inbox for category /=A//=C/" +id = "/=A//=C/.01" + +# Here, we're disallowing inboxes (AC.01) that don't match the exact naming scheme above. +[[system.default.children.children.children]] +name = "Invalid Inbox" +format = "/=A//=C/.01 /*Inbox/" +id = "/=A//=C/.01" +forbidden = true # This just causes jdlint to fail anything that matches the format + +# Here, we're defining our terminal ID folders +[[system.default.children.children.children]] +name = "ID" +format = "/=A//=C/./##ID/ /*IDName/" +id = "/=A//=C/./=ID/" +can_be_file = true # This allows us to have a file as a terminal folder; you might like this for notes, for example +allow_arbitrary_contents = true # This tells jdlint to tolerate absolutely anything in this folder + +# Defining a top-level folder for work packages +[[system.default.children]] +name = "Work Packages Area" +format = "W0000-W9999 /*Area/" +id = "W0000-W9999" + +# And terminal work package folders +[[system.default.children.children]] +name = "Work Package" +format = "W/####WPID/~/##AC/./##ID/ /*WPName/" +id = "W/=WPID/" +allow_arbitrary_contents = true + +## ############################################################################# +# system.roots define folders that should be organized according to your JD +# system; for example, you might have ~/Documents as one root and ~/Dropbox +# as another. +## ############################################################################# +[[system.roots]] +# The name to refer to the root as in errors/JSON output +name = "Documents" +# The path to the files +path = "~/Documents" +# Any ignore patterns that apply only to that set of files +ignore = [".trash"] + +[[system.roots]] +name = "Dropbox" +path = "~/Dropbox" +ignore = [] + +# You can override the default structure for just *one* location if you want, +# e.g. if you only wanted work packages and nothing else in your dropbox +[[system.roots.children]] +name = "Work Packages Area" +format = "W0000-W9999 /*Area/" +id = "W0000-W9999" +# This allows us to not need a JDex entry for something +no_jdex_entry = true + +[[system.roots.children.children]] +name = "Work Package" +format = "W/####WPID/~/##AC/./##ID/ /*WPName/" +# You can override an expected JDex entry with `jdex_entry` +jdex_entry = "Something wildly arbitrary for /=WPID/; you should change this" +id = "W/=WPID/" +allow_arbitrary_contents = true diff --git a/configs/partially_nested_jdex.toml b/configs/partially_nested_jdex.toml new file mode 100644 index 0000000..2575e24 --- /dev/null +++ b/configs/partially_nested_jdex.toml @@ -0,0 +1,203 @@ +## ############################################################################# +# The linter section handles global settings +## ############################################################################# +[linter] +# Globally disable one or more rules by name, e.g. with `disable_rules = ["ID_DIFFERENT_FROM_JDEX"]` +disable_rules = [] +# Output machine-readable JSON instead of human-readable terminal output +json_output = false +# File patterns to ignore anywhere they appear; these support rudimentary globbing +ignore = [".stignore", ".stfolder", ".git*"] + +## ############################################################################# +# The system section handles defining your system format +## ############################################################################# +[system] +## ############################################################################# +# The system.jdex section defines how your JDex is stored on the disk +## ############################################################################# +[system.jdex] +# The path to begin JDex structure within. +path = "~/Obsidian/00-09 System/00 System Management/00.00 System Index" +# File patterns to ignore just in the JDex +ignore = [".obsidian"] + +# `children` specifies folders on the disk that are expected in the JDex. These +# may then define `notes`, which specify files on the disk (generally they are +# going to be Markdown notes, but they don't have to be) that make up the JDex. +[[system.jdex.children]] +# The name to refer to the note type as in errors/JSON output +name = "JDex Area Folder" +# The expected format of the note +format = "/#A/0-/=A/9 /*Area/" + +[[system.jdex.children.children]] +name = "JDex Category Folder" +format = "/=A//#C/ /*Category/" + +# Note that notes are always matched sequentially as they are defined in the +# config file, meaning you can define a more narrow case first and then a more +# broad case later, as is done with Area notes before Category notes here (as +# Area notes would match the Category format). +[[system.jdex.children.children.notes]] +name = "JDex Area Note" +format = "/=A/0.00 /*Name/.md" + +# `ids` specifies what IDs a note creates, and what their full "name" is. Note +# that a note *can* allow more than one, e.g. 10.00 allows Area 10-19, Category +# 10, and ID 10.00 to exist. Note that you can use bound segments in this. +[[system.jdex.children.children.notes.ids]] +id = "/=A/0-/=A/9" +entry = "/=A/0-/=A/9 /=Area/" + +[[system.jdex.children.children.notes.ids]] +id = "/=A/0" +entry = "/=A/0 /=Category/" + +[[system.jdex.children.children.notes.ids]] +id = "/=A/0.00" +entry = "/=A/0.00 /=Name/" + +[[system.jdex.children.children.notes]] +name = "JDex Category Note" +format = "/=A//=C/.00 /*Name/.md" + +[[system.jdex.children.children.notes.ids]] +id = "/=A//=C/" +entry = "/=A//=C/ /=Category/" + +[[system.jdex.children.children.notes.ids]] +id = "/=A//=C/.00" +entry = "/=A//=C/.00 /=Name/" + +[[system.jdex.children.children.notes]] +name = "JDex Id Note" +format = "/=A//=C/./##ID/ /*Name/.md" + +[[system.jdex.children.children.notes.ids]] +id = "/=A//=C/./=ID/" +entry = "/=A//=C/./=ID/ /=Name/" + +# Defining a top-level folder for work packages +[[system.jdex.children]] +name = "JDex Work Packages Folder" +format = "W0000-W9999 /*Area/" + +[[system.jdex.children.notes]] +name = "JDex Work Package Id Note" +format = "W/####WPID/~/##AC/./##ID/ /*WPName/.md" + +[[system.jdex.children.notes.ids]] +id = "W/=WPID/" +entry = "W/=WPID/~/=AC/./=ID/ /=WPName/" + +## ############################################################################# +# system.default defines the *default* structure used by any roots to check +## ############################################################################# +[system.default] +# At any level, we can define ".children" to specify one or more formats of +# folders to be allowed within the current level; here, we are defining the +# topmost level. +# +# Note that children are always matched sequentially as they are defined in the +# config file, meaning you can define a more narrow case first and then a more +# broad case later. +[[system.default.children]] +# The name to refer to the folder type as in errors/JSON output +name = "Area" +# The expected format of the folder +format = "/#A/0-/=A/9 /*Area/" +# The ID of this entry; this will be checked for uniqueness and matched to the +# JDex. Note that you can use bound segments in this. +id = "/=A/0-/=A/9" + +# Here we are defining the child folders of the top-level area folders +[[system.default.children.children]] +name = "Category" +format = "/=A//#C/ /*Category/" # Note that we're using bound segments from the parent area +id = "/=A//=C/" + +# Here, we define a folder without any children; that tells jdlint that it +# should be empty. This is useful for Headers and Inboxes. +[[system.default.children.children.children]] +name = "Header" +format = "/=A//=C/./#I/0 ■ /*Header/" +id = "/=A//=C/./=I/0" + +[[system.default.children.children.children]] +name = "Inbox" +format = "00.01 Inbox for the /*System/ System 📥" +id = "00.01" + +[[system.default.children.children.children]] +name = "Inbox" +format = "/=A/0.01 Inbox for area /=A/0-/=A/9 📥" +id = "/=A/0.01" + +[[system.default.children.children.children]] +name = "Inbox" +format = "/=A//=C/.01 Inbox for category /=A//=C/" +id = "/=A//=C/.01" + +# Here, we're disallowing inboxes (AC.01) that don't match the exact naming scheme above. +[[system.default.children.children.children]] +name = "Invalid Inbox" +format = "/=A//=C/.01 /*Inbox/" +id = "/=A//=C/.01" +forbidden = true # This just causes jdlint to fail anything that matches the format + +# Here, we're defining our terminal ID folders +[[system.default.children.children.children]] +name = "ID" +format = "/=A//=C/./##ID/ /*IDName/" +id = "/=A//=C/./=ID/" +can_be_file = true # This allows us to have a file as a terminal folder; you might like this for notes, for example +allow_arbitrary_contents = true # This tells jdlint to tolerate absolutely anything in this folder + +# Defining a top-level folder for work packages +[[system.default.children]] +name = "Work Packages Area" +format = "W0000-W9999 /*Area/" +id = "W0000-W9999" + +# And terminal work package folders +[[system.default.children.children]] +name = "Work Package" +format = "W/####WPID/~/##AC/./##ID/ /*WPName/" +id = "W/=WPID/" +allow_arbitrary_contents = true + +## ############################################################################# +# system.roots define folders that should be organized according to your JD +# system; for example, you might have ~/Documents as one root and ~/Dropbox +# as another. +## ############################################################################# +[[system.roots]] +# The name to refer to the root as in errors/JSON output +name = "Documents" +# The path to the files +path = "~/Documents" +# Any ignore patterns that apply only to that set of files +ignore = [".trash"] + +[[system.roots]] +name = "Dropbox" +path = "~/Dropbox" +ignore = [] + +# You can override the default structure for just *one* location if you want, +# e.g. if you only wanted work packages and nothing else in your dropbox +[[system.roots.children]] +name = "Work Packages Area" +format = "W0000-W9999 /*Area/" +id = "W0000-W9999" +# This allows us to not need a JDex entry for something +no_jdex_entry = true + +[[system.roots.children.children]] +name = "Work Package" +format = "W/####WPID/~/##AC/./##ID/ /*WPName/" +# You can override an expected JDex entry with `jdex_entry` +jdex_entry = "Something wildly arbitrary for /=WPID/; you should change this" +id = "W/=WPID/" +allow_arbitrary_contents = true diff --git a/configs/siriusstarr.toml b/configs/siriusstarr.toml new file mode 100644 index 0000000..b2345c5 --- /dev/null +++ b/configs/siriusstarr.toml @@ -0,0 +1,319 @@ +## ############################################################################# +# The linter section handles global settings +## ############################################################################# +[linter] +# Globally disable one or more rules by name, e.g. with `disable_rules = ["ID_DIFFERENT_FROM_JDEX"]` +disable_rules = [] +# Output machine-readable JSON instead of human-readable terminal output +json_output = false +# File patterns to ignore anywhere they appear; these support rudimentary globbing +ignore = [".stignore", ".stfolder", ".git*"] + +## ############################################################################# +# The system section handles defining your system format +## ############################################################################# +[system] +## ############################################################################# +# The system.jdex section defines how your JDex is stored on the disk +## ############################################################################# +[system.jdex] +# The path to begin JDex structure within. +path = "~/Knowledge" +# File patterns to ignore just in the JDex +ignore = [".obsidian", ".trash"] + +# `children` specifies folders on the disk that are expected in the JDex. These +# may then define `notes`, which specify files on the disk (generally they are +# going to be Markdown notes, but they don't have to be) that make up the JDex. +[[system.jdex.children]] +# The name to refer to the note type as in errors/JSON output +name = "JDex Branch Folder" +# The expected format of the note +format = "B/##BB/ /*Branch/" + +[[system.jdex.children.children]] +name = "JDex Branch Index Folder" +format = "C/=BB/.00 📇 Index" + +[[system.jdex.children.children.notes]] +name = "JDex Branch Note" +format = "B/=BB/ /=Branch/.md" + +# `ids` specifies what IDs a note creates, and what their full "name" is. Note +# that a note *can* allow more than one, e.g. 10.00 allows Area 10-19, Category +# 10, and ID 10.00 to exist. Note that you can use bound segments in this. +[[system.jdex.children.children.notes.ids]] +id = "B/=BB/" +entry = "B/=BB/ /=Branch/" + +[[system.jdex.children.children.notes]] +name = "JDex Branch Index Note" +format = "C/=BB/./##ID/ /*Name/.md" + +[[system.jdex.children.children.notes.ids]] +id = "C/=BB/./=ID/" +entry = "C/=BB/./=ID/ /=Name/" + +[[system.jdex.children.children.notes]] +name = "Nest To Cache Note" +format = "N/=BB/./####ID/~C/=BB/./##RelID/ /*Nest/.md" + +[[system.jdex.children.children.notes.ids]] +id = "N/=BB/./=ID/" +entry = "N/=BB/./=ID/~C/=BB/./=RelID/ /=Nest/" + +[[system.jdex.children.children.notes]] +name = "Nest To Nest Note" +format = "N/=BB/./####ID/~N/=BB/./####RelID/ /*Nest/.md" + +[[system.jdex.children.children.notes.ids]] +id = "N/=BB/./=ID/" +entry = "N/=BB/./=ID/~N/=BB/./=RelID/ /=Nest/" + +[[system.jdex.children.children.children]] +name = "Attachments Folder" +format = "Attachments" +allow_arbitrary_contents = true + +[[system.jdex.children.children]] +name = "JDex Cache Non-Index Folder" +format = "C/=BB/./##ID/ /*Cache/" +allow_arbitrary_contents = true + +[[system.jdex.children.children]] +name = "JDex Nest Non-Index Folder" +format = "N/=BB/./####ID/~/*Nest/" +allow_arbitrary_contents = true + +[[system.roots]] +name = "Knowledge" +path = "~/Knowledge" +ignore = [".git", ".gitignore", ".obsidian", ".trash"] + +[[system.roots.children]] +name = "Trunk" +format = "B/#T/0 /*Trunk/ ⬇️" +id = "B/=T/0" + +[[system.roots.children.children]] +name = "Trunk Index" +format = "C/=T/0.00 📇 Index" +id = "C/=T/0.00" +allow_arbitrary_contents = true + +[[system.roots.children]] +name = "Branch" +format = "B/##BB/ /*Branch/" +id = "B/=BB/" + +[[system.roots.children.children]] +name = "Index" +format = "C/=BB/.00 📇 Index" +id = "C/=BB/.00" +allow_arbitrary_contents = true + +[[system.roots.children.children]] +name = "Header" +format = "C/=BB/./#H/0 /*Header/ ⬇️" +id = "C/=BB/./=H/0" + +# Disallow headers that don't match this format +[[system.roots.children.children]] +name = "Invalid Header Or Index" +format = "C/=BB/./#H/0 /*Header/" +id = "C/=BB/./=H/0" +forbidden = true + +[[system.roots.children.children]] +name = "Inbox" +format = "C/=BB/.01 📥️ Inbox" +id = "C/=BB/.01" + +# Disallow inboxes that don't match this format +[[system.roots.children.children]] +name = "Invalid Inbox" +format = "C/=BB/.01 /*Inbox/" +id = "C/=BB/.01" +forbidden = true + +[[system.roots.children.children]] +name = "Cache" +format = "C/=BB/./##ID/ /*Cache/" + +allow_arbitrary_contents = true +id = "C/=BB/./=ID/" + +[[system.roots.children.children]] +name = "Nest" +format = "N/=BB/./####ID/~/*Nest/" +id = "N/=BB/./=ID/" +allow_arbitrary_contents = true + +[[system.roots]] +name = "Caches" +path = "~/Caches" + +[[system.roots]] +name = "Knowledge" +path = "~/Knowledge" +ignore = [".obsidian", ".trash"] + +[[system.roots]] +name = "Nests" +path = "~/Nests" + +[[system.roots.children]] +name = "Nest" +format = "N/##BB/./####ID/~/*Nest/" +id = "N/=BB/./=ID/" +allow_arbitrary_contents = true + +[[system.default.children]] +name = "Trunk" +format = "B/#T/0 /*Trunk/ ⬇️" +id = "B/=T/0" + +[[system.default.children]] +name = "Branch" +format = "B/##BB/ /*Branch/" +id = "B/=BB/" + +## ############################################################################# +# Here, we're defining standard zeros so that they are all named consistently +## ############################################################################# +[[system.default.children.children]] +name = "Index" +format = "C/=BB/.00 📇 Index" +id = "C/=BB/.00" +allow_arbitrary_contents = true + +[[system.default.children.children]] +name = "Invalid Index" +format = "C/=BB/.00/*Index/" +id = "C/=BB/.00" +forbidden = true + +[[system.default.children.children]] +name = "Inbox" +format = "C/=BB/.01 📥️ Inbox" +id = "C/=BB/.01" + +[[system.default.children.children]] +name = "Invalid Inbox" +format = "C/=BB/.01/*Inbox/" +id = "C/=BB/.01" +forbidden = true + +[[system.default.children.children]] +name = "Control" +format = "C/=BB/.02 ✅ Control" +id = "C/=BB/.02" +allow_arbitrary_contents = true + +[[system.default.children.children]] +name = "Invalid Control" +format = "C/=BB/.02/*Control/" +id = "C/=BB/.02" +forbidden = true + +[[system.default.children.children]] +name = "Templates" +format = "C/=BB/.03 📑 Templates & Standards" +id = "C/=BB/.03" +allow_arbitrary_contents = true + +[[system.default.children.children]] +name = "Invalid Templates" +format = "C/=BB/.03/*Templates/" +id = "C/=BB/.03" +forbidden = true + +[[system.default.children.children]] +name = "Links" +format = "C/=BB/.04 🔗 Sources, Resources, & Links" +id = "C/=BB/.04" +allow_arbitrary_contents = true + +[[system.default.children.children]] +name = "Invalid Links" +format = "C/=BB/.04/*Links/" +id = "C/=BB/.04" +forbidden = true + +[[system.default.children.children]] +name = "Ideas" +format = "C/=BB/.05 💭 Inspiration, Ideas, & Percolation" +id = "C/=BB/.05" +allow_arbitrary_contents = true + +[[system.default.children.children]] +name = "Invalid Ideas" +format = "C/=BB/.05/*Ideas/" +id = "C/=BB/.05" +forbidden = true + +[[system.default.children.children]] +name = "Tags" +format = "C/=BB/.06 🏷️ Topics & Tags" +id = "C/=BB/.06" +allow_arbitrary_contents = true + +[[system.default.children.children]] +name = "Invalid Tags" +format = "C/=BB/.06/*Tags/" +id = "C/=BB/.06" +forbidden = true + +[[system.default.children.children]] +name = "Unused .07" +format = "C/=BB/.07/*Name/" +id = "C/=BB/.07" +forbidden = true + +[[system.default.children.children]] +name = "Unused .08" +format = "C/=BB/.08/*Name/" +id = "C/=BB/.08" +forbidden = true + +[[system.default.children.children]] +name = "ADR" +format = "C/=BB/.09 ⚖️ Decision Log" +id = "C/=BB/.09" +allow_arbitrary_contents = true + +[[system.default.children.children]] +name = "Invalid ADR" +format = "C/=BB/.09/*ADR/" +id = "C/=BB/.09" +forbidden = true + +## ############################################################################# +# Ensure headers are empty and properly named +## ############################################################################# +[[system.default.children.children]] +name = "Header" +format = "C/=BB/./#H/0 /*Header/ ⬇️" +id = "C/=BB/./=H/0" + +# Disallow headers that don't match this format +[[system.default.children.children]] +name = "Invalid Header Or Index" +format = "C/=BB/./#H/0 /*Header/" +id = "C/=BB/./=H/0" +forbidden = true + +## ############################################################################# +# Actual Content +## ############################################################################# +[[system.default.children.children]] +name = "Cache" +format = "C/=BB/./##ID/ /*Cache/" +allow_arbitrary_contents = true +id = "C/=BB/./=ID/" + +[[system.default.children.children]] +name = "Nest" +format = "N/=BB/./####ID/~/*Nest/" +id = "N/=BB/./=ID/" +allow_arbitrary_contents = true diff --git a/jdlint.py b/jdlint.py index 5b9f850..7f72f01 100755 --- a/jdlint.py +++ b/jdlint.py @@ -7,10 +7,10 @@ import argparse import dataclasses import json -import textwrap import os import re import sys +import textwrap import typing from dataclasses import dataclass from pathlib import Path, PurePath @@ -88,7 +88,10 @@ class ConfigSystemRoot: """A root (base folder) of a JD system to check for correctness, e.g. ~/Documents.""" def __init__( - self, at: str, default_structure: list[ConfigSystemTier], from_file: dict + self, + at: str, + default_structure: list[ConfigSystemTier], + from_file: dict, ) -> None: """Create a valid configuration given a loaded section of a config file.""" # Acquire and set defaults @@ -248,7 +251,6 @@ class ConfigSystem: def __init__(self, from_file: dict) -> None: """Create a valid configuration given a loaded system section of a config file.""" - default_structure = [ ConfigSystemTier( f"system.default.children[{i}]", @@ -353,6 +355,37 @@ def __init__( self.build = lambda d: "".join([f(d) for f in build]) +class ConfigJDexID: + """Configuration for how a JDex ID is related to a note.""" + + def __init__( + self, + at: str, + ancestors: ConfigFormatAncestorInfo, + from_file: dict, + ) -> None: + """Create a valid note format given a loaded section of a config file.""" + # Compile Format + if "id" not in from_file: + raise (ConfigMissingKeyError(f"{at}.id")) + self.id = ConfigStaticFormat( + f"{at}.id", + ancestors, + from_file.pop("id"), + ) + if "entry" not in from_file: + raise (ConfigMissingKeyError(f"{at}.entry")) + self.entry = ConfigStaticFormat( + f"{at}.entry", + ancestors, + from_file.pop("entry"), + ) + + # Ensure no extra fields + for key in from_file: + raise ConfigExtraKeyError(f"{at}.{key}", tuple(self.__dict__.keys())) + + class ConfigJDexNotes: """Configuration for how JDex notes are formatted.""" @@ -371,16 +404,24 @@ def __init__( ancestors, from_file, ) - if "ids" not in from_file: + if "ids" not in from_file and not self.format.forbidden: raise (ConfigMissingKeyError(f"{at}.ids")) self.ids = [ - ConfigStaticFormat( + ConfigJDexID( f"{at}.ids[{i}]", self.format, v, ) for i, v in enumerate(from_file.pop("ids", [])) ] + if "jdex_entry" in from_file: + self.jdex_entry = ConfigStaticFormat( + f"{at}.jdex_entry", + self.format, + from_file.pop("jdex_entry"), + ) + else: + self.jdex_entry = None # Ensure no extra fields for key in from_file: @@ -453,18 +494,19 @@ def __init__( # Acquire and set defaults self.can_be_file = from_file.pop("can_be_file", False) + self.no_jdex_entry = from_file.pop("no_jdex_entry", False) # Call the folder tier stuff super().__init__(ConfigSystemTier, at, ancestors, from_file) - if "jdex_note" in from_file: - self.jdex_note = ConfigStaticFormat( - f"{at}.jdex_note", + if "jdex_entry" in from_file: + self.jdex_entry = ConfigStaticFormat( + f"{at}.jdex_entry", self.format, - from_file.pop("jdex_note"), + from_file.pop("jdex_entry"), ) else: - self.jdex_note = None + self.jdex_entry = None if "id" not in from_file: raise (ConfigMissingKeyError(f"{at}.id")) @@ -492,6 +534,11 @@ def __init__( at, "If forbidden, can_be_file must be false.", ) + if self.no_jdex_entry and self.jdex_entry: + raise ConfigConflictError( + at, + "Only one of no_jdex_entry and jdex_entry may be set.", + ) # Ensure no extra fields for key in from_file: @@ -834,14 +881,16 @@ class IssueIDDifferentFromJDex(Issue): """An ID with a differently-named JDex entry.""" id: str - expected_jdex_note: str - known_jdex_notes: list[PurePath] + expected_jdex_entry: str + known_jdex_entries: list[JDexEntry] type: Literal["ID_DIFFERENT_FROM_JDEX"] = "ID_DIFFERENT_FROM_JDEX" def display(self, base_path: PurePath) -> str: """Display this particular instance of an error.""" - known = "\n".join((n.name for n in self.known_jdex_notes)) - return f"{self.file.relative_to(base_path)!s}\n ID: {self.id}\n Expected:\n {self.expected_jdex_note}\n Actual:\n{textwrap.indent(known, ' ')}]" + known = "\n".join( + f"{n.entry} [from {n.path.name}]" for n in self.known_jdex_entries + ) + return f"{self.file.relative_to(base_path)!s}\n ID: {self.id}\n Expected:\n {self.expected_jdex_entry}\n Actual:\n{textwrap.indent(known, ' ')}" def explain(self) -> _Explanation: """Explain what this error is.""" @@ -860,9 +909,7 @@ class IssueEncounteredForbiddenFolder(Issue): def display(self, base_path: PurePath) -> str: """Display this particular instance of an error.""" - return ( - f'{self.file.relative_to(base_path)!s} (matched "{self.matched_pattern}")' - ) + return f"{self.file.relative_to(base_path)!s}\n (matched {_print_pattern(self.matched_pattern)})" def explain(self) -> _Explanation: """Explain what this error is.""" @@ -909,9 +956,7 @@ class JDexIssueFileWhereFolderExpected(JDexIssue): def display(self, base_path: PurePath) -> str: """Display this particular instance of an error.""" - return ( - f'{self.file.relative_to(base_path)!s} (matched "{self.matched_pattern}")' - ) + return f"{self.file.relative_to(base_path)!s}\n (matched {_print_pattern(self.matched_pattern)})" def explain(self) -> _Explanation: """Explain what this error is.""" @@ -930,9 +975,7 @@ class JDexIssueFolderWhereNoteExpected(JDexIssue): def display(self, base_path: PurePath) -> str: """Display this particular instance of an error.""" - return ( - f'{self.file.relative_to(base_path)!s} (matched "{self.matched_pattern}")' - ) + return f"{self.file.relative_to(base_path)!s}\n (matched {_print_pattern(self.matched_pattern)})" def explain(self) -> _Explanation: """Explain what this error is.""" @@ -1000,9 +1043,7 @@ class JDexIssueEncounteredForbiddenFolder(JDexIssue): def display(self, base_path: PurePath) -> str: """Display this particular instance of an error.""" - return ( - f'{self.file.relative_to(base_path)!s} (matched "{self.matched_pattern}")' - ) + return f"{self.file.relative_to(base_path)!s}\n (matched {_print_pattern(self.matched_pattern)})" def explain(self) -> _Explanation: """Explain what this error is.""" @@ -1021,9 +1062,7 @@ class JDexIssueEncounteredForbiddenNote(JDexIssue): def display(self, base_path: PurePath) -> str: """Display this particular instance of an error.""" - return ( - f'{self.file.relative_to(base_path)!s} (matched "{self.matched_pattern}")' - ) + return f"{self.file.relative_to(base_path)!s}\n (matched {_print_pattern(self.matched_pattern)})" def explain(self) -> _Explanation: """Explain what this error is.""" @@ -1070,13 +1109,21 @@ class SystemFolder: children: dict[str, list[SystemFolder]] +@dataclass(frozen=True) +class JDexEntry: + """An entry in a JDex, including the path to the note that defined it.""" + + entry: str + path: PurePath + + @dataclass(frozen=True) class JDexLintResults: """All errors returned from linting the JDex.""" errors: list[JDexIssue] path: PurePath - entries: dict[str, list[PurePath]] + entries: dict[str, list[JDexEntry]] @dataclass(frozen=True) @@ -1232,12 +1279,12 @@ def _process_single_file_jdex(path: Path) -> _JDexResults: ) -def _get_jdex_notes_here_or_children( +def _get_jdex_entries_here_or_children( ignored: tuple[str], bound_segments: dict[str, str], path: os.PathLike, tier: ConfigJDexTier | ConfigSystemJDex, -) -> tuple[dict[str, list[PurePath]], list[JDexIssue]]: +) -> tuple[dict[str, list[JDexEntry]], list[JDexIssue]]: # Compile regexes for children valid_children = [ (re.compile(c.format.build_regex(bound_segments)), c) for c in tier.children @@ -1246,8 +1293,8 @@ def _get_jdex_notes_here_or_children( (re.compile(n.format.build_regex(bound_segments)), n) for n in tier.notes ] - accumulated_notes = {} - accumulated_errors = [] + accumulated_entries: dict[str, list[JDexEntry]] = {} + accumulated_errors: list[JDexIssue] = [] has_content = False with os.scandir(path) as contents: @@ -1284,14 +1331,14 @@ def _get_jdex_notes_here_or_children( break # Walk child - (child_notes, child_errors) = _get_jdex_notes_here_or_children( + (child_entries, child_errors) = _get_jdex_entries_here_or_children( ignored, {**bound_segments, **match.groupdict()}, PurePath(x), child, ) - for id, notes in child_notes.items(): - _insert_concat(id, notes, accumulated_notes) + for id, entries in child_entries.items(): + _insert_concat(id, entries, accumulated_entries) accumulated_errors.extend(child_errors) break else: @@ -1323,12 +1370,17 @@ def _get_jdex_notes_here_or_children( ) break - # Create note entry + # Create entry for id in note.ids: _insert_append( - id.build({**bound_segments, **match.groupdict()}), - PurePath(x), - accumulated_notes, + id.id.build({**bound_segments, **match.groupdict()}), + JDexEntry( + id.entry.build( + {**bound_segments, **match.groupdict()}, + ), + PurePath(x), + ), + accumulated_entries, ) break else: @@ -1351,14 +1403,14 @@ def _get_jdex_notes_here_or_children( if not has_content: # We have a fully empty JDex folder; it shouldn't exist if it's doing nothing. accumulated_errors.append(JDexIssueEmptyFolder(PurePath(path))) - return (accumulated_notes, accumulated_errors) + return (accumulated_entries, accumulated_errors) def _process_jdex( ignored: tuple[str], jdex: ConfigSystemJDex, -) -> tuple[dict[str, list[PurePath]], list[JDexIssue]]: - (jdex_notes_by_id, jdex_errors) = _get_jdex_notes_here_or_children( +) -> tuple[dict[str, list[JDexEntry]], list[JDexIssue]]: + (jdex_entries_by_id, jdex_errors) = _get_jdex_entries_here_or_children( ignored + jdex.ignore, {}, jdex.path, @@ -1367,12 +1419,12 @@ def _process_jdex( # Check for duplicate ids duplicate_id_errors = [ - JDexIssueDuplicateID(ns[0], tuple(ns), id) - for id, ns in jdex_notes_by_id.items() + JDexIssueDuplicateID(ns[0].path, tuple(n.path for n in ns), id) + for id, ns in jdex_entries_by_id.items() if len(ns) != 1 ] return ( - jdex_notes_by_id, + jdex_entries_by_id, jdex_errors + duplicate_id_errors, ) @@ -1382,7 +1434,7 @@ def _process_system_level_and_children( bound_segments: dict[str, str], path: os.PathLike, tier: ConfigSystemRoot | ConfigSystemTier, - jdex: None | dict[str, list[PurePath]], + jdex: None | dict[str, list[JDexEntry]], by_id_dict: dict[str, list[tuple[str | None, PurePath]]], ) -> tuple[dict[str, list[SystemFolder]], list[Issue]]: # Compile regexes for children @@ -1448,18 +1500,19 @@ def _process_system_level_and_children( SystemFolder(PurePath(x), child_structure), accumulated_structure, ) - _insert_append( - child_id, - ( - child.jdex_note.build( - {**bound_segments, **match.groupdict()}, - ) - if child.jdex_note - else None, - PurePath(x), - ), - by_id_dict, - ) + if not child.no_jdex_entry: + _insert_append( + child_id, + ( + child.jdex_entry.build( + {**bound_segments, **match.groupdict()}, + ) + if child.jdex_entry + else x.name, + PurePath(x), + ), + by_id_dict, + ) accumulated_errors.extend(child_errors) break else: @@ -1497,7 +1550,7 @@ def _process_system_root( ignored: tuple[str], root: ConfigSystemRoot, system: ConfigSystem, - jdex: None | dict[str, list[PurePath]], + jdex: None | dict[str, list[JDexEntry]], ) -> tuple[dict[str, list[SystemFolder]], list[Issue]]: by_id: dict[str, list[tuple[str | None, PurePath]]] = {} (root_structure, root_errors) = _process_system_level_and_children( @@ -1523,14 +1576,14 @@ def _process_system_root( if id not in jdex: id_errors.append(IssueIDNotInJDex(fs[0][1], id)) else: - jdex_notes = [n.name for n in jdex[id]] - for expected_jdex_note, f in fs: - if expected_jdex_note and expected_jdex_note not in jdex_notes: + jdex_entries = [n.entry for n in jdex[id]] + for expected_jdex_entry, f in fs: + if expected_jdex_entry and expected_jdex_entry not in jdex_entries: id_errors.append( IssueIDDifferentFromJDex( f, id, - expected_jdex_note, + expected_jdex_entry, jdex[id], ), ) @@ -1540,9 +1593,9 @@ def _process_system_root( def lint_system(config: Config) -> LintResults: jdex_errors = [] - jdex_notes = {} + jdex_entries = {} if config.system.jdex: - (jdex_notes, jdex_errors) = _process_jdex( + (jdex_entries, jdex_errors) = _process_jdex( config.linter.ignore, config.system.jdex, ) @@ -1553,10 +1606,10 @@ def lint_system(config: Config) -> LintResults: config.linter.ignore, root, config.system, - jdex_notes if config.system.jdex else None, + jdex_entries if config.system.jdex else None, ) ignored_errors += sum( - (1 for e in root_errors if e.type in config.linter.disable_rules) + 1 for e in root_errors if e.type in config.linter.disable_rules ) root_errors = [ e for e in root_errors if e.type not in config.linter.disable_rules @@ -1568,7 +1621,7 @@ def lint_system(config: Config) -> LintResults: ) ignored_jdex_errors = sum( - (1 for e in jdex_errors if e.type in config.linter.disable_rules) + 1 for e in jdex_errors if e.type in config.linter.disable_rules ) return LintResults( @@ -1578,7 +1631,7 @@ def lint_system(config: Config) -> LintResults: key=_sort_jdex_error, ), config.system.jdex.path, - jdex_notes, + jdex_entries, ) if config.system.jdex else None, @@ -1699,13 +1752,13 @@ def lint_system(config: Config) -> LintResults: [e.display(root.path) for e in errs], ), " ", - ) + ), ) print(f"---\n{explanation.fix}\n") if results.ignored_errs: print( - f"{'':=^80}\n{'Ignored Errors: ' + str(results.ignored_errs):^80}\n{'':=^80}" + f"{'':=^80}\n{'Ignored Errors: ' + str(results.ignored_errs):^80}\n{'':=^80}", ) if any_errors: # Exit unhappily diff --git a/tests/ENCOUNTERED_FORBIDDEN_FOLDER/jdlint.toml b/tests/ENCOUNTERED_FORBIDDEN_FOLDER/jdlint.toml index 9fffbc9..13e98cd 100644 --- a/tests/ENCOUNTERED_FORBIDDEN_FOLDER/jdlint.toml +++ b/tests/ENCOUNTERED_FORBIDDEN_FOLDER/jdlint.toml @@ -6,9 +6,6 @@ ignore = [".placeholder"] name = "Files" path = "files" -## ########################################################### -# Standard -## ########################################################### [[system.default.children]] name = "Area" format = "/#A/0-/=A/9 /*Area/" diff --git a/tests/FOLDER_SHOULD_BE_EMPTY/jdlint.toml b/tests/FOLDER_SHOULD_BE_EMPTY/jdlint.toml index 3ad276a..10b8c43 100644 --- a/tests/FOLDER_SHOULD_BE_EMPTY/jdlint.toml +++ b/tests/FOLDER_SHOULD_BE_EMPTY/jdlint.toml @@ -6,37 +6,29 @@ ignore = [".placeholder"] name = "Files" path = "files" -## ########################################################### -# Standard -## ########################################################### [[system.default.children]] name = "Area" format = "/#A/0-/=A/9 /*Area/" -jdex_note = "/=A/0.00 /=Area/.md" id = "/=A/0-/=A/9" [[system.default.children.children]] name = "Category" format = "/=A//#C/ /*Category/" -jdex_note = "/=A//=C/.00 /=Category/.md" id = "/=A//=C/" [[system.default.children.children.children]] name = "Header" format = "/=A//=C/./#I/0 ■ /*Header/" -jdex_note = "/=A//=C/./=I/0 ■ /=Header/.md" id = "/=A//=C/./=I/0" [[system.default.children.children.children]] name = "Inbox" format = "/=A//=C/.01 /*Inbox/" -jdex_note = "/=A//=C/.01 /=Inbox/.md" id = "/=A//=C/.01" [[system.default.children.children.children]] name = "ID" format = "/=A//=C/./##ID/ /*IDName/" -jdex_note = "/=A//=C/./=ID/ /=IDName/.md" id = "/=A//=C/./=ID/" can_be_file = true allow_arbitrary_contents = true diff --git a/tests/ID_DIFFERENT_FROM_JDEX/jdlint.toml b/tests/ID_DIFFERENT_FROM_JDEX/jdlint.toml index 2e63069..77dc76e 100644 --- a/tests/ID_DIFFERENT_FROM_JDEX/jdlint.toml +++ b/tests/ID_DIFFERENT_FROM_JDEX/jdlint.toml @@ -9,9 +9,6 @@ path = "files" [system.jdex] path = "jdex" -## ########################################################### -# Partially Nested -## ########################################################### [[system.jdex.children]] name = "JDex Area Folder" format = "/#A/0-/=A/9 /*Area/" @@ -23,37 +20,48 @@ format = "/=A//#C/ /*Category/" [[system.jdex.children.children.notes]] name = "JDex Area Note" format = "/=A/0.00 /*Name/.md" -ids = ["/=A/0-/=A/9", "/=A/0.00"] + +[[system.jdex.children.children.notes.ids]] +id = "/=A/0-/=A/9" +entry = "/=A/0-/=A/9 /=Name/" + +[[system.jdex.children.children.notes.ids]] +id = "/=A/0.00" +entry = "/=A/0.00 /=Name/" [[system.jdex.children.children.notes]] name = "JDex Category Note" format = "/=A//=C/.00 /*Name/.md" -ids = ["/=A//=C/", "/=A//=C/.00"] + +[[system.jdex.children.children.notes.ids]] +id = "/=A//=C/" +entry = "/=A//=C/ /=Name/" + +[[system.jdex.children.children.notes.ids]] +id = "/=A//=C/.00" +entry = "/=A//=C/.00 /=Name/" [[system.jdex.children.children.notes]] name = "JDex Id Note" format = "/=A//=C/./##ID/ /*Name/.md" -ids = ["/=A//=C/./=ID/"] -## ########################################################### -# Standard -## ########################################################### +[[system.jdex.children.children.notes.ids]] +id = "/=A//=C/./=ID/" +entry = "/=A//=C/./=ID/ /=Name/" + [[system.default.children]] name = "Area" format = "/#A/0-/=A/9 /*Area/" -jdex_note = "/=A/0.00 /=Area/.md" id = "/=A/0-/=A/9" [[system.default.children.children]] name = "Category" format = "/=A//#C/ /*Category/" -jdex_note = "/=A//=C/.00 /=Category/.md" id = "/=A//=C/" [[system.default.children.children.children]] name = "ID" format = "/=A//=C/./##ID/ /*IDName/" -jdex_note = "/=A//=C/./=ID/ /=IDName/.md" id = "/=A//=C/./=ID/" can_be_file = true allow_arbitrary_contents = true diff --git a/tests/ID_DIFFERENT_FROM_JDEX/result.json b/tests/ID_DIFFERENT_FROM_JDEX/result.json index a18df65..323a853 100644 --- a/tests/ID_DIFFERENT_FROM_JDEX/result.json +++ b/tests/ID_DIFFERENT_FROM_JDEX/result.json @@ -5,9 +5,12 @@ "type": "ID_DIFFERENT_FROM_JDEX", "id": "11.12", "file": "files/10-19 Life Admin/11 Me, Myself, & I/11.12 Myslef", - "expected_jdex_note": "11.12 Myslef.md", - "known_jdex_notes": [ - "jdex/10-19 Life Admin/11 Me, Myself, & I/11.12 Myself.md" + "expected_jdex_entry": "11.12 Myslef", + "known_jdex_entries": [ + { + "entry": "11.12 Myself", + "path": "jdex/10-19 Life Admin/11 Me, Myself, & I/11.12 Myself.md" + } ] } ] diff --git a/tests/ID_NOT_IN_JDEX/jdlint.toml b/tests/ID_NOT_IN_JDEX/jdlint.toml index 2e63069..77dc76e 100644 --- a/tests/ID_NOT_IN_JDEX/jdlint.toml +++ b/tests/ID_NOT_IN_JDEX/jdlint.toml @@ -9,9 +9,6 @@ path = "files" [system.jdex] path = "jdex" -## ########################################################### -# Partially Nested -## ########################################################### [[system.jdex.children]] name = "JDex Area Folder" format = "/#A/0-/=A/9 /*Area/" @@ -23,37 +20,48 @@ format = "/=A//#C/ /*Category/" [[system.jdex.children.children.notes]] name = "JDex Area Note" format = "/=A/0.00 /*Name/.md" -ids = ["/=A/0-/=A/9", "/=A/0.00"] + +[[system.jdex.children.children.notes.ids]] +id = "/=A/0-/=A/9" +entry = "/=A/0-/=A/9 /=Name/" + +[[system.jdex.children.children.notes.ids]] +id = "/=A/0.00" +entry = "/=A/0.00 /=Name/" [[system.jdex.children.children.notes]] name = "JDex Category Note" format = "/=A//=C/.00 /*Name/.md" -ids = ["/=A//=C/", "/=A//=C/.00"] + +[[system.jdex.children.children.notes.ids]] +id = "/=A//=C/" +entry = "/=A//=C/ /=Name/" + +[[system.jdex.children.children.notes.ids]] +id = "/=A//=C/.00" +entry = "/=A//=C/.00 /=Name/" [[system.jdex.children.children.notes]] name = "JDex Id Note" format = "/=A//=C/./##ID/ /*Name/.md" -ids = ["/=A//=C/./=ID/"] -## ########################################################### -# Standard -## ########################################################### +[[system.jdex.children.children.notes.ids]] +id = "/=A//=C/./=ID/" +entry = "/=A//=C/./=ID/ /=Name/" + [[system.default.children]] name = "Area" format = "/#A/0-/=A/9 /*Area/" -jdex_note = "/=A/0.00 /=Area/.md" id = "/=A/0-/=A/9" [[system.default.children.children]] name = "Category" format = "/=A//#C/ /*Category/" -jdex_note = "/=A//=C/.00 /=Category/.md" id = "/=A//=C/" [[system.default.children.children.children]] name = "ID" format = "/=A//=C/./##ID/ /*IDName/" -jdex_note = "/=A//=C/./=ID/ /=IDName/.md" id = "/=A//=C/./=ID/" can_be_file = true allow_arbitrary_contents = true diff --git a/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml b/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml index 5f75ea0..c8eedac 100644 --- a/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml +++ b/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml @@ -20,7 +20,10 @@ allow_arbitrary_contents = true [[system.jdex.children.children.notes]] name = "C1" format = "X.md" -ids = ["/=A/./=Name/"] + +[[system.jdex.children.children.notes.ids]] +id = "/=A/./=Name/" +entry = "/=A/./=Name/" [[system.jdex.children.children]] name = "B2" @@ -29,16 +32,21 @@ format = "/=A//*Name/" [[system.jdex.children.children.notes]] name = "C3" format = "/=A/.md" -ids = ["/=A/"] + +[[system.jdex.children.children.notes.ids]] +id = "/=A/" +entry = "/=A/" [[system.jdex.children.children.notes]] name = "C2" format = "/=A//=Name/.md" -ids = ["/=A/./=Name/"] + +[[system.jdex.children.children.notes.ids]] +id = "/=A/./=Name/" +entry = "/=A/./=Name/" [[system.default.children]] name = "Area" format = "/#A/" -jdex_note = "/=A/.md" id = "/=A/" allow_arbitrary_contents = true diff --git a/tests/JDEX_DUPLICATE_ID/jdlint.toml b/tests/JDEX_DUPLICATE_ID/jdlint.toml index f9a7637..c92e7b7 100644 --- a/tests/JDEX_DUPLICATE_ID/jdlint.toml +++ b/tests/JDEX_DUPLICATE_ID/jdlint.toml @@ -15,16 +15,21 @@ format = "/#A/" [[system.jdex.children.notes]] name = "C" format = "/=A/.md" -ids = ["/=A/"] + +[[system.jdex.children.notes.ids]] +id = "/=A/" +entry = "/=A/" [[system.jdex.children.notes]] name = "B" -format = "/=A//#ID//*Name/.md" -ids = ["/=A/./=ID/"] +format = "/=A//#ID/ /*Name/.md" + +[[system.jdex.children.notes.ids]] +id = "/=A/./=ID/" +entry = "/=A/./=ID/ /=Name/" [[system.default.children]] name = "Area" format = "/#A/" -jdex_note = "/=A/.md" id = "/=A/" allow_arbitrary_contents = true diff --git a/tests/JDEX_EMPTY_FOLDER/jdlint.toml b/tests/JDEX_EMPTY_FOLDER/jdlint.toml index ad7d35e..e84e8cb 100644 --- a/tests/JDEX_EMPTY_FOLDER/jdlint.toml +++ b/tests/JDEX_EMPTY_FOLDER/jdlint.toml @@ -21,11 +21,13 @@ allow_arbitrary_contents = true [[system.jdex.children.children.notes]] name = "C1" format = "X.md" -ids = ["/=A/"] + +[[system.jdex.children.children.notes.ids]] +id = "/=A/" +entry = "/=A/" [[system.default.children]] name = "Area" format = "/#A/" -jdex_note = "X.md" id = "/=A/" allow_arbitrary_contents = true diff --git a/tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/jdlint.toml b/tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/jdlint.toml index e85ce8d..50d3a0e 100644 --- a/tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/jdlint.toml +++ b/tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/jdlint.toml @@ -9,9 +9,6 @@ path = "files" [system.jdex] path = "jdex" -## ########################################################### -# Partially Nested -## ########################################################### [[system.jdex.children]] name = "JDex Area Folder" format = "/#A/0-/=A/9 /*Area/" @@ -23,7 +20,15 @@ format = "/=A/0 Management of area /=A/0-/=A/9" [[system.jdex.children.children.notes]] name = "JDex Area Note" format = "/=A/0.00 /*Name/.md" -ids = ["/=A/0-/=A/9", "/=A/0.00"] +jdex_entry = "/=A/0.00 /=Name/" + +[[system.jdex.children.children.notes.ids]] +id = "/=A/0-/=A/9" +entry = "/=A/0-/=A/9 /=Name/" + +[[system.jdex.children.children.notes.ids]] +id = "/=A/0.00" +entry = "/=A/0.00 /=Name/" [[system.jdex.children.children]] name = "JDex Bad Area Management Folder" @@ -34,40 +39,40 @@ forbidden = true name = "JDex Category Folder" format = "/=A//#C/ /*Category/" -[[system.jdex.children.children.notes]] -name = "JDex Area Note" -format = "/=A/0.00 /*Name/.md" -ids = ["/=A/0-/=A/9", "/=A/0.00"] - [[system.jdex.children.children.notes]] name = "JDex Category Note" format = "/=A//=C/.00 /*Name/.md" -ids = ["/=A//=C/", "/=A//=C/.00"] +jdex_entry = "/=A//=C/.00 /=Name/" + +[[system.jdex.children.children.notes.ids]] +id = "/=A//=C/" +entry = "/=A//=C/ /=Name/" + +[[system.jdex.children.children.notes.ids]] +id = "/=A//=C/.00" +entry = "/=A//=C/.00 /=Name/" [[system.jdex.children.children.notes]] name = "JDex Id Note" format = "/=A//=C/./##ID/ /*Name/.md" -ids = ["/=A//=C/./=ID/"] -## ########################################################### -# Standard -## ########################################################### +[[system.jdex.children.children.notes.ids]] +id = "/=A//=C/./=ID/" +entry = "/=A//=C/./=ID/ /=Name/" + [[system.default.children]] name = "Area" format = "/#A/0-/=A/9 /*Area/" -jdex_note = "/=A/0.00 /=Area/.md" id = "/=A/0-/=A/9" [[system.default.children.children]] name = "Category" format = "/=A//#C/ /*Category/" -jdex_note = "/=A//=C/.00 /=Category/.md" id = "/=A//=C/" [[system.default.children.children.children]] name = "ID" format = "/=A//=C/./##ID/ /*IDName/" -jdex_note = "/=A//=C/./=ID/ /=IDName/.md" id = "/=A//=C/./=ID/" can_be_file = true allow_arbitrary_contents = true diff --git a/tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/jdlint.toml b/tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/jdlint.toml index e86d83a..3e2fe13 100644 --- a/tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/jdlint.toml +++ b/tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/jdlint.toml @@ -9,9 +9,6 @@ path = "files" [system.jdex] path = "jdex" -## ########################################################### -# Partially Nested -## ########################################################### [[system.jdex.children]] name = "JDex Area Folder" format = "/#A/0-/=A/9 /*Area/" @@ -23,48 +20,61 @@ format = "/=A//#C/ /*Category/" [[system.jdex.children.children.notes]] name = "JDex Area Note" format = "/=A/0.00 /*Name/.md" -ids = ["/=A/0-/=A/9", "/=A/0.00"] + +[[system.jdex.children.children.notes.ids]] +id = "/=A/0-/=A/9" +entry = "/=A/0-/=A/9 /=Name/" + +[[system.jdex.children.children.notes.ids]] +id = "/=A/0.00" +entry = "/=A/0.00 /=Name/" [[system.jdex.children.children.notes]] name = "JDex Category Note" format = "/=A//=C/.00 /*Name/.md" -ids = ["/=A//=C/", "/=A//=C/.00"] + +[[system.jdex.children.children.notes.ids]] +id = "/=A//=C/" +entry = "/=A//=C/ /=Name/" + +[[system.jdex.children.children.notes.ids]] +id = "/=A//=C/.00" +entry = "/=A//=C/.00 /=Name/" [[system.jdex.children.children.notes]] name = "JDex Inbox" format = "/=A//=C/.01 Inbox.md" -ids = ["/=A//=C/.01"] + +[[system.jdex.children.children.notes.ids]] +id = "/=A//=C/.01" +entry = "/=A//=C/.01 Inbox" [[system.jdex.children.children.notes]] name = "JDex Bad Inbox" format = "/=A//=C/.01 /*Name/" -ids = ["/=A//=C/.01"] forbidden = true [[system.jdex.children.children.notes]] name = "JDex Id Note" format = "/=A//=C/./##ID/ /*Name/.md" -ids = ["/=A//=C/./=ID/"] -## ########################################################### -# Standard -## ########################################################### +[[system.jdex.children.children.notes.ids]] +id = "/=A//=C/./=ID/" +entry = "/=A//=C/./=ID/ /=Name/" + [[system.default.children]] name = "Area" format = "/#A/0-/=A/9 /*Area/" -jdex_note = "/=A/0.00 /=Area/.md" id = "/=A/0-/=A/9" [[system.default.children.children]] name = "Category" format = "/=A//#C/ /*Category/" -jdex_note = "/=A//=C/.00 /=Category/.md" id = "/=A//=C/" [[system.default.children.children.children]] name = "ID" format = "/=A//=C/./##ID/ /*IDName/" -jdex_note = "/=A//=C/./=ID/ /=IDName/.md" id = "/=A//=C/./=ID/" can_be_file = true allow_arbitrary_contents = true diff --git a/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/jdlint.toml b/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/jdlint.toml index 6c0a100..7ce621b 100644 --- a/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/jdlint.toml +++ b/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/jdlint.toml @@ -19,16 +19,21 @@ format = "/=A//*Name/" [[system.jdex.children.children.notes]] name = "C" format = "/=A/.md" -ids = ["/=A/"] + +[[system.jdex.children.children.notes.ids]] +id = "/=A/" +entry = "/=A/" [[system.jdex.children.children.notes]] name = "C" format = "/=A//=Name/.md" -ids = ["/=A/./=Name/"] + +[[system.jdex.children.children.notes.ids]] +id = "/=A/./=Name/" +entry = "/=A/./=Name/" [[system.default.children]] name = "Area" format = "/#A/" -jdex_note = "/=A/.md" id = "/=A/" allow_arbitrary_contents = true diff --git a/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/jdlint.toml b/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/jdlint.toml index ded3294..bed73d7 100644 --- a/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/jdlint.toml +++ b/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/jdlint.toml @@ -19,16 +19,21 @@ format = "/=A//*Name/" [[system.jdex.children.children.notes]] name = "JDex Note" format = "/=A/.md" -ids = ["/=A/"] + +[[system.jdex.children.children.notes.ids]] +id = "/=A/" +entry = "/=A/" [[system.jdex.children.children.notes]] name = "JDex Note" format = "/=A//=Name/.md" -ids = ["/=A/./=Name/"] + +[[system.jdex.children.children.notes.ids]] +id = "/=A/./=Name/" +entry = "/=A/./=Name/" [[system.default.children]] name = "Area" format = "/#A/" -jdex_note = "/=A/.md" id = "/=A/" allow_arbitrary_contents = true diff --git a/tests/jdex_entry/files/10-19 Life Admin/10 No JDex/.placeholder b/tests/jdex_entry/files/10-19 Life Admin/10 No JDex/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/tests/jdex_entry/files/10-19 Life Admin/11 Me, Myself, & I/11.11 Me/Content b/tests/jdex_entry/files/10-19 Life Admin/11 Me, Myself, & I/11.11 Me/Content new file mode 100644 index 0000000..e69de29 diff --git a/tests/jdex_entry/files/10-19 Life Admin/11 Me, Myself, & I/11.12 Myslef/Content b/tests/jdex_entry/files/10-19 Life Admin/11 Me, Myself, & I/11.12 Myslef/Content new file mode 100644 index 0000000..e69de29 diff --git a/tests/jdex_entry/jdex/10-19 Life Admin/10 Management of area 10-19/10.00 Life Admin.md b/tests/jdex_entry/jdex/10-19 Life Admin/10 Management of area 10-19/10.00 Life Admin.md new file mode 100644 index 0000000..e69de29 diff --git a/tests/jdex_entry/jdex/10-19 Life Admin/11 Me, Myself, & I/11.00 Me, Myself, & I.md b/tests/jdex_entry/jdex/10-19 Life Admin/11 Me, Myself, & I/11.00 Me, Myself, & I.md new file mode 100644 index 0000000..e69de29 diff --git a/tests/jdex_entry/jdex/10-19 Life Admin/11 Me, Myself, & I/11.11 Me.md b/tests/jdex_entry/jdex/10-19 Life Admin/11 Me, Myself, & I/11.11 Me.md new file mode 100644 index 0000000..e69de29 diff --git a/tests/jdex_entry/jdex/10-19 Life Admin/11 Me, Myself, & I/11.12 Myself.md b/tests/jdex_entry/jdex/10-19 Life Admin/11 Me, Myself, & I/11.12 Myself.md new file mode 100644 index 0000000..e69de29 diff --git a/tests/jdex_entry/jdlint.toml b/tests/jdex_entry/jdlint.toml new file mode 100644 index 0000000..af1e671 --- /dev/null +++ b/tests/jdex_entry/jdlint.toml @@ -0,0 +1,81 @@ +[linter] +json_output = true +ignore = [".placeholder"] + +[[system.roots]] +name = "Files" +path = "files" + +[system.jdex] +path = "jdex" + +[[system.jdex.children]] +name = "JDex Area Folder" +format = "/#A/0-/=A/9 /*Area/" + +[[system.jdex.children.children]] +name = "JDex Category Folder" +format = "/=A//#C/ /*Category/" + +[[system.jdex.children.children.notes]] +name = "JDex Area Note" +format = "/=A/0.00 /*Name/.md" + +[[system.jdex.children.children.notes.ids]] +id = "/=A/0-/=A/9" +entry = "/=A/0-/=A/9 /=Name/" + +[[system.jdex.children.children.notes.ids]] +id = "/=A/0.00" +entry = "/=A/0.00 /=Name/" + +[[system.jdex.children.children.notes]] +name = "JDex Category Note" +format = "/=A//=C/.00 /*Name/.md" + +[[system.jdex.children.children.notes.ids]] +id = "/=A//=C/" +entry = "/=A//=C/ /=Name/" + +[[system.jdex.children.children.notes.ids]] +id = "/=A//=C/.00" +entry = "/=A//=C/.00 /=Name/" + +[[system.jdex.children.children.notes]] +name = "JDex Id Note" +format = "/=A//=C/./##ID/ /*Name/.md" + +[[system.jdex.children.children.notes.ids]] +id = "/=A//=C/./=ID/" +entry = "/=A//=C/./=ID/ /=Name/" + +[[system.default.children]] +name = "Area" +format = "/#A/0-/=A/9 /*Area/" +id = "/=A/0-/=A/9" + +[[system.default.children.children]] +name = "10 Category" +format = "10 /*Category/" +id = "10" +no_jdex_entry = true + +[[system.default.children.children]] +name = "Category" +format = "/=A//#C/ /*Category/" +id = "/=A//=C/" + +[[system.default.children.children.children]] +name = "ID" +format = "/=A//=C/.12 Myslef" +id = "/=A//=C/.12" +jdex_entry = "/=A//=C/.12 Myself" +can_be_file = true +allow_arbitrary_contents = true + +[[system.default.children.children.children]] +name = "ID" +format = "/=A//=C/./##ID/ /*IDName/" +id = "/=A//=C/./=ID/" +can_be_file = true +allow_arbitrary_contents = true diff --git a/tests/jdex_entry/result.json b/tests/jdex_entry/result.json new file mode 100644 index 0000000..da0ee54 --- /dev/null +++ b/tests/jdex_entry/result.json @@ -0,0 +1,4 @@ +{ + "errors": {}, + "jdex_errors": [] +} \ No newline at end of file From e93ed18a948be51473c77340d70dc92d85b6357b Mon Sep 17 00:00:00 2001 From: SiriusStarr <2049163+SiriusStarr@users.noreply.github.com> Date: Sun, 26 Jul 2026 13:50:30 -0700 Subject: [PATCH 17/23] Test all returned info and sort all info --- README.md | 4 +- jdlint.py | 207 +++++++++++------- run_tests.py | 11 +- .../jdlint.toml | 2 +- .../result.json | 168 ++++++++------ tests/DUPLICATE_ID/jdlint.toml | 2 +- tests/DUPLICATE_ID/result.json | 84 +++++-- tests/EMPTY_FOLDER/jdlint.toml | 2 +- tests/EMPTY_FOLDER/result.json | 33 ++- .../ENCOUNTERED_FORBIDDEN_FOLDER/result.json | 82 ++++--- tests/FILE_WHERE_FOLDER_EXPECTED/jdlint.toml | 4 +- tests/FILE_WHERE_FOLDER_EXPECTED/result.json | 69 +++--- .../{11.12 Myslef => 11.12 Myself}/Content | 0 tests/FOLDER_SHOULD_BE_EMPTY/result.json | 76 +++++-- tests/ID_DIFFERENT_FROM_JDEX/result.json | 97 +++++++- tests/ID_NOT_IN_JDEX/result.json | 95 +++++++- .../jdlint.toml | 2 +- .../result.json | 195 ++++++++++------- tests/JDEX_DUPLICATE_ID/jdlint.toml | 2 +- tests/JDEX_DUPLICATE_ID/result.json | 55 ++++- tests/JDEX_EMPTY_FOLDER/jdlint.toml | 2 +- tests/JDEX_EMPTY_FOLDER/result.json | 37 +++- .../result.json | 101 +++++++-- .../result.json | 103 +++++++-- .../jdlint.toml | 2 +- .../result.json | 55 ++++- .../jdlint.toml | 2 +- .../result.json | 57 +++-- tests/can_be_file/jdlint.toml | 2 +- tests/can_be_file/result.json | 41 +++- tests/jdex_entry/result.json | 85 ++++++- 31 files changed, 1238 insertions(+), 439 deletions(-) rename tests/FOLDER_SHOULD_BE_EMPTY/files/10-19 Life Admin/11 Me, Myself, & I/{11.12 Myslef => 11.12 Myself}/Content (100%) diff --git a/README.md b/README.md index 6aebb8d..2a9301f 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,9 @@ just once with the flag: Note that these JSON results additionally contain complete information about the structure of your system that jdlint had to scan, if that information if of use -to you. +to you. This structure only returns portions of the system that are at least +possibly valid; it will not return forbidden files/folders, or files where +folders are required, for example. ## Errors diff --git a/jdlint.py b/jdlint.py index 7f72f01..ea714a4 100755 --- a/jdlint.py +++ b/jdlint.py @@ -733,7 +733,7 @@ class Issue: file: PurePath type = None - def display(self, base_path: PurePath) -> str: + def display(self) -> str: """Display this particular instance of an error.""" raise NotImplementedError @@ -749,7 +749,7 @@ class JDexIssue: file: PurePath type = None - def display(self, base_path: PurePath) -> str: + def display(self) -> str: """Display this particular instance of an error.""" raise NotImplementedError @@ -764,9 +764,9 @@ class IssueEmptyFolder(Issue): type: Literal["EMPTY_FOLDER"] = "EMPTY_FOLDER" - def display(self, base_path: PurePath) -> str: + def display(self) -> str: """Display this particular instance of an error.""" - return f"{self.file.relative_to(base_path)!s}" + return f"{self.file!s}" def explain(self) -> _Explanation: """Explain what this error is.""" @@ -783,9 +783,9 @@ class IssueFileWhereFolderExpected(Issue): matched_pattern: ContentPattern type: Literal["FILE_WHERE_FOLDER_EXPECTED"] = "FILE_WHERE_FOLDER_EXPECTED" - def display(self, base_path: PurePath) -> str: + def display(self) -> str: """Display this particular instance of an error.""" - return f"{self.file.relative_to(base_path)!s}\n (matched {_print_pattern(self.matched_pattern)})" + return f"{self.file!s}\n (matched {_print_pattern(self.matched_pattern)})" def explain(self) -> _Explanation: """Explain what this error is.""" @@ -804,9 +804,9 @@ class IssueArbitraryContentWhereNotAllowed(Issue): "ARBITRARY_CONTENT_WHERE_NOT_ALLOWED" ) - def display(self, base_path: PurePath) -> str: + def display(self) -> str: """Display this particular instance of an error.""" - return f"{self.file.relative_to(base_path)!s}\n{textwrap.indent(_print_unmatched_patterns(self.possible_formats), ' ')}" + return f"{self.file!s}\n{textwrap.indent(_print_unmatched_patterns(self.possible_formats), ' ')}" def explain(self) -> _Explanation: """Explain what this error is.""" @@ -823,11 +823,9 @@ class IssueFolderShouldBeEmpty(Issue): children: tuple[PurePath, ...] type: Literal["FOLDER_SHOULD_BE_EMPTY"] = "FOLDER_SHOULD_BE_EMPTY" - def display(self, base_path: PurePath) -> str: + def display(self) -> str: """Display this particular instance of an error.""" - return ( - f"{self.file.relative_to(base_path)!s}\n has {len(self.children)} children" - ) + return f"{self.file!s}\n has {len(self.children)} children" def explain(self) -> _Explanation: """Explain what this error is.""" @@ -845,7 +843,7 @@ class IssueDuplicateID(Issue): id: str type: Literal["DUPLICATE_ID"] = "DUPLICATE_ID" - def display(self, base_path: PurePath) -> str: + def display(self) -> str: """Display this particular instance of an error.""" return f"{self.id}:\n " + "\n ".join([str(f.name) for f in self.files]) @@ -864,9 +862,9 @@ class IssueIDNotInJDex(Issue): id: str type: Literal["ID_NOT_IN_JDEX"] = "ID_NOT_IN_JDEX" - def display(self, base_path: PurePath) -> str: + def display(self) -> str: """Display this particular instance of an error.""" - return f"{self.file.relative_to(base_path)!s} [ID: {self.id}]" + return f"{self.file!s} [ID: {self.id}]" def explain(self) -> _Explanation: """Explain what this error is.""" @@ -885,12 +883,12 @@ class IssueIDDifferentFromJDex(Issue): known_jdex_entries: list[JDexEntry] type: Literal["ID_DIFFERENT_FROM_JDEX"] = "ID_DIFFERENT_FROM_JDEX" - def display(self, base_path: PurePath) -> str: + def display(self) -> str: """Display this particular instance of an error.""" known = "\n".join( f"{n.entry} [from {n.path.name}]" for n in self.known_jdex_entries ) - return f"{self.file.relative_to(base_path)!s}\n ID: {self.id}\n Expected:\n {self.expected_jdex_entry}\n Actual:\n{textwrap.indent(known, ' ')}" + return f"{self.file!s}\n ID: {self.id}\n Expected:\n {self.expected_jdex_entry}\n Actual:\n{textwrap.indent(known, ' ')}" def explain(self) -> _Explanation: """Explain what this error is.""" @@ -907,9 +905,9 @@ class IssueEncounteredForbiddenFolder(Issue): matched_pattern: ContentPattern type: Literal["ENCOUNTERED_FORBIDDEN_FOLDER"] = "ENCOUNTERED_FORBIDDEN_FOLDER" - def display(self, base_path: PurePath) -> str: + def display(self) -> str: """Display this particular instance of an error.""" - return f"{self.file.relative_to(base_path)!s}\n (matched {_print_pattern(self.matched_pattern)})" + return f"{self.file!s}\n (matched {_print_pattern(self.matched_pattern)})" def explain(self) -> _Explanation: """Explain what this error is.""" @@ -935,7 +933,7 @@ class JDexIssueDuplicateID(JDexIssue): id: str type: Literal["JDEX_DUPLICATE_ID"] = "JDEX_DUPLICATE_ID" - def display(self, base_path: PurePath) -> str: + def display(self) -> str: """Display this particular instance of an error.""" return f"{self.id}:\n " + "\n ".join([str(f.name) for f in self.files]) @@ -954,9 +952,9 @@ class JDexIssueFileWhereFolderExpected(JDexIssue): matched_pattern: ContentPattern type: Literal["JDEX_FILE_WHERE_FOLDER_EXPECTED"] = "JDEX_FILE_WHERE_FOLDER_EXPECTED" - def display(self, base_path: PurePath) -> str: + def display(self) -> str: """Display this particular instance of an error.""" - return f"{self.file.relative_to(base_path)!s}\n (matched {_print_pattern(self.matched_pattern)})" + return f"{self.file!s}\n (matched {_print_pattern(self.matched_pattern)})" def explain(self) -> _Explanation: """Explain what this error is.""" @@ -973,9 +971,9 @@ class JDexIssueFolderWhereNoteExpected(JDexIssue): matched_pattern: ContentPattern type: Literal["JDEX_FOLDER_WHERE_NOTE_EXPECTED"] = "JDEX_FOLDER_WHERE_NOTE_EXPECTED" - def display(self, base_path: PurePath) -> str: + def display(self) -> str: """Display this particular instance of an error.""" - return f"{self.file.relative_to(base_path)!s}\n (matched {_print_pattern(self.matched_pattern)})" + return f"{self.file!s}\n (matched {_print_pattern(self.matched_pattern)})" def explain(self) -> _Explanation: """Explain what this error is.""" @@ -1002,9 +1000,9 @@ class JDexIssueArbitraryContentWhereNotAllowed(JDexIssue): "JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED" ) - def display(self, base_path: PurePath) -> str: + def display(self) -> str: """Display this particular instance of an error.""" - return f"{self.file.relative_to(base_path)!s}\n{textwrap.indent(_print_unmatched_patterns(self.possible_formats), ' ')}" + return f"{self.file!s}\n{textwrap.indent(_print_unmatched_patterns(self.possible_formats), ' ')}" def explain(self) -> _Explanation: """Explain what this error is.""" @@ -1020,9 +1018,9 @@ class JDexIssueEmptyFolder(JDexIssue): type: Literal["JDEX_EMPTY_FOLDER"] = "JDEX_EMPTY_FOLDER" - def display(self, base_path: PurePath) -> str: + def display(self) -> str: """Display this particular instance of an error.""" - return f"{self.file.relative_to(base_path)!s}" + return f"{self.file!s}" def explain(self) -> _Explanation: """Explain what this error is.""" @@ -1041,9 +1039,9 @@ class JDexIssueEncounteredForbiddenFolder(JDexIssue): "JDEX_ENCOUNTERED_FORBIDDEN_FOLDER" ) - def display(self, base_path: PurePath) -> str: + def display(self) -> str: """Display this particular instance of an error.""" - return f"{self.file.relative_to(base_path)!s}\n (matched {_print_pattern(self.matched_pattern)})" + return f"{self.file!s}\n (matched {_print_pattern(self.matched_pattern)})" def explain(self) -> _Explanation: """Explain what this error is.""" @@ -1060,9 +1058,9 @@ class JDexIssueEncounteredForbiddenNote(JDexIssue): matched_pattern: ContentPattern type: Literal["JDEX_ENCOUNTERED_FORBIDDEN_NOTE"] = "JDEX_ENCOUNTERED_FORBIDDEN_NOTE" - def display(self, base_path: PurePath) -> str: + def display(self) -> str: """Display this particular instance of an error.""" - return f"{self.file.relative_to(base_path)!s}\n (matched {_print_pattern(self.matched_pattern)})" + return f"{self.file!s}\n (matched {_print_pattern(self.matched_pattern)})" def explain(self) -> _Explanation: """Explain what this error is.""" @@ -1103,10 +1101,17 @@ class _Explanation: @dataclass(frozen=True) class SystemFolder: - """A folder detected in a JD root, including its path and its children (by ID)""" + """A folder detected in a JD root, including its path and its children (by ID).""" + + path: PurePath + children: dict[str, list[SystemFolder | SystemFile]] + + +@dataclass(frozen=True) +class SystemFile: + """A file detected in a JD root, consisting of its path.""" path: PurePath - children: dict[str, list[SystemFolder]] @dataclass(frozen=True) @@ -1132,7 +1137,7 @@ class RootLintResults: errors: list[Issue] path: PurePath - structure: dict[str, list[SystemFolder]] + structure: dict[str, list[SystemFolder | SystemFile]] @dataclass(frozen=True) @@ -1212,6 +1217,10 @@ def _sort_error(e: Issue) -> tuple[str, tuple[tuple[str, ...], str]]: ) +def _sort_jdex_entry(e: JDexEntry) -> tuple[str, PurePath]: + return (e.entry, e.path) + + def _entry_is_ignored( ignored: tuple[str] | None, f: os.DirEntry, @@ -1225,20 +1234,22 @@ def _entry_is_ignored( E = TypeVar("E") -def _insert_append(k, v, d) -> None: # noqa: ANN001 +def _insert_append_sorted(k, v, d, key=None) -> None: # noqa: ANN001 """Add value as a singleton if it's not already in the dict, else append it to the list.""" if k not in d: d.update({k: []}) d[k].append(v) + d[k].sort(key=key) -def _insert_concat(k, vs: list, d) -> None: # noqa: ANN001 +def _insert_concat_sorted(k, vs: list, d, key=None) -> None: # noqa: ANN001 """Add value as a singleton if it's not already in the dict, else append it to the list.""" if k not in d: d.update({k: []}) d[k].extend(vs) + d[k].sort(key=key) def _process_single_file_jdex(path: Path) -> _JDexResults: @@ -1283,6 +1294,7 @@ def _get_jdex_entries_here_or_children( ignored: tuple[str], bound_segments: dict[str, str], path: os.PathLike, + relative_to: PurePath, tier: ConfigJDexTier | ConfigSystemJDex, ) -> tuple[dict[str, list[JDexEntry]], list[JDexIssue]]: # Compile regexes for children @@ -1308,7 +1320,7 @@ def _get_jdex_entries_here_or_children( if child.format.forbidden: accumulated_errors.append( JDexIssueEncounteredForbiddenFolder( - PurePath(x), + PurePath(x).relative_to(relative_to), ContentPattern( child.format.name, child.format.raw_format, @@ -1321,7 +1333,7 @@ def _get_jdex_entries_here_or_children( # This is an error accumulated_errors.append( JDexIssueFileWhereFolderExpected( - PurePath(x), + PurePath(x).relative_to(relative_to), ContentPattern( child.format.name, child.format.raw_format, @@ -1335,10 +1347,13 @@ def _get_jdex_entries_here_or_children( ignored, {**bound_segments, **match.groupdict()}, PurePath(x), + relative_to, child, ) for id, entries in child_entries.items(): - _insert_concat(id, entries, accumulated_entries) + _insert_concat_sorted( + id, entries, accumulated_entries, key=_sort_jdex_entry + ) accumulated_errors.extend(child_errors) break else: @@ -1348,7 +1363,7 @@ def _get_jdex_entries_here_or_children( if note.format.forbidden: accumulated_errors.append( JDexIssueEncounteredForbiddenNote( - PurePath(x), + PurePath(x).relative_to(relative_to), ContentPattern( note.format.name, note.format.raw_format, @@ -1361,7 +1376,7 @@ def _get_jdex_entries_here_or_children( # This is an error accumulated_errors.append( JDexIssueFolderWhereNoteExpected( - PurePath(x), + PurePath(x).relative_to(relative_to), ContentPattern( note.format.name, note.format.raw_format, @@ -1372,15 +1387,16 @@ def _get_jdex_entries_here_or_children( # Create entry for id in note.ids: - _insert_append( + _insert_append_sorted( id.id.build({**bound_segments, **match.groupdict()}), JDexEntry( id.entry.build( {**bound_segments, **match.groupdict()}, ), - PurePath(x), + PurePath(x).relative_to(relative_to), ), accumulated_entries, + key=_sort_jdex_entry, ) break else: @@ -1389,7 +1405,7 @@ def _get_jdex_entries_here_or_children( # This is an error accumulated_errors.append( JDexIssueArbitraryContentWhereNotAllowed( - PurePath(x), + PurePath(x).relative_to(relative_to), tuple( ContentPattern(c.format.name, c.format.raw_format) for c in tier.children @@ -1402,7 +1418,9 @@ def _get_jdex_entries_here_or_children( ) if not has_content: # We have a fully empty JDex folder; it shouldn't exist if it's doing nothing. - accumulated_errors.append(JDexIssueEmptyFolder(PurePath(path))) + accumulated_errors.append( + JDexIssueEmptyFolder(PurePath(path).relative_to(relative_to)) + ) return (accumulated_entries, accumulated_errors) @@ -1414,12 +1432,17 @@ def _process_jdex( ignored + jdex.ignore, {}, jdex.path, + jdex.path, jdex, ) # Check for duplicate ids duplicate_id_errors = [ - JDexIssueDuplicateID(ns[0].path, tuple(n.path for n in ns), id) + JDexIssueDuplicateID( + ns[0].path, + tuple(n.path for n in ns), + id, + ) for id, ns in jdex_entries_by_id.items() if len(ns) != 1 ] @@ -1433,10 +1456,11 @@ def _process_system_level_and_children( ignored: tuple[str], bound_segments: dict[str, str], path: os.PathLike, + relative_to: PurePath, tier: ConfigSystemRoot | ConfigSystemTier, jdex: None | dict[str, list[JDexEntry]], by_id_dict: dict[str, list[tuple[str | None, PurePath]]], -) -> tuple[dict[str, list[SystemFolder]], list[Issue]]: +) -> tuple[dict[str, list[SystemFolder | SystemFile]], list[Issue]]: # Compile regexes for children valid_children = [ (re.compile(c.format.build_regex(bound_segments)), c) for c in tier.children @@ -1459,7 +1483,7 @@ def _process_system_level_and_children( if child.format.forbidden: accumulated_errors.append( IssueEncounteredForbiddenFolder( - PurePath(x), + PurePath(x).relative_to(relative_to), ContentPattern( child.format.name, child.format.raw_format, @@ -1468,40 +1492,53 @@ def _process_system_level_and_children( ) break # Is a valid child folder + child_id = child.id.build({**bound_segments, **match.groupdict()}) if x.is_file(): - if not child.can_be_file: + if child.can_be_file: + _insert_append_sorted( + child_id, + SystemFile( + PurePath(x).relative_to(relative_to), + ), + accumulated_structure, + key=lambda e: e.path, + ) + else: # This is an error accumulated_errors.append( IssueFileWhereFolderExpected( - PurePath(x), + PurePath(x).relative_to(relative_to), ContentPattern( child.format.name, child.format.raw_format, ), ), ) - break - # Walk child - (child_structure, child_errors) = ( - _process_system_level_and_children( - ignored, - {**bound_segments, **match.groupdict()}, - PurePath(x), - child, - jdex, - by_id_dict, + else: + # Walk child + (child_structure, child_errors) = ( + _process_system_level_and_children( + ignored, + {**bound_segments, **match.groupdict()}, + PurePath(x), + relative_to, + child, + jdex, + by_id_dict, + ) ) - ) - - child_id = child.id.build({**bound_segments, **match.groupdict()}) - _insert_append( - child_id, - SystemFolder(PurePath(x), child_structure), - accumulated_structure, - ) + _insert_append_sorted( + child_id, + SystemFolder( + PurePath(x).relative_to(relative_to), child_structure + ), + accumulated_structure, + key=lambda e: e.path, + ) + accumulated_errors.extend(child_errors) if not child.no_jdex_entry: - _insert_append( + _insert_append_sorted( child_id, ( child.jdex_entry.build( @@ -1509,22 +1546,25 @@ def _process_system_level_and_children( ) if child.jdex_entry else x.name, - PurePath(x), + PurePath(x).relative_to(relative_to), ), by_id_dict, + # Sort duplicates by their path, not their JDex entry + key=lambda e: e[1], ) - accumulated_errors.extend(child_errors) break else: # If we got here, it matched no known child/note if not getattr(tier, "allow_arbitrary_contents", False): # If the tier has no children specified, it should be empty if not tier.children: - children_that_should_not_be.append(PurePath(x)) + children_that_should_not_be.append( + PurePath(x).relative_to(relative_to) + ) else: accumulated_errors.append( IssueArbitraryContentWhereNotAllowed( - PurePath(x), + PurePath(x).relative_to(relative_to), tuple( ContentPattern(c.format.name, c.format.raw_format) for c in tier.children @@ -1534,7 +1574,7 @@ def _process_system_level_and_children( if children_that_should_not_be: accumulated_errors.append( IssueFolderShouldBeEmpty( - PurePath(path), + PurePath(path).relative_to(relative_to), tuple(children_that_should_not_be), ), ) @@ -1542,7 +1582,9 @@ def _process_system_level_and_children( tier.children or getattr(tier, "allow_arbitrary_contents", False) ): # We have a fully empty folder; it shouldn't exist if it's doing nothing (unless it should be empty). - accumulated_errors.append(IssueEmptyFolder(PurePath(path))) + accumulated_errors.append( + IssueEmptyFolder(PurePath(path).relative_to(relative_to)) + ) return (accumulated_structure, accumulated_errors) @@ -1551,12 +1593,13 @@ def _process_system_root( root: ConfigSystemRoot, system: ConfigSystem, jdex: None | dict[str, list[JDexEntry]], -) -> tuple[dict[str, list[SystemFolder]], list[Issue]]: +) -> tuple[dict[str, list[SystemFolder | SystemFile]], list[Issue]]: by_id: dict[str, list[tuple[str | None, PurePath]]] = {} (root_structure, root_errors) = _process_system_level_and_children( ignored + root.ignore, {}, root.path, + root.path, root, jdex, by_id, @@ -1712,7 +1755,9 @@ def lint_system(config: Config) -> LintResults: if results.jdex: jdex_errs_by_type: dict[JDexIssueType, list[JDexIssue]] = {} for je in results.jdex.errors if results.jdex else []: - _insert_append(je.type, je, jdex_errs_by_type) + _insert_append_sorted( + je.type, je, jdex_errs_by_type, key=_sort_jdex_error + ) # Print JDex errors if any if jdex_errs_by_type: any_errors = True @@ -1724,7 +1769,7 @@ def lint_system(config: Config) -> LintResults: print( textwrap.indent( "\n".join( - [e.display(results.jdex.path) for e in errs], + [e.display() for e in errs], ), " ", ), @@ -1740,7 +1785,7 @@ def lint_system(config: Config) -> LintResults: if root.errors: errs_by_type = {} for e in root.errors: - _insert_append(e.type, e, errs_by_type) + _insert_append_sorted(e.type, e, errs_by_type, key=_sort_error) print(f"{'':=^80}\n{location + ' Errors Found:':^80}\n{'':=^80}") for errs in errs_by_type.values(): first_err = next(iter(errs)) # Just get the first error @@ -1749,7 +1794,7 @@ def lint_system(config: Config) -> LintResults: print( textwrap.indent( "\n".join( - [e.display(root.path) for e in errs], + [e.display() for e in errs], ), " ", ), diff --git a/run_tests.py b/run_tests.py index f73d1fe..9dd9497 100755 --- a/run_tests.py +++ b/run_tests.py @@ -46,16 +46,7 @@ def tests(self) -> None: # Convert lint results into loaded format actual = json.loads( json.dumps( - { - "errors": { - root_name: root.errors - for root_name, root in results.roots.items() - if root.errors - }, - "jdex_errors": results.jdex.errors - if results.jdex - else [], - }, + results, cls=jdlint._EnhancedJSONEncoder, ) ) diff --git a/tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml b/tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml index 890c8c6..4186f81 100644 --- a/tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml +++ b/tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml @@ -2,7 +2,7 @@ json_output = true [[system.roots]] -name = "JDex" +name = "Files" path = "files" [[system.default.children]] diff --git a/tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/result.json b/tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/result.json index a191615..1a7f10e 100644 --- a/tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/result.json +++ b/tests/ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/result.json @@ -1,75 +1,107 @@ { - "errors": { - "JDex": [ - { - "type": "ARBITRARY_CONTENT_WHERE_NOT_ALLOWED", - "file": "files/1/File", - "possible_formats": [ + "roots": { + "Files": { + "errors": [ + { + "type": "ARBITRARY_CONTENT_WHERE_NOT_ALLOWED", + "file": "1/File", + "possible_formats": [ + { + "name": [ + "A", + "B1" + ], + "format": "/*Name//=A/" + }, + { + "name": [ + "A", + "B2" + ], + "format": "/=A//*Name/" + } + ] + }, + { + "type": "ARBITRARY_CONTENT_WHERE_NOT_ALLOWED", + "file": "1/Folder", + "possible_formats": [ + { + "name": [ + "A", + "B1" + ], + "format": "/*Name//=A/" + }, + { + "name": [ + "A", + "B2" + ], + "format": "/=A//*Name/" + } + ] + }, + { + "type": "ARBITRARY_CONTENT_WHERE_NOT_ALLOWED", + "file": "1/1A/File", + "possible_formats": [ + { + "name": [ + "A", + "B2", + "C2" + ], + "format": "/=A//=Name/" + } + ] + }, + { + "type": "ARBITRARY_CONTENT_WHERE_NOT_ALLOWED", + "file": "1/1A/Folder", + "possible_formats": [ + { + "name": [ + "A", + "B2", + "C2" + ], + "format": "/=A//=Name/" + } + ] + } + ], + "path": "files", + "structure": { + "1": [ { - "name": [ - "A", - "B1" - ], - "format": "/*Name//=A/" - }, - { - "name": [ - "A", - "B2" - ], - "format": "/=A//*Name/" - } - ] - }, - { - "type": "ARBITRARY_CONTENT_WHERE_NOT_ALLOWED", - "file": "files/1/Folder", - "possible_formats": [ - { - "name": [ - "A", - "B1" - ], - "format": "/*Name//=A/" - }, - { - "name": [ - "A", - "B2" - ], - "format": "/=A//*Name/" - } - ] - }, - { - "type": "ARBITRARY_CONTENT_WHERE_NOT_ALLOWED", - "file": "files/1/1A/File", - "possible_formats": [ - { - "name": [ - "A", - "B2", - "C2" - ], - "format": "/=A//=Name/" - } - ] - }, - { - "type": "ARBITRARY_CONTENT_WHERE_NOT_ALLOWED", - "file": "files/1/1A/Folder", - "possible_formats": [ - { - "name": [ - "A", - "B2", - "C2" - ], - "format": "/=A//=Name/" + "path": "1", + "children": { + "1.A": [ + { + "path": "1/1A", + "children": { + "1.A Child": [ + { + "path": "1/1A/1A", + "children": {} + } + ] + } + } + ], + "1.B": [ + { + "path": "1/B1", + "children": {} + } + ] + } } ] } - ] + } }, - "jdex_errors": [] + "jdex": null, + "ignored_errs": 0 } \ No newline at end of file diff --git a/tests/DUPLICATE_ID/jdlint.toml b/tests/DUPLICATE_ID/jdlint.toml index 045500d..6755dd2 100644 --- a/tests/DUPLICATE_ID/jdlint.toml +++ b/tests/DUPLICATE_ID/jdlint.toml @@ -2,7 +2,7 @@ json_output = true [[system.roots]] -name = "JDex" +name = "Files" path = "files" [[system.default.children]] diff --git a/tests/DUPLICATE_ID/result.json b/tests/DUPLICATE_ID/result.json index e4cd213..d7340ac 100644 --- a/tests/DUPLICATE_ID/result.json +++ b/tests/DUPLICATE_ID/result.json @@ -1,27 +1,65 @@ { - "errors": { - "JDex": [ - { - "type": "DUPLICATE_ID", - "file": "files/1", - "files": [ - "files/1", - "files/B1" - ], - "id": "1" - }, - { - "type": "DUPLICATE_ID", - "file": "files/1/11 A", - "files": [ - "files/1/11 A", - "files/1/11 B", - "files/B1/11 A", - "files/B1/11 B" - ], - "id": "1.1" + "roots": { + "Files": { + "errors": [ + { + "type": "DUPLICATE_ID", + "file": "1", + "files": [ + "1", + "B1" + ], + "id": "1" + }, + { + "type": "DUPLICATE_ID", + "file": "1/11 A", + "files": [ + "1/11 A", + "1/11 B", + "B1/11 A", + "B1/11 B" + ], + "id": "1.1" + } + ], + "path": "files", + "structure": { + "1": [ + { + "path": "1", + "children": { + "1.1": [ + { + "path": "1/11 A", + "children": {} + }, + { + "path": "1/11 B", + "children": {} + } + ] + } + }, + { + "path": "B1", + "children": { + "1.1": [ + { + "path": "B1/11 A", + "children": {} + }, + { + "path": "B1/11 B", + "children": {} + } + ] + } + } + ] } - ] + } }, - "jdex_errors": [] + "jdex": null, + "ignored_errs": 0 } \ No newline at end of file diff --git a/tests/EMPTY_FOLDER/jdlint.toml b/tests/EMPTY_FOLDER/jdlint.toml index aa6004d..1cf11a9 100644 --- a/tests/EMPTY_FOLDER/jdlint.toml +++ b/tests/EMPTY_FOLDER/jdlint.toml @@ -3,7 +3,7 @@ json_output = true ignore = [".placeholder"] [[system.roots]] -name = "JDex" +name = "Files" path = "files" [[system.default.children]] diff --git a/tests/EMPTY_FOLDER/result.json b/tests/EMPTY_FOLDER/result.json index 909c596..c0d3f88 100644 --- a/tests/EMPTY_FOLDER/result.json +++ b/tests/EMPTY_FOLDER/result.json @@ -1,11 +1,30 @@ { - "errors": { - "JDex": [ - { - "type": "EMPTY_FOLDER", - "file": "files/1/1A" + "roots": { + "Files": { + "errors": [ + { + "type": "EMPTY_FOLDER", + "file": "1/1A" + } + ], + "path": "files", + "structure": { + "1": [ + { + "path": "1", + "children": { + "1.A": [ + { + "path": "1/1A", + "children": {} + } + ] + } + } + ] } - ] + } }, - "jdex_errors": [] + "jdex": null, + "ignored_errs": 0 } \ No newline at end of file diff --git a/tests/ENCOUNTERED_FORBIDDEN_FOLDER/result.json b/tests/ENCOUNTERED_FORBIDDEN_FOLDER/result.json index 56970ea..7987d4c 100644 --- a/tests/ENCOUNTERED_FORBIDDEN_FOLDER/result.json +++ b/tests/ENCOUNTERED_FORBIDDEN_FOLDER/result.json @@ -1,31 +1,61 @@ { - "errors": { - "Files": [ - { - "type": "ENCOUNTERED_FORBIDDEN_FOLDER", - "matched_pattern": { - "name": [ - "Area", - "Category", - "Bad Inbox" - ], - "format": "/=A//=C/.01 /*Inbox/" + "roots": { + "Files": { + "errors": [ + { + "type": "ENCOUNTERED_FORBIDDEN_FOLDER", + "matched_pattern": { + "name": [ + "Area", + "Category", + "Bad Inbox" + ], + "format": "/=A//=C/.01 /*Inbox/" + }, + "file": "10-19 Life Admin/11 Me, Myself, & I/11.01 Inbxo" }, - "file": "files/10-19 Life Admin/11 Me, Myself, & I/11.01 Inbxo" - }, - { - "type": "ENCOUNTERED_FORBIDDEN_FOLDER", - "matched_pattern": { - "name": [ - "Area", - "Category", - "Bad Header" - ], - "format": "/=A//=C/./#I/0 /*Header/" - }, - "file": "files/10-19 Life Admin/11 Me, Myself, & I/11.10 ■Selves.md" + { + "type": "ENCOUNTERED_FORBIDDEN_FOLDER", + "matched_pattern": { + "name": [ + "Area", + "Category", + "Bad Header" + ], + "format": "/=A//=C/./#I/0 /*Header/" + }, + "file": "10-19 Life Admin/11 Me, Myself, & I/11.10 ■Selves.md" + } + ], + "path": "files", + "structure": { + "10-19": [ + { + "path": "10-19 Life Admin", + "children": { + "11": [ + { + "path": "10-19 Life Admin/11 Me, Myself, & I", + "children": { + "11.11": [ + { + "path": "10-19 Life Admin/11 Me, Myself, & I/11.11 Me.md" + } + ], + "11.12": [ + { + "path": "10-19 Life Admin/11 Me, Myself, & I/11.12 Myself.md" + } + ] + } + } + ] + } + } + ] } - ] + } }, - "jdex_errors": [] + "jdex": null, + "ignored_errs": 0 } \ No newline at end of file diff --git a/tests/FILE_WHERE_FOLDER_EXPECTED/jdlint.toml b/tests/FILE_WHERE_FOLDER_EXPECTED/jdlint.toml index 2a7cc92..7a09ead 100644 --- a/tests/FILE_WHERE_FOLDER_EXPECTED/jdlint.toml +++ b/tests/FILE_WHERE_FOLDER_EXPECTED/jdlint.toml @@ -2,7 +2,7 @@ json_output = true [[system.roots]] -name = "JDex" +name = "Files" path = "files" [[system.default.children]] @@ -18,4 +18,4 @@ id = "/=A/./=Name/" [[system.default.children.children.children]] name = "C" format = "/=A//=Name/" -id = "/=A/./=Name/" +id = "/=A/./=Name/F" diff --git a/tests/FILE_WHERE_FOLDER_EXPECTED/result.json b/tests/FILE_WHERE_FOLDER_EXPECTED/result.json index 69df4c5..b28cd64 100644 --- a/tests/FILE_WHERE_FOLDER_EXPECTED/result.json +++ b/tests/FILE_WHERE_FOLDER_EXPECTED/result.json @@ -1,30 +1,49 @@ { - "errors": { - "JDex": [ - { - "type": "FILE_WHERE_FOLDER_EXPECTED", - "file": "files/1/1B", - "matched_pattern": { - "name": [ - "A", - "B" - ], - "format": "/=A//*Name/" - } - }, - { - "type": "FILE_WHERE_FOLDER_EXPECTED", - "file": "files/1/1A/1A", - "matched_pattern": { - "name": [ - "A", - "B", - "C" - ], - "format": "/=A//=Name/" + "roots": { + "Files": { + "errors": [ + { + "type": "FILE_WHERE_FOLDER_EXPECTED", + "file": "1/1B", + "matched_pattern": { + "name": [ + "A", + "B" + ], + "format": "/=A//*Name/" + } + }, + { + "type": "FILE_WHERE_FOLDER_EXPECTED", + "file": "1/1A/1A", + "matched_pattern": { + "name": [ + "A", + "B", + "C" + ], + "format": "/=A//=Name/" + } } + ], + "path": "files", + "structure": { + "1": [ + { + "path": "1", + "children": { + "1.A": [ + { + "path": "1/1A", + "children": {} + } + ] + } + } + ] } - ] + } }, - "jdex_errors": [] + "jdex": null, + "ignored_errs": 0 } \ No newline at end of file diff --git a/tests/FOLDER_SHOULD_BE_EMPTY/files/10-19 Life Admin/11 Me, Myself, & I/11.12 Myslef/Content b/tests/FOLDER_SHOULD_BE_EMPTY/files/10-19 Life Admin/11 Me, Myself, & I/11.12 Myself/Content similarity index 100% rename from tests/FOLDER_SHOULD_BE_EMPTY/files/10-19 Life Admin/11 Me, Myself, & I/11.12 Myslef/Content rename to tests/FOLDER_SHOULD_BE_EMPTY/files/10-19 Life Admin/11 Me, Myself, & I/11.12 Myself/Content diff --git a/tests/FOLDER_SHOULD_BE_EMPTY/result.json b/tests/FOLDER_SHOULD_BE_EMPTY/result.json index 0229d72..41411d2 100644 --- a/tests/FOLDER_SHOULD_BE_EMPTY/result.json +++ b/tests/FOLDER_SHOULD_BE_EMPTY/result.json @@ -1,21 +1,65 @@ { - "errors": { - "Files": [ - { - "type": "FOLDER_SHOULD_BE_EMPTY", - "file": "files/10-19 Life Admin/11 Me, Myself, & I/11.01 Inbox", - "children": [ - "files/10-19 Life Admin/11 Me, Myself, & I/11.01 Inbox/Content" - ] - }, - { - "type": "FOLDER_SHOULD_BE_EMPTY", - "file": "files/10-19 Life Admin/11 Me, Myself, & I/11.10 ■ Selves", - "children": [ - "files/10-19 Life Admin/11 Me, Myself, & I/11.10 ■ Selves/Content" + "roots": { + "Files": { + "errors": [ + { + "type": "FOLDER_SHOULD_BE_EMPTY", + "file": "10-19 Life Admin/11 Me, Myself, & I/11.01 Inbox", + "children": [ + "10-19 Life Admin/11 Me, Myself, & I/11.01 Inbox/Content" + ] + }, + { + "type": "FOLDER_SHOULD_BE_EMPTY", + "file": "10-19 Life Admin/11 Me, Myself, & I/11.10 ■ Selves", + "children": [ + "10-19 Life Admin/11 Me, Myself, & I/11.10 ■ Selves/Content" + ] + } + ], + "path": "files", + "structure": { + "10-19": [ + { + "path": "10-19 Life Admin", + "children": { + "11": [ + { + "path": "10-19 Life Admin/11 Me, Myself, & I", + "children": { + "11.01": [ + { + "path": "10-19 Life Admin/11 Me, Myself, & I/11.01 Inbox", + "children": {} + } + ], + "11.10": [ + { + "path": "10-19 Life Admin/11 Me, Myself, & I/11.10 ■ Selves", + "children": {} + } + ], + "11.11": [ + { + "path": "10-19 Life Admin/11 Me, Myself, & I/11.11 Me", + "children": {} + } + ], + "11.12": [ + { + "path": "10-19 Life Admin/11 Me, Myself, & I/11.12 Myself", + "children": {} + } + ] + } + } + ] + } + } ] } - ] + } }, - "jdex_errors": [] + "jdex": null, + "ignored_errs": 0 } \ No newline at end of file diff --git a/tests/ID_DIFFERENT_FROM_JDEX/result.json b/tests/ID_DIFFERENT_FROM_JDEX/result.json index 323a853..e28bf46 100644 --- a/tests/ID_DIFFERENT_FROM_JDEX/result.json +++ b/tests/ID_DIFFERENT_FROM_JDEX/result.json @@ -1,19 +1,92 @@ { - "errors": { - "Files": [ - { - "type": "ID_DIFFERENT_FROM_JDEX", - "id": "11.12", - "file": "files/10-19 Life Admin/11 Me, Myself, & I/11.12 Myslef", - "expected_jdex_entry": "11.12 Myslef", - "known_jdex_entries": [ + "roots": { + "Files": { + "errors": [ + { + "type": "ID_DIFFERENT_FROM_JDEX", + "id": "11.12", + "file": "10-19 Life Admin/11 Me, Myself, & I/11.12 Myslef", + "expected_jdex_entry": "11.12 Myslef", + "known_jdex_entries": [ + { + "entry": "11.12 Myself", + "path": "10-19 Life Admin/11 Me, Myself, & I/11.12 Myself.md" + } + ] + } + ], + "path": "files", + "structure": { + "10-19": [ { - "entry": "11.12 Myself", - "path": "jdex/10-19 Life Admin/11 Me, Myself, & I/11.12 Myself.md" + "path": "10-19 Life Admin", + "children": { + "11": [ + { + "path": "10-19 Life Admin/11 Me, Myself, & I", + "children": { + "11.11": [ + { + "path": "10-19 Life Admin/11 Me, Myself, & I/11.11 Me", + "children": {} + } + ], + "11.12": [ + { + "path": "10-19 Life Admin/11 Me, Myself, & I/11.12 Myslef", + "children": {} + } + ] + } + } + ] + } } ] } - ] + } }, - "jdex_errors": [] + "ignored_errs": 0, + "jdex": { + "errors": [], + "path": "jdex", + "entries": { + "10-19": [ + { + "entry": "10-19 Life Admin", + "path": "10-19 Life Admin/10 Management of area 10-19/10.00 Life Admin.md" + } + ], + "10.00": [ + { + "entry": "10.00 Life Admin", + "path": "10-19 Life Admin/10 Management of area 10-19/10.00 Life Admin.md" + } + ], + "11": [ + { + "entry": "11 Me, Myself, & I", + "path": "10-19 Life Admin/11 Me, Myself, & I/11.00 Me, Myself, & I.md" + } + ], + "11.00": [ + { + "entry": "11.00 Me, Myself, & I", + "path": "10-19 Life Admin/11 Me, Myself, & I/11.00 Me, Myself, & I.md" + } + ], + "11.11": [ + { + "entry": "11.11 Me", + "path": "10-19 Life Admin/11 Me, Myself, & I/11.11 Me.md" + } + ], + "11.12": [ + { + "entry": "11.12 Myself", + "path": "10-19 Life Admin/11 Me, Myself, & I/11.12 Myself.md" + } + ] + } + } } \ No newline at end of file diff --git a/tests/ID_NOT_IN_JDEX/result.json b/tests/ID_NOT_IN_JDEX/result.json index af5d922..9250445 100644 --- a/tests/ID_NOT_IN_JDEX/result.json +++ b/tests/ID_NOT_IN_JDEX/result.json @@ -1,12 +1,91 @@ { - "errors": { - "Files": [ - { - "type": "ID_NOT_IN_JDEX", - "id": "11.13", - "file": "files/10-19 Life Admin/11 Me, Myself, & I/11.13 I" + "roots": { + "Files": { + "errors": [ + { + "type": "ID_NOT_IN_JDEX", + "id": "11.13", + "file": "10-19 Life Admin/11 Me, Myself, & I/11.13 I" + } + ], + "path": "files", + "structure": { + "10-19": [ + { + "path": "10-19 Life Admin", + "children": { + "11": [ + { + "path": "10-19 Life Admin/11 Me, Myself, & I", + "children": { + "11.11": [ + { + "path": "10-19 Life Admin/11 Me, Myself, & I/11.11 Me", + "children": {} + } + ], + "11.12": [ + { + "path": "10-19 Life Admin/11 Me, Myself, & I/11.12 Myself", + "children": {} + } + ], + "11.13": [ + { + "path": "10-19 Life Admin/11 Me, Myself, & I/11.13 I", + "children": {} + } + ] + } + } + ] + } + } + ] } - ] + } }, - "jdex_errors": [] + "ignored_errs": 0, + "jdex": { + "errors": [], + "path": "jdex", + "entries": { + "10-19": [ + { + "entry": "10-19 Life Admin", + "path": "10-19 Life Admin/10 Management of area 10-19/10.00 Life Admin.md" + } + ], + "10.00": [ + { + "entry": "10.00 Life Admin", + "path": "10-19 Life Admin/10 Management of area 10-19/10.00 Life Admin.md" + } + ], + "11": [ + { + "entry": "11 Me, Myself, & I", + "path": "10-19 Life Admin/11 Me, Myself, & I/11.00 Me, Myself, & I.md" + } + ], + "11.00": [ + { + "entry": "11.00 Me, Myself, & I", + "path": "10-19 Life Admin/11 Me, Myself, & I/11.00 Me, Myself, & I.md" + } + ], + "11.11": [ + { + "entry": "11.11 Me", + "path": "10-19 Life Admin/11 Me, Myself, & I/11.11 Me.md" + } + ], + "11.12": [ + { + "entry": "11.12 Myself", + "path": "10-19 Life Admin/11 Me, Myself, & I/11.12 Myself.md" + } + ] + } + } } \ No newline at end of file diff --git a/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml b/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml index c8eedac..4b85e08 100644 --- a/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml +++ b/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/jdlint.toml @@ -2,7 +2,7 @@ json_output = true [[system.roots]] -name = "JDex" +name = "Files" path = "files" [system.jdex] diff --git a/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/result.json b/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/result.json index 55e6539..6b59bd0 100644 --- a/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/result.json +++ b/tests/JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED/result.json @@ -1,89 +1,126 @@ { - "errors": {}, - "jdex_errors": [ - { - "type": "JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED", - "file": "files/1/File", - "possible_formats": [ - { - "name": [ - "A", - "B1" - ], - "format": "/*Name//=A/" - }, - { - "name": [ - "A", - "B2" - ], - "format": "/=A//*Name/" - } - ] - }, - { - "type": "JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED", - "file": "files/1/Folder", - "possible_formats": [ - { - "name": [ - "A", - "B1" - ], - "format": "/*Name//=A/" - }, + "roots": { + "Files": { + "errors": [], + "path": "files", + "structure": { + "1": [ + { + "path": "1", + "children": {} + } + ] + } + } + }, + "jdex": { + "errors": [ + { + "type": "JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED", + "file": "1/File", + "possible_formats": [ + { + "name": [ + "A", + "B1" + ], + "format": "/*Name//=A/" + }, + { + "name": [ + "A", + "B2" + ], + "format": "/=A//*Name/" + } + ] + }, + { + "type": "JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED", + "file": "1/Folder", + "possible_formats": [ + { + "name": [ + "A", + "B1" + ], + "format": "/*Name//=A/" + }, + { + "name": [ + "A", + "B2" + ], + "format": "/=A//*Name/" + } + ] + }, + { + "type": "JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED", + "file": "1/1A/File", + "possible_formats": [ + { + "name": [ + "A", + "B2", + "C3" + ], + "format": "/=A/.md" + }, + { + "name": [ + "A", + "B2", + "C2" + ], + "format": "/=A//=Name/.md" + } + ] + }, + { + "type": "JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED", + "file": "1/1A/Folder", + "possible_formats": [ + { + "name": [ + "A", + "B2", + "C3" + ], + "format": "/=A/.md" + }, + { + "name": [ + "A", + "B2", + "C2" + ], + "format": "/=A//=Name/.md" + } + ] + } + ], + "path": "files", + "entries": { + "1": [ { - "name": [ - "A", - "B2" - ], - "format": "/=A//*Name/" + "entry": "1", + "path": "1/1A/1.md" } - ] - }, - { - "type": "JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED", - "file": "files/1/1A/File", - "possible_formats": [ - { - "name": [ - "A", - "B2", - "C3" - ], - "format": "/=A/.md" - }, + ], + "1.A": [ { - "name": [ - "A", - "B2", - "C2" - ], - "format": "/=A//=Name/.md" + "entry": "1.A", + "path": "1/1A/1A.md" } - ] - }, - { - "type": "JDEX_ARBITRARY_CONTENT_WHERE_NOT_ALLOWED", - "file": "files/1/1A/Folder", - "possible_formats": [ - { - "name": [ - "A", - "B2", - "C3" - ], - "format": "/=A/.md" - }, + ], + "1.B": [ { - "name": [ - "A", - "B2", - "C2" - ], - "format": "/=A//=Name/.md" + "entry": "1.B", + "path": "1/B1/X.md" } ] } - ] + }, + "ignored_errs": 0 } \ No newline at end of file diff --git a/tests/JDEX_DUPLICATE_ID/jdlint.toml b/tests/JDEX_DUPLICATE_ID/jdlint.toml index c92e7b7..777d85a 100644 --- a/tests/JDEX_DUPLICATE_ID/jdlint.toml +++ b/tests/JDEX_DUPLICATE_ID/jdlint.toml @@ -2,7 +2,7 @@ json_output = true [[system.roots]] -name = "JDex" +name = "Files" path = "files" [system.jdex] diff --git a/tests/JDEX_DUPLICATE_ID/result.json b/tests/JDEX_DUPLICATE_ID/result.json index 73469a5..6c8d5da 100644 --- a/tests/JDEX_DUPLICATE_ID/result.json +++ b/tests/JDEX_DUPLICATE_ID/result.json @@ -1,14 +1,49 @@ { - "errors": {}, - "jdex_errors": [ - { - "type": "JDEX_DUPLICATE_ID", - "file": "files/1/11 A.md", - "files": [ - "files/1/11 A.md", - "files/1/11 B.md" + "roots": { + "Files": { + "errors": [], + "path": "files", + "structure": { + "1": [ + { + "path": "1", + "children": {} + } + ] + } + } + }, + "jdex": { + "errors": [ + { + "type": "JDEX_DUPLICATE_ID", + "file": "1/11 A.md", + "files": [ + "1/11 A.md", + "1/11 B.md" + ], + "id": "1.1" + } + ], + "path": "files", + "entries": { + "1": [ + { + "entry": "1", + "path": "1/1.md" + } ], - "id": "1.1" + "1.1": [ + { + "entry": "1.1 A", + "path": "1/11 A.md" + }, + { + "entry": "1.1 B", + "path": "1/11 B.md" + } + ] } - ] + }, + "ignored_errs": 0 } \ No newline at end of file diff --git a/tests/JDEX_EMPTY_FOLDER/jdlint.toml b/tests/JDEX_EMPTY_FOLDER/jdlint.toml index e84e8cb..1787edd 100644 --- a/tests/JDEX_EMPTY_FOLDER/jdlint.toml +++ b/tests/JDEX_EMPTY_FOLDER/jdlint.toml @@ -3,7 +3,7 @@ json_output = true ignore = [".placeholder"] [[system.roots]] -name = "JDex" +name = "Files" path = "files" [system.jdex] diff --git a/tests/JDEX_EMPTY_FOLDER/result.json b/tests/JDEX_EMPTY_FOLDER/result.json index 7e27b22..15055ad 100644 --- a/tests/JDEX_EMPTY_FOLDER/result.json +++ b/tests/JDEX_EMPTY_FOLDER/result.json @@ -1,9 +1,34 @@ { - "errors": {}, - "jdex_errors": [ - { - "type": "JDEX_EMPTY_FOLDER", - "file": "files/1/1A" + "roots": { + "Files": { + "errors": [], + "path": "files", + "structure": { + "1": [ + { + "path": "1", + "children": {} + } + ] + } } - ] + }, + "jdex": { + "errors": [ + { + "type": "JDEX_EMPTY_FOLDER", + "file": "1/1A" + } + ], + "path": "files", + "entries": { + "1": [ + { + "entry": "1", + "path": "1/1B/X.md" + } + ] + } + }, + "ignored_errs": 0 } \ No newline at end of file diff --git a/tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/result.json b/tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/result.json index 0ae1e23..a6d3f24 100644 --- a/tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/result.json +++ b/tests/JDEX_ENCOUNTERED_FORBIDDEN_FOLDER/result.json @@ -1,16 +1,91 @@ { - "errors": {}, - "jdex_errors": [ - { - "type": "JDEX_ENCOUNTERED_FORBIDDEN_FOLDER", - "matched_pattern": { - "name": [ - "JDex Area Folder", - "JDex Bad Area Management Folder" - ], - "format": "/=A/0 /*AreaManagement/" - }, - "file": "jdex/10-19 Life Admin/10 Managemnt of area 10-19" + "roots": { + "Files": { + "errors": [], + "path": "files", + "structure": { + "10-19": [ + { + "path": "10-19 Life Admin", + "children": { + "11": [ + { + "path": "10-19 Life Admin/11 Me, Myself, & I", + "children": { + "11.11": [ + { + "path": "10-19 Life Admin/11 Me, Myself, & I/11.11 Me", + "children": {} + } + ], + "11.12": [ + { + "path": "10-19 Life Admin/11 Me, Myself, & I/11.12 Myself", + "children": {} + } + ] + } + } + ] + } + } + ] + } } - ] + }, + "ignored_errs": 0, + "jdex": { + "errors": [ + { + "type": "JDEX_ENCOUNTERED_FORBIDDEN_FOLDER", + "matched_pattern": { + "name": [ + "JDex Area Folder", + "JDex Bad Area Management Folder" + ], + "format": "/=A/0 /*AreaManagement/" + }, + "file": "10-19 Life Admin/10 Managemnt of area 10-19" + } + ], + "path": "jdex", + "entries": { + "10-19": [ + { + "entry": "10-19 Life Admin", + "path": "10-19 Life Admin/10 Management of area 10-19/10.00 Life Admin.md" + } + ], + "10.00": [ + { + "entry": "10.00 Life Admin", + "path": "10-19 Life Admin/10 Management of area 10-19/10.00 Life Admin.md" + } + ], + "11": [ + { + "entry": "11 Me, Myself, & I", + "path": "10-19 Life Admin/11 Me, Myself, & I/11.00 Me, Myself, & I.md" + } + ], + "11.00": [ + { + "entry": "11.00 Me, Myself, & I", + "path": "10-19 Life Admin/11 Me, Myself, & I/11.00 Me, Myself, & I.md" + } + ], + "11.11": [ + { + "entry": "11.11 Me", + "path": "10-19 Life Admin/11 Me, Myself, & I/11.11 Me.md" + } + ], + "11.12": [ + { + "entry": "11.12 Myself", + "path": "10-19 Life Admin/11 Me, Myself, & I/11.12 Myself.md" + } + ] + } + } } \ No newline at end of file diff --git a/tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/result.json b/tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/result.json index 713deef..6de6d00 100644 --- a/tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/result.json +++ b/tests/JDEX_ENCOUNTERED_FORBIDDEN_NOTE/result.json @@ -1,17 +1,92 @@ { - "errors": {}, - "jdex_errors": [ - { - "type": "JDEX_ENCOUNTERED_FORBIDDEN_NOTE", - "matched_pattern": { - "name": [ - "JDex Area Folder", - "JDex Category Folder", - "JDex Bad Inbox" - ], - "format": "/=A//=C/.01 /*Name/" - }, - "file": "jdex/10-19 Life Admin/11 Me, Myself, & I/11.01 Inbxo.md" + "roots": { + "Files": { + "errors": [], + "path": "files", + "structure": { + "10-19": [ + { + "path": "10-19 Life Admin", + "children": { + "11": [ + { + "path": "10-19 Life Admin/11 Me, Myself, & I", + "children": { + "11.11": [ + { + "path": "10-19 Life Admin/11 Me, Myself, & I/11.11 Me", + "children": {} + } + ], + "11.12": [ + { + "path": "10-19 Life Admin/11 Me, Myself, & I/11.12 Myself", + "children": {} + } + ] + } + } + ] + } + } + ] + } } - ] + }, + "ignored_errs": 0, + "jdex": { + "errors": [ + { + "type": "JDEX_ENCOUNTERED_FORBIDDEN_NOTE", + "matched_pattern": { + "name": [ + "JDex Area Folder", + "JDex Category Folder", + "JDex Bad Inbox" + ], + "format": "/=A//=C/.01 /*Name/" + }, + "file": "10-19 Life Admin/11 Me, Myself, & I/11.01 Inbxo.md" + } + ], + "path": "jdex", + "entries": { + "10-19": [ + { + "entry": "10-19 Life Admin", + "path": "10-19 Life Admin/10 Management of area 10-19/10.00 Life Admin.md" + } + ], + "10.00": [ + { + "entry": "10.00 Life Admin", + "path": "10-19 Life Admin/10 Management of area 10-19/10.00 Life Admin.md" + } + ], + "11": [ + { + "entry": "11 Me, Myself, & I", + "path": "10-19 Life Admin/11 Me, Myself, & I/11.00 Me, Myself, & I.md" + } + ], + "11.00": [ + { + "entry": "11.00 Me, Myself, & I", + "path": "10-19 Life Admin/11 Me, Myself, & I/11.00 Me, Myself, & I.md" + } + ], + "11.11": [ + { + "entry": "11.11 Me", + "path": "10-19 Life Admin/11 Me, Myself, & I/11.11 Me.md" + } + ], + "11.12": [ + { + "entry": "11.12 Myself", + "path": "10-19 Life Admin/11 Me, Myself, & I/11.12 Myself.md" + } + ] + } + } } \ No newline at end of file diff --git a/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/jdlint.toml b/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/jdlint.toml index 7ce621b..e0f6317 100644 --- a/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/jdlint.toml +++ b/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/jdlint.toml @@ -2,7 +2,7 @@ json_output = true [[system.roots]] -name = "JDex" +name = "Files" path = "files" [system.jdex] diff --git a/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/result.json b/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/result.json index 594f93b..533e012 100644 --- a/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/result.json +++ b/tests/JDEX_FILE_WHERE_FOLDER_EXPECTED/result.json @@ -1,16 +1,47 @@ { - "errors": {}, - "jdex_errors": [ - { - "type": "JDEX_FILE_WHERE_FOLDER_EXPECTED", - "file": "files/1/1B", - "matched_pattern": { - "name": [ - "A", - "B" - ], - "format": "/=A//*Name/" + "roots": { + "Files": { + "errors": [], + "path": "files", + "structure": { + "1": [ + { + "path": "1", + "children": {} + } + ] } } - ] + }, + "jdex": { + "errors": [ + { + "type": "JDEX_FILE_WHERE_FOLDER_EXPECTED", + "file": "1/1B", + "matched_pattern": { + "name": [ + "A", + "B" + ], + "format": "/=A//*Name/" + } + } + ], + "path": "files", + "entries": { + "1": [ + { + "entry": "1", + "path": "1/1A/1.md" + } + ], + "1.A": [ + { + "entry": "1.A", + "path": "1/1A/1A.md" + } + ] + } + }, + "ignored_errs": 0 } \ No newline at end of file diff --git a/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/jdlint.toml b/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/jdlint.toml index bed73d7..07f2843 100644 --- a/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/jdlint.toml +++ b/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/jdlint.toml @@ -2,7 +2,7 @@ json_output = true [[system.roots]] -name = "JDex" +name = "Files" path = "files" [system.jdex] diff --git a/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/result.json b/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/result.json index 62f7d4d..db8e832 100644 --- a/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/result.json +++ b/tests/JDEX_FOLDER_WHERE_NOTE_EXPECTED/result.json @@ -1,17 +1,48 @@ { - "errors": {}, - "jdex_errors": [ - { - "type": "JDEX_FOLDER_WHERE_NOTE_EXPECTED", - "file": "files/1/1B/1B.md", - "matched_pattern": { - "name": [ - "A", - "B", - "JDex Note" - ], - "format": "/=A//=Name/.md" + "roots": { + "Files": { + "errors": [], + "path": "files", + "structure": { + "1": [ + { + "path": "1", + "children": {} + } + ] } } - ] + }, + "jdex": { + "errors": [ + { + "type": "JDEX_FOLDER_WHERE_NOTE_EXPECTED", + "file": "1/1B/1B.md", + "matched_pattern": { + "name": [ + "A", + "B", + "JDex Note" + ], + "format": "/=A//=Name/.md" + } + } + ], + "path": "files", + "entries": { + "1": [ + { + "entry": "1", + "path": "1/1A/1.md" + } + ], + "1.A": [ + { + "entry": "1.A", + "path": "1/1A/1A.md" + } + ] + } + }, + "ignored_errs": 0 } \ No newline at end of file diff --git a/tests/can_be_file/jdlint.toml b/tests/can_be_file/jdlint.toml index 9cde5f6..fc9c740 100644 --- a/tests/can_be_file/jdlint.toml +++ b/tests/can_be_file/jdlint.toml @@ -2,7 +2,7 @@ json_output = true [[system.roots]] -name = "JDex" +name = "Files" path = "files" [[system.default.children]] diff --git a/tests/can_be_file/result.json b/tests/can_be_file/result.json index da0ee54..8677165 100644 --- a/tests/can_be_file/result.json +++ b/tests/can_be_file/result.json @@ -1,4 +1,41 @@ { - "errors": {}, - "jdex_errors": [] + "roots": { + "Files": { + "errors": [ + { + "type": "DUPLICATE_ID", + "file": "1/1A", + "files": [ + "1/1A", + "1/1A/1A" + ], + "id": "1.A" + } + ], + "path": "files", + "structure": { + "1": [ + { + "path": "1", + "children": { + "1.A": [ + { + "path": "1/1A", + "children": { + "1.A": [ + { + "path": "1/1A/1A" + } + ] + } + } + ] + } + } + ] + } + } + }, + "jdex": null, + "ignored_errs": 0 } \ No newline at end of file diff --git a/tests/jdex_entry/result.json b/tests/jdex_entry/result.json index da0ee54..7e7f126 100644 --- a/tests/jdex_entry/result.json +++ b/tests/jdex_entry/result.json @@ -1,4 +1,85 @@ { - "errors": {}, - "jdex_errors": [] + "jdex": { + "errors": [], + "path": "jdex", + "entries": { + "10-19": [ + { + "entry": "10-19 Life Admin", + "path": "10-19 Life Admin/10 Management of area 10-19/10.00 Life Admin.md" + } + ], + "10.00": [ + { + "entry": "10.00 Life Admin", + "path": "10-19 Life Admin/10 Management of area 10-19/10.00 Life Admin.md" + } + ], + "11": [ + { + "entry": "11 Me, Myself, & I", + "path": "10-19 Life Admin/11 Me, Myself, & I/11.00 Me, Myself, & I.md" + } + ], + "11.00": [ + { + "entry": "11.00 Me, Myself, & I", + "path": "10-19 Life Admin/11 Me, Myself, & I/11.00 Me, Myself, & I.md" + } + ], + "11.11": [ + { + "entry": "11.11 Me", + "path": "10-19 Life Admin/11 Me, Myself, & I/11.11 Me.md" + } + ], + "11.12": [ + { + "entry": "11.12 Myself", + "path": "10-19 Life Admin/11 Me, Myself, & I/11.12 Myself.md" + } + ] + } + }, + "roots": { + "Files": { + "errors": [], + "path": "files", + "structure": { + "10-19": [ + { + "path": "10-19 Life Admin", + "children": { + "10": [ + { + "path": "10-19 Life Admin/10 No JDex", + "children": {} + } + ], + "11": [ + { + "path": "10-19 Life Admin/11 Me, Myself, & I", + "children": { + "11.11": [ + { + "path": "10-19 Life Admin/11 Me, Myself, & I/11.11 Me", + "children": {} + } + ], + "11.12": [ + { + "path": "10-19 Life Admin/11 Me, Myself, & I/11.12 Myslef", + "children": {} + } + ] + } + } + ] + } + } + ] + } + } + }, + "ignored_errs": 0 } \ No newline at end of file From 66ffbe2775683d6340575efb73408c70bcb58c42 Mon Sep 17 00:00:00 2001 From: SiriusStarr <2049163+SiriusStarr@users.noreply.github.com> Date: Sun, 26 Jul 2026 14:17:13 -0700 Subject: [PATCH 18/23] Begin cleaning up code --- jdlint.py | 248 +++++++++++++++++++++++++++++---------------------- run_tests.py | 13 ++- 2 files changed, 149 insertions(+), 112 deletions(-) diff --git a/jdlint.py b/jdlint.py index ea714a4..bf87dc6 100755 --- a/jdlint.py +++ b/jdlint.py @@ -82,6 +82,11 @@ def __init__(self, key: str, issue: str) -> None: ############################################################################### # Config ############################################################################### +def _report_extra_keys(at: str, from_file: dict, valid: tuple[str, ...]) -> None: + # Ensure no extra fields + for key in from_file: + err = ConfigExtraKeyError(f"{at}.{key}", valid) + raise err class ConfigSystemRoot: @@ -96,36 +101,42 @@ def __init__( """Create a valid configuration given a loaded section of a config file.""" # Acquire and set defaults if "name" not in from_file: - raise (ConfigMissingKeyError(f"{at}.name")) + err = ConfigMissingKeyError(f"{at}.name") + raise err self.name = from_file.pop("name") if "path" not in from_file: - raise (ConfigMissingKeyError(f"{at}.path")) + err = ConfigMissingKeyError(f"{at}.path") + raise err self.path = Path(from_file.pop("path")).expanduser() self.ignore = from_file.pop("ignore", []) if not isinstance(self.name, str): - raise ConfigTypeError( + err = ConfigTypeError( f"{at}.name", "str", type(self.name).__name__, ) + raise err # Validate path is good if not self.path.is_dir(): - raise ConfigValueError( + err = ConfigValueError( f"{at}.path", "Root path isn't a folder that exists!", str(self.path), ) + raise err if not isinstance(self.ignore, list): - raise ConfigTypeError( + err = ConfigTypeError( f"{at}.ignore", "list", type(self.ignore).__name__, ) + raise err for r in self.ignore: if not isinstance(r, str): - raise ConfigTypeError(f"{at}.ignore", "str", type(r).__name__) + err = ConfigTypeError(f"{at}.ignore", "str", type(r).__name__) + raise err # Load specialized structure, if any if "children" in from_file: @@ -140,16 +151,13 @@ def __init__( elif default_structure: self.children = default_structure else: - raise ( - ConfigConflictError( - f"{at}", - "Either system.default.children must be specified or every root must specify its own children.", - ) + err = ConfigConflictError( + f"{at}", + "Either system.default.children must be specified or every root must specify its own children.", ) + raise err - # Ensure no extra fields - for key in from_file: - raise ConfigExtraKeyError(f"{at}.{key}", tuple(self.__dict__.keys())) + _report_extra_keys(at, from_file, tuple(self.__dict__.keys())) class ConfigSystemJDex: @@ -159,26 +167,30 @@ def __init__(self, at: str, from_file: dict) -> None: """Create a valid configuration given a loaded section of a config file.""" # Acquire and set defaults if "path" not in from_file: - raise (ConfigMissingKeyError(f"{at}.path")) + err = ConfigMissingKeyError(f"{at}.path") + raise err self.path = Path(from_file.pop("path")).expanduser() self.ignore = from_file.pop("ignore", []) # Validate if not self.path.is_dir(): - raise ConfigValueError( + err = ConfigValueError( f"{at}.path", "JDex path isn't a folder that exists!", str(self.path), ) + raise err if not isinstance(self.ignore, list): - raise ConfigTypeError( + err = ConfigTypeError( f"{at}.ignore", "list", type(self.ignore).__name__, ) + raise err for r in self.ignore: if not isinstance(r, str): - raise ConfigTypeError(f"{at}.ignore", "str", type(r).__name__) + err = ConfigTypeError(f"{at}.ignore", "str", type(r).__name__) + raise err self.children = [ ConfigJDexTier( @@ -197,9 +209,7 @@ def __init__(self, at: str, from_file: dict) -> None: for i, v in enumerate(from_file.pop("notes", [])) ] - # Ensure no extra fields - for key in from_file: - raise ConfigExtraKeyError(f"{at}.{key}", tuple(self.__dict__.keys())) + _report_extra_keys(at, from_file, tuple(self.__dict__.keys())) class ConfigLinter: @@ -214,36 +224,39 @@ def __init__(self, from_file: dict) -> None: # Validate if not isinstance(self.disable_rules, list): - raise ConfigTypeError( + err = ConfigTypeError( "linter.disable_rules", "list", type(self.disable_rules).__name__, ) + raise err for r in self.disable_rules: if r in [e.type for e in typing.get_args(AnyIssueType)]: continue - raise ConfigValueError("linter.disable_rules", "not a valid rule name", r) + err = ConfigValueError("linter.disable_rules", "not a valid rule name", r) + raise err if not isinstance(self.json_output, bool): - raise ConfigTypeError( + err = ConfigTypeError( "linter.json_output", "bool", type(self.json_output).__name__, ) + raise err if not isinstance(self.ignore, list): - raise ConfigTypeError( + err = ConfigTypeError( "linter.ignore", "list", type(self.ignore).__name__, ) + raise err for r in self.ignore: if not isinstance(r, str): - raise ConfigTypeError("linter.ignore", "str", type(r).__name__) + err = ConfigTypeError("linter.ignore", "str", type(r).__name__) + raise err - # Ensure no extra fields - for key in from_file: - raise ConfigExtraKeyError(f"linter.{key}", tuple(self.__dict__.keys())) + _report_extra_keys("linter", from_file, tuple(self.__dict__.keys())) class ConfigSystem: @@ -273,24 +286,24 @@ def __init__(self, from_file: dict) -> None: accum_paths = {} for root in self.roots: if root.name in accum_names: - raise ConfigConflictError( + err = ConfigConflictError( "system.roots", f"System root names must be unique. {root.name} occurs multiple times.", ) + raise err if root.path in accum_paths: - raise ConfigConflictError( + err = ConfigConflictError( "system.roots", f"System root paths must be unique. {root.path} occurs multiple times.", ) + raise err if "jdex" in from_file: self.jdex = ConfigSystemJDex("system.jdex", from_file.pop("jdex")) else: self.jdex = None - # Ensure no extra fields - for key in from_file: - raise ConfigExtraKeyError(f"system.{key}", tuple(self.__dict__.keys())) + _report_extra_keys("system", from_file, tuple(self.__dict__.keys())) class ConfigStaticFormat: @@ -367,23 +380,23 @@ def __init__( """Create a valid note format given a loaded section of a config file.""" # Compile Format if "id" not in from_file: - raise (ConfigMissingKeyError(f"{at}.id")) + err = ConfigMissingKeyError(f"{at}.id") + raise err self.id = ConfigStaticFormat( f"{at}.id", ancestors, from_file.pop("id"), ) if "entry" not in from_file: - raise (ConfigMissingKeyError(f"{at}.entry")) + err = ConfigMissingKeyError(f"{at}.entry") + raise err self.entry = ConfigStaticFormat( f"{at}.entry", ancestors, from_file.pop("entry"), ) - # Ensure no extra fields - for key in from_file: - raise ConfigExtraKeyError(f"{at}.{key}", tuple(self.__dict__.keys())) + _report_extra_keys(at, from_file, tuple(self.__dict__.keys())) class ConfigJDexNotes: @@ -398,14 +411,16 @@ def __init__( """Create a valid note format given a loaded section of a config file.""" # Compile Format if "format" not in from_file: - raise (ConfigMissingKeyError(f"{at}.format")) + err = ConfigMissingKeyError(f"{at}.format") + raise err self.format = ConfigFormat( f"{at}", ancestors, from_file, ) if "ids" not in from_file and not self.format.forbidden: - raise (ConfigMissingKeyError(f"{at}.ids")) + err = ConfigMissingKeyError(f"{at}.ids") + raise err self.ids = [ ConfigJDexID( f"{at}.ids[{i}]", @@ -423,9 +438,7 @@ def __init__( else: self.jdex_entry = None - # Ensure no extra fields - for key in from_file: - raise ConfigExtraKeyError(f"{at}.{key}", tuple(self.__dict__.keys())) + _report_extra_keys(at, from_file, tuple(self.__dict__.keys())) class ConfigFolderTier: @@ -444,11 +457,12 @@ def __init__( # Validate if not isinstance(self.allow_arbitrary_contents, bool): - raise ConfigTypeError( + err = ConfigTypeError( f"{at}.allow_arbitrary_contents", "bool", type(self.allow_arbitrary_contents).__name__, ) + raise err # Compile Format & Children self.format = ConfigFormat( @@ -509,7 +523,8 @@ def __init__( self.jdex_entry = None if "id" not in from_file: - raise (ConfigMissingKeyError(f"{at}.id")) + err = ConfigMissingKeyError(f"{at}.id") + raise err self.id = ConfigStaticFormat( f"{at}.id", self.format, @@ -517,32 +532,34 @@ def __init__( ) if not isinstance(self.can_be_file, bool): - raise ConfigTypeError( + err = ConfigTypeError( f"{at}.can_be_file", "bool", type(self.can_be_file).__name__, ) + raise err if self.children and self.can_be_file: - raise ConfigConflictError( + err = ConfigConflictError( at, "If children are specified, can_be_file must be false.", ) + raise err if self.format.forbidden and self.can_be_file: - raise ConfigConflictError( + err = ConfigConflictError( at, "If forbidden, can_be_file must be false.", ) + raise err if self.no_jdex_entry and self.jdex_entry: - raise ConfigConflictError( + err = ConfigConflictError( at, "Only one of no_jdex_entry and jdex_entry may be set.", ) + raise err - # Ensure no extra fields - for key in from_file: - raise ConfigExtraKeyError(f"{at}.{key}", tuple(self.__dict__.keys())) + _report_extra_keys(at, from_file, tuple(self.__dict__.keys())) class ConfigJDexTier(ConfigFolderTier): @@ -573,13 +590,13 @@ def __init__( "If forbidden, notes cannot be speciefed.", ) - # Ensure no extra fields - for key in from_file: - raise ConfigExtraKeyError(f"{at}.{key}", tuple(self.__dict__.keys())) + _report_extra_keys(at, from_file, tuple(self.__dict__.keys())) @dataclass class ConfigFormatAncestorInfo: + """Information about the ancestors of a format.""" + name: tuple[str, ...] segments: tuple[str, ...] @@ -598,52 +615,60 @@ def __init__( ) -> None: """Create a valid format given a string from a config file.""" if "format" not in from_file: - raise (ConfigMissingKeyError(f"{at}.format")) + err = ConfigMissingKeyError(f"{at}.format") + raise err self.raw_format = from_file.pop("format") self.forbidden = from_file.pop("forbidden", False) if "name" not in from_file: - raise (ConfigMissingKeyError(f"{at}.name")) + err = ConfigMissingKeyError(f"{at}.name") + raise err # Validate if not isinstance(from_file["name"], str): - raise ConfigTypeError( + err = ConfigTypeError( f"{at}.name", "str", type(from_file["name"]).__name__, ) + raise err if not isinstance(self.raw_format, str): - raise ConfigTypeError( + err = ConfigTypeError( f"{at}.format", "str", type(self.raw_format).__name__, ) + raise err if not isinstance(self.forbidden, bool): - raise ConfigTypeError( + err = ConfigTypeError( f"{at}.forbidden", "bool", type(self.forbidden).__name__, ) + raise err if from_file["name"] == "": - raise ConfigValueError( + err = ConfigValueError( f"{at}.name", "Malformed name; must not be empty.", str(from_file), ) + raise err if self.raw_format.count("/") % 2 != 0: - raise ConfigValueError( + err = ConfigValueError( f"{at}.format", "Malformed format; there must be an even number of / characters. You have an extra one/are missing one.", str(from_file), ) + raise err if self.raw_format == "": - raise ConfigValueError( + err = ConfigValueError( f"{at}.format", "Malformed format; must not be empty.", str(from_file), ) + raise err regex = [] new_segments = [] @@ -656,11 +681,12 @@ def __init__( # Variable segment match = ConfigFormat.variable_segment_re.fullmatch(v) if not match: - raise ConfigValueError( + err = ConfigValueError( f"{at}.format", "Malformed format; variable segment must consist of =, *, or one or more # followed by an alphabetic identifier.", v, ) + raise err if match.group(1) == "=": if match.group(2) in ancestors.segments: p = match.group(2) @@ -672,24 +698,27 @@ def __init__( ) else: - raise ConfigValueError( + err = ConfigValueError( f"{at}.format", "Malformed format; variable segment referenced an identifier never bound.", v, ) + raise err else: if match.group(2) in ancestors.segments: - raise ConfigValueError( + err = ConfigValueError( f"{at}.format", "Malformed format; variable segment tried to rebind an identifier already bound in a parent.", v, ) + raise err if match.group(2) in new_segments: - raise ConfigValueError( + err = ConfigValueError( f"{at}.format", "Malformed format; variable segment tried to rebind an identifier already bound.", v, ) + raise err identifier = match.group(2) new_segments.append(identifier) if match.group(1) == "*": @@ -698,8 +727,9 @@ def __init__( ) else: # Must be a ## type variable + match_len = len(match.group(1)) regex.append( - lambda _, identifier=identifier, match_len=len(match.group(1)): ( + lambda _, identifier=identifier, match_len=match_len: ( f"(?P<{identifier}>[0-9]{{{match_len}}})" ), ) @@ -717,7 +747,8 @@ def __init__(self, from_file: dict) -> None: self.linter = ConfigLinter(from_file.get("linter", {})) if "system" not in from_file: - raise (ConfigMissingKeyError("system")) + err = ConfigMissingKeyError("system") + raise err self.system = ConfigSystem(from_file["system"]) @@ -797,7 +828,7 @@ def explain(self) -> _Explanation: @dataclass(frozen=True) class IssueArbitraryContentWhereNotAllowed(Issue): - """Content was found in the that didn't match any expected format.""" + """Content was found that didn't match any expected format.""" possible_formats: tuple[ContentPattern, ...] type: Literal["ARBITRARY_CONTENT_WHERE_NOT_ALLOWED"] = ( @@ -1350,9 +1381,12 @@ def _get_jdex_entries_here_or_children( relative_to, child, ) - for id, entries in child_entries.items(): + for jid, entries in child_entries.items(): _insert_concat_sorted( - id, entries, accumulated_entries, key=_sort_jdex_entry + jid, + entries, + accumulated_entries, + key=_sort_jdex_entry, ) accumulated_errors.extend(child_errors) break @@ -1386,11 +1420,11 @@ def _get_jdex_entries_here_or_children( break # Create entry - for id in note.ids: + for jid in note.ids: _insert_append_sorted( - id.id.build({**bound_segments, **match.groupdict()}), + jid.id.build({**bound_segments, **match.groupdict()}), JDexEntry( - id.entry.build( + jid.entry.build( {**bound_segments, **match.groupdict()}, ), PurePath(x).relative_to(relative_to), @@ -1419,7 +1453,7 @@ def _get_jdex_entries_here_or_children( if not has_content: # We have a fully empty JDex folder; it shouldn't exist if it's doing nothing. accumulated_errors.append( - JDexIssueEmptyFolder(PurePath(path).relative_to(relative_to)) + JDexIssueEmptyFolder(PurePath(path).relative_to(relative_to)), ) return (accumulated_entries, accumulated_errors) @@ -1441,9 +1475,9 @@ def _process_jdex( JDexIssueDuplicateID( ns[0].path, tuple(n.path for n in ns), - id, + jid, ) - for id, ns in jdex_entries_by_id.items() + for jid, ns in jdex_entries_by_id.items() if len(ns) != 1 ] return ( @@ -1531,7 +1565,8 @@ def _process_system_level_and_children( _insert_append_sorted( child_id, SystemFolder( - PurePath(x).relative_to(relative_to), child_structure + PurePath(x).relative_to(relative_to), + child_structure, ), accumulated_structure, key=lambda e: e.path, @@ -1559,7 +1594,7 @@ def _process_system_level_and_children( # If the tier has no children specified, it should be empty if not tier.children: children_that_should_not_be.append( - PurePath(x).relative_to(relative_to) + PurePath(x).relative_to(relative_to), ) else: accumulated_errors.append( @@ -1583,7 +1618,7 @@ def _process_system_level_and_children( ): # We have a fully empty folder; it shouldn't exist if it's doing nothing (unless it should be empty). accumulated_errors.append( - IssueEmptyFolder(PurePath(path).relative_to(relative_to)) + IssueEmptyFolder(PurePath(path).relative_to(relative_to)), ) return (accumulated_structure, accumulated_errors) @@ -1591,7 +1626,6 @@ def _process_system_level_and_children( def _process_system_root( ignored: tuple[str], root: ConfigSystemRoot, - system: ConfigSystem, jdex: None | dict[str, list[JDexEntry]], ) -> tuple[dict[str, list[SystemFolder | SystemFile]], list[Issue]]: by_id: dict[str, list[tuple[str | None, PurePath]]] = {} @@ -1607,27 +1641,27 @@ def _process_system_root( # Check for duplicate IDs duplicate_id_errors = [ - IssueDuplicateID(fs[0][1], tuple([f[1] for f in fs]), id) - for id, fs in by_id.items() + IssueDuplicateID(fs[0][1], tuple([f[1] for f in fs]), jid) + for jid, fs in by_id.items() if len(fs) != 1 ] # If we have a JDex, we can do some additional checks id_errors = [] if jdex is not None: - for id, fs in by_id.items(): - if id not in jdex: - id_errors.append(IssueIDNotInJDex(fs[0][1], id)) + for jid, fs in by_id.items(): + if jid not in jdex: + id_errors.append(IssueIDNotInJDex(fs[0][1], jid)) else: - jdex_entries = [n.entry for n in jdex[id]] + jdex_entries = [n.entry for n in jdex[jid]] for expected_jdex_entry, f in fs: if expected_jdex_entry and expected_jdex_entry not in jdex_entries: id_errors.append( IssueIDDifferentFromJDex( f, - id, + jid, expected_jdex_entry, - jdex[id], + jdex[jid], ), ) @@ -1635,6 +1669,7 @@ def _process_system_root( def lint_system(config: Config) -> LintResults: + """Given a valid jdlint config, lint the specified system and return results.""" jdex_errors = [] jdex_entries = {} if config.system.jdex: @@ -1648,7 +1683,6 @@ def lint_system(config: Config) -> LintResults: (root_structure, root_errors) = _process_system_root( config.linter.ignore, root, - config.system, jdex_entries if config.system.jdex else None, ) ignored_errors += sum( @@ -1733,7 +1767,8 @@ def lint_system(config: Config) -> LintResults: for r in args.disable: if r in [e.type for e in typing.get_args(AnyIssueType)]: continue - raise ConfigValueError("--disable", "not a valid rule name", r) + err = ConfigValueError("--disable", "not a valid rule name", r) + raise err config.linter.disable_rules.extend(args.disable) @@ -1756,17 +1791,20 @@ def lint_system(config: Config) -> LintResults: jdex_errs_by_type: dict[JDexIssueType, list[JDexIssue]] = {} for je in results.jdex.errors if results.jdex else []: _insert_append_sorted( - je.type, je, jdex_errs_by_type, key=_sort_jdex_error + je.type, + je, + jdex_errs_by_type, + key=_sort_jdex_error, ) # Print JDex errors if any if jdex_errs_by_type: any_errors = True - print(f"{'':=^80}\n{'JDex Errors Found':^80}\n{'':=^80}") + print(f"{'':=^80}\n{'JDex Errors Found':^80}\n{'':=^80}") # noqa: T201 for errs in jdex_errs_by_type.values(): first_j_err = next(iter(errs)) # Just get the first error explanation = first_j_err.explain() - print(f"\n{explanation.explanation} ({first_j_err.type})") - print( + print(f"\n{explanation.explanation} ({first_j_err.type})") # noqa: T201 + print( # noqa: T201 textwrap.indent( "\n".join( [e.display() for e in errs], @@ -1774,8 +1812,8 @@ def lint_system(config: Config) -> LintResults: " ", ), ) - print(explanation.fix) - print("\n") + print(explanation.fix) # noqa: T201 + print("\n") # noqa: T201 # Print file errors if any if any(r.errors for r in results.roots.values()): @@ -1786,12 +1824,12 @@ def lint_system(config: Config) -> LintResults: errs_by_type = {} for e in root.errors: _insert_append_sorted(e.type, e, errs_by_type, key=_sort_error) - print(f"{'':=^80}\n{location + ' Errors Found:':^80}\n{'':=^80}") + print(f"{'':=^80}\n{location + ' Errors Found:':^80}\n{'':=^80}") # noqa: T201 for errs in errs_by_type.values(): first_err = next(iter(errs)) # Just get the first error explanation = first_err.explain() - print(f"\n{first_err.type:^80}\n{explanation.explanation}\n---") - print( + print(f"\n{first_err.type:^80}\n{explanation.explanation}\n---") # noqa: T201 + print( # noqa: T201 textwrap.indent( "\n".join( [e.display() for e in errs], @@ -1799,10 +1837,10 @@ def lint_system(config: Config) -> LintResults: " ", ), ) - print(f"---\n{explanation.fix}\n") + print(f"---\n{explanation.fix}\n") # noqa: T201 if results.ignored_errs: - print( + print( # noqa: T201 f"{'':=^80}\n{'Ignored Errors: ' + str(results.ignored_errs):^80}\n{'':=^80}", ) if any_errors: @@ -1810,5 +1848,5 @@ def lint_system(config: Config) -> LintResults: sys.exit(1) if not config.linter.json_output: - print("Everything looks good!") + print("Everything looks good!") # noqa: T201 sys.exit(0) diff --git a/run_tests.py b/run_tests.py index 9dd9497..ec1aae9 100755 --- a/run_tests.py +++ b/run_tests.py @@ -3,15 +3,14 @@ """Tests for jdlint.""" from __future__ import annotations -import contextlib -import dataclasses -import tomllib +import contextlib import json import os import unittest from pathlib import Path, PurePath -from typing import Any + +import tomllib import jdlint @@ -26,7 +25,7 @@ def tests(self) -> None: # Find all tests with os.scandir(PurePath("tests")) as test_it: for f in test_it: - # Make a subtest and open result file + # Make a sub-test and open result file with ( self.subTest(msg=f.name, f=f), Path( @@ -39,7 +38,7 @@ def tests(self) -> None: # Load config config = jdlint.Config(tomllib.load(config_file)) - # Lint the test dir + # Lint the test directory results = jdlint.lint_system(config) expected = json.load(golden_file) @@ -48,7 +47,7 @@ def tests(self) -> None: json.dumps( results, cls=jdlint._EnhancedJSONEncoder, - ) + ), ) # Compare results From 65988dba0db5a5712ebb3cb27c375989ce1347b2 Mon Sep 17 00:00:00 2001 From: SiriusStarr <2049163+SiriusStarr@users.noreply.github.com> Date: Mon, 27 Jul 2026 17:26:31 -0700 Subject: [PATCH 19/23] Improve pretty-printing --- configs/siriusstarr.toml | 2 +- jdlint.py | 895 ++++++++++++++++++--------------------- 2 files changed, 403 insertions(+), 494 deletions(-) diff --git a/configs/siriusstarr.toml b/configs/siriusstarr.toml index b2345c5..c423b69 100644 --- a/configs/siriusstarr.toml +++ b/configs/siriusstarr.toml @@ -47,7 +47,7 @@ id = "B/=BB/" entry = "B/=BB/ /=Branch/" [[system.jdex.children.children.notes]] -name = "JDex Branch Index Note" +name = "JDex Cache Note" format = "C/=BB/./##ID/ /*Name/.md" [[system.jdex.children.children.notes.ids]] diff --git a/jdlint.py b/jdlint.py index bf87dc6..ea65720 100755 --- a/jdlint.py +++ b/jdlint.py @@ -89,8 +89,67 @@ def _report_extra_keys(at: str, from_file: dict, valid: tuple[str, ...]) -> None raise err +def _pop_nonempty_str_attribute(at: str, attr: str, from_file: dict) -> str: + """Given a parent location, a mandatory attribute to get, and data, return it.""" + if attr not in from_file: + err = ConfigMissingKeyError(f"{at}.{attr}") + raise err + val = from_file.pop(attr) + if not isinstance(val, str): + err = ConfigTypeError( + f"{at}.{attr}", + "str", + type(val).__name__, + ) + raise err + if val == "": + err = ConfigValueError( + f"{at}.{attr}", + "Must not be empty.", + val, + ) + raise err + return val + + +def _pop_default_false_bool(at: str, attr: str, from_file: dict) -> bool: + """Get a boolean at the specified attribute, defaulting to false, or fail.""" + val = from_file.pop(attr, False) + if not isinstance(val, bool): + err = ConfigTypeError( + f"{at}.{attr}", + "bool", + type(val).__name__, + ) + raise err + return val + + +def _pop_default_empty_list(at: str, attr: str, from_file: dict) -> list: + """Get a list at the specified attribute, defaulting to [], or fail.""" + val = from_file.pop(attr, []) + if not isinstance(val, list): + err = ConfigTypeError( + f"{at}.{attr}", + "list", + type(val).__name__, + ) + raise err + return val + + +def _pop_ignore_list(at: str, from_file: dict) -> list[str]: + """Get a list of strings at .ignore or fail, defaulting to [].""" + val = _pop_default_empty_list(at, "ignore", from_file) + for i, r in enumerate(val): + if not isinstance(r, str): + err = ConfigTypeError(f"{at}.ignore[{i}]", "str", type(r).__name__) + raise err + return val + + class ConfigSystemRoot: - """A root (base folder) of a JD system to check for correctness, e.g. ~/Documents.""" + """A root of a JD system to check for correctness, e.g. ~/Documents.""" def __init__( self, @@ -99,24 +158,11 @@ def __init__( from_file: dict, ) -> None: """Create a valid configuration given a loaded section of a config file.""" - # Acquire and set defaults - if "name" not in from_file: - err = ConfigMissingKeyError(f"{at}.name") - raise err - self.name = from_file.pop("name") - if "path" not in from_file: - err = ConfigMissingKeyError(f"{at}.path") - raise err - self.path = Path(from_file.pop("path")).expanduser() - self.ignore = from_file.pop("ignore", []) - - if not isinstance(self.name, str): - err = ConfigTypeError( - f"{at}.name", - "str", - type(self.name).__name__, - ) - raise err + self.name = _pop_nonempty_str_attribute(at, "name", from_file) + self.path = Path( + _pop_nonempty_str_attribute(at, "path", from_file), + ).expanduser() + self.ignore = _pop_ignore_list(at, from_file) # Validate path is good if not self.path.is_dir(): @@ -126,17 +172,6 @@ def __init__( str(self.path), ) raise err - if not isinstance(self.ignore, list): - err = ConfigTypeError( - f"{at}.ignore", - "list", - type(self.ignore).__name__, - ) - raise err - for r in self.ignore: - if not isinstance(r, str): - err = ConfigTypeError(f"{at}.ignore", "str", type(r).__name__) - raise err # Load specialized structure, if any if "children" in from_file: @@ -146,7 +181,9 @@ def __init__( ConfigFormatAncestorInfo((), ()), v, ) - for i, v in enumerate(from_file.pop("children", [])) + for i, v in enumerate( + _pop_default_empty_list(at, "children", from_file), + ) ] elif default_structure: self.children = default_structure @@ -166,13 +203,12 @@ class ConfigSystemJDex: def __init__(self, at: str, from_file: dict) -> None: """Create a valid configuration given a loaded section of a config file.""" # Acquire and set defaults - if "path" not in from_file: - err = ConfigMissingKeyError(f"{at}.path") - raise err - self.path = Path(from_file.pop("path")).expanduser() - self.ignore = from_file.pop("ignore", []) + self.path = Path( + _pop_nonempty_str_attribute(at, "path", from_file), + ).expanduser() + self.ignore = _pop_ignore_list(at, from_file) - # Validate + # Validate path if not self.path.is_dir(): err = ConfigValueError( f"{at}.path", @@ -180,17 +216,6 @@ def __init__(self, at: str, from_file: dict) -> None: str(self.path), ) raise err - if not isinstance(self.ignore, list): - err = ConfigTypeError( - f"{at}.ignore", - "list", - type(self.ignore).__name__, - ) - raise err - for r in self.ignore: - if not isinstance(r, str): - err = ConfigTypeError(f"{at}.ignore", "str", type(r).__name__) - raise err self.children = [ ConfigJDexTier( @@ -198,7 +223,7 @@ def __init__(self, at: str, from_file: dict) -> None: ConfigFormatAncestorInfo((), ()), v, ) - for i, v in enumerate(from_file.pop("children", [])) + for i, v in enumerate(_pop_default_empty_list(at, "children", from_file)) ] self.notes = [ ConfigJDexNotes( @@ -206,7 +231,7 @@ def __init__(self, at: str, from_file: dict) -> None: ConfigFormatAncestorInfo((), ()), v, ) - for i, v in enumerate(from_file.pop("notes", [])) + for i, v in enumerate(_pop_default_empty_list(at, "notes", from_file)) ] _report_extra_keys(at, from_file, tuple(self.__dict__.keys())) @@ -218,18 +243,15 @@ class ConfigLinter: def __init__(self, from_file: dict) -> None: """Create a valid configuration given a loaded linter section of a config file.""" # Acquire and set defaults - self.disable_rules = from_file.pop("disable_rules", []) - self.json_output = from_file.pop("json_output", False) - self.ignore = from_file.pop("ignore", []) + self.disable_rules = _pop_default_empty_list( + "linter", + "disable_rules", + from_file, + ) + self.json_output = _pop_default_false_bool("linter", "json_output", from_file) + self.ignore = _pop_ignore_list("linter", from_file) # Validate - if not isinstance(self.disable_rules, list): - err = ConfigTypeError( - "linter.disable_rules", - "list", - type(self.disable_rules).__name__, - ) - raise err for r in self.disable_rules: if r in [e.type for e in typing.get_args(AnyIssueType)]: continue @@ -244,18 +266,6 @@ def __init__(self, from_file: dict) -> None: ) raise err - if not isinstance(self.ignore, list): - err = ConfigTypeError( - "linter.ignore", - "list", - type(self.ignore).__name__, - ) - raise err - for r in self.ignore: - if not isinstance(r, str): - err = ConfigTypeError("linter.ignore", "str", type(r).__name__) - raise err - _report_extra_keys("linter", from_file, tuple(self.__dict__.keys())) @@ -270,7 +280,13 @@ def __init__(self, from_file: dict) -> None: ConfigFormatAncestorInfo((), ()), v, ) - for i, v in enumerate(from_file.pop("default", {}).pop("children", [])) + for i, v in enumerate( + _pop_default_empty_list( + "system.default", + "children", + from_file.pop("default", {}), + ), + ) ] self.roots = [ @@ -279,7 +295,7 @@ def __init__(self, from_file: dict) -> None: default_structure, v, ) - for i, v in enumerate(from_file.pop("roots", [])) + for i, v in enumerate(_pop_default_empty_list("system", "roots", from_file)) ] accum_names = {} @@ -320,22 +336,10 @@ def __init__( ) -> None: """Create a valid format given a string from a config file.""" # Validate - if not isinstance(from_file, str): - raise ConfigTypeError( - at, - "str", - type(from_file).__name__, - ) if from_file.count("/") % 2 != 0: raise ConfigValueError( at, - "Malformed id/JDex note format; there must be an even number of / characters. You have an extra one/are missing one.", - from_file, - ) - if from_file == "": - raise ConfigValueError( - at, - "Malformed id/JDex note format; must not be empty.", + "Malformed format; there must be an even number of / characters. You have an extra/are missing one.", from_file, ) @@ -351,7 +355,7 @@ def __init__( if not match: raise ConfigValueError( at, - "Malformed id/JDex note format; variable segment must consist of = followed by an alphabetic identifier.", + "Malformed format; variable segment must consist of = followed by an alphabetic identifier.", v, ) if match.group(1) in ancestors.segments: @@ -361,7 +365,7 @@ def __init__( else: raise ConfigValueError( at, - "Malformed id/JDex note format; variable segment referenced an identifier never bound.", + "Malformed format; variable segment referenced an identifier never bound.", v, ) @@ -378,22 +382,15 @@ def __init__( from_file: dict, ) -> None: """Create a valid note format given a loaded section of a config file.""" - # Compile Format - if "id" not in from_file: - err = ConfigMissingKeyError(f"{at}.id") - raise err self.id = ConfigStaticFormat( f"{at}.id", ancestors, - from_file.pop("id"), + _pop_nonempty_str_attribute(at, "id", from_file), ) - if "entry" not in from_file: - err = ConfigMissingKeyError(f"{at}.entry") - raise err self.entry = ConfigStaticFormat( f"{at}.entry", ancestors, - from_file.pop("entry"), + _pop_nonempty_str_attribute(at, "entry", from_file), ) _report_extra_keys(at, from_file, tuple(self.__dict__.keys())) @@ -427,13 +424,13 @@ def __init__( self.format, v, ) - for i, v in enumerate(from_file.pop("ids", [])) + for i, v in enumerate(_pop_default_empty_list(at, "ids", from_file)) ] if "jdex_entry" in from_file: self.jdex_entry = ConfigStaticFormat( f"{at}.jdex_entry", self.format, - from_file.pop("jdex_entry"), + _pop_nonempty_str_attribute(at, "jdex_entry", from_file), ) else: self.jdex_entry = None @@ -442,7 +439,7 @@ def __init__( class ConfigFolderTier: - """A tier (hierarchical level) of a JD system, e.g. a Category, whether in the JDex or the system itself.""" + """A tier of a JD system, e.g. a Category, whether in the JDex or the system itself.""" def __init__( self, @@ -453,7 +450,11 @@ def __init__( ) -> None: """Create a valid tier given a loaded section of a config file.""" # Acquire and set defaults - self.allow_arbitrary_contents = from_file.pop("allow_arbitrary_contents", False) + self.allow_arbitrary_contents = _pop_default_false_bool( + at, + "allow_arbitrary_contents", + from_file, + ) # Validate if not isinstance(self.allow_arbitrary_contents, bool): @@ -476,7 +477,7 @@ def __init__( self.format, v, ) - for i, v in enumerate(from_file.pop("children", [])) + for i, v in enumerate(_pop_default_empty_list(at, "children", from_file)) ] if self.children and self.allow_arbitrary_contents: raise ConfigConflictError( @@ -496,7 +497,7 @@ def __init__( class ConfigSystemTier(ConfigFolderTier): - """A tier (hierarchical level) of a JD system, e.g. a Category, in the system (not the JDex).""" + """A tier of a JD system, e.g. a Category, in the system (not the JDex).""" def __init__( self, @@ -507,8 +508,8 @@ def __init__( """Create a valid tier given a loaded section of a config file.""" # Acquire and set defaults - self.can_be_file = from_file.pop("can_be_file", False) - self.no_jdex_entry = from_file.pop("no_jdex_entry", False) + self.can_be_file = _pop_default_false_bool(at, "can_be_file", from_file) + self.no_jdex_entry = _pop_default_false_bool(at, "no_jdex_entry", from_file) # Call the folder tier stuff super().__init__(ConfigSystemTier, at, ancestors, from_file) @@ -517,18 +518,15 @@ def __init__( self.jdex_entry = ConfigStaticFormat( f"{at}.jdex_entry", self.format, - from_file.pop("jdex_entry"), + _pop_nonempty_str_attribute(at, "jdex_entry", from_file), ) else: self.jdex_entry = None - if "id" not in from_file: - err = ConfigMissingKeyError(f"{at}.id") - raise err self.id = ConfigStaticFormat( f"{at}.id", self.format, - from_file.pop("id"), + _pop_nonempty_str_attribute(at, "id", from_file), ) if not isinstance(self.can_be_file, bool): @@ -581,13 +579,13 @@ def __init__( self.format, v, ) - for i, v in enumerate(from_file.pop("notes", [])) + for i, v in enumerate(_pop_default_empty_list(at, "notes", from_file)) ] if self.notes and self.format.forbidden: raise ConfigConflictError( at, - "If forbidden, notes cannot be speciefed.", + "If forbidden, notes cannot be specified.", ) _report_extra_keys(at, from_file, tuple(self.__dict__.keys())) @@ -614,47 +612,11 @@ def __init__( from_file: dict, ) -> None: """Create a valid format given a string from a config file.""" - if "format" not in from_file: - err = ConfigMissingKeyError(f"{at}.format") - raise err - - self.raw_format = from_file.pop("format") - self.forbidden = from_file.pop("forbidden", False) - - if "name" not in from_file: - err = ConfigMissingKeyError(f"{at}.name") - raise err + name = _pop_nonempty_str_attribute(at, "name", from_file) + self.raw_format = _pop_nonempty_str_attribute(at, "format", from_file) + self.forbidden = _pop_default_false_bool(at, "forbidden", from_file) # Validate - if not isinstance(from_file["name"], str): - err = ConfigTypeError( - f"{at}.name", - "str", - type(from_file["name"]).__name__, - ) - raise err - if not isinstance(self.raw_format, str): - err = ConfigTypeError( - f"{at}.format", - "str", - type(self.raw_format).__name__, - ) - raise err - if not isinstance(self.forbidden, bool): - err = ConfigTypeError( - f"{at}.forbidden", - "bool", - type(self.forbidden).__name__, - ) - raise err - - if from_file["name"] == "": - err = ConfigValueError( - f"{at}.name", - "Malformed name; must not be empty.", - str(from_file), - ) - raise err if self.raw_format.count("/") % 2 != 0: err = ConfigValueError( f"{at}.format", @@ -662,13 +624,6 @@ def __init__( str(from_file), ) raise err - if self.raw_format == "": - err = ConfigValueError( - f"{at}.format", - "Malformed format; must not be empty.", - str(from_file), - ) - raise err regex = [] new_segments = [] @@ -677,64 +632,64 @@ def __init__( if i % 2 == 0: # Literal segment regex.append(lambda _, v=v: re.escape(v)) + continue + # Variable segment + match = ConfigFormat.variable_segment_re.fullmatch(v) + if not match: + err = ConfigValueError( + f"{at}.format", + "Malformed format; variable segment must consist of =, *, or one or more # followed by an alphabetic identifier.", + v, + ) + raise err + segment_type = match.group(1) + identifier = match.group(2) + if segment_type == "=": + if identifier in ancestors.segments: + p = identifier + regex.append(lambda d, p=p: re.escape(d[p])) + elif identifier in new_segments: + regex.append( + lambda _, identifier=identifier: f"(?P={identifier})", + ) + + else: + err = ConfigValueError( + f"{at}.format", + f'Malformed format; variable segment referenced the identifier "{identifier}" which has not been bound.', + v, + ) + raise err else: - # Variable segment - match = ConfigFormat.variable_segment_re.fullmatch(v) - if not match: + if identifier in ancestors.segments: err = ConfigValueError( f"{at}.format", - "Malformed format; variable segment must consist of =, *, or one or more # followed by an alphabetic identifier.", + f'Malformed format; variable segment tried to rebind the identifier "{identifier}", which was already bound in a parent.', v, ) raise err - if match.group(1) == "=": - if match.group(2) in ancestors.segments: - p = match.group(2) - regex.append(lambda d, p=p: re.escape(d[p])) - elif match.group(2) in new_segments: - identifier = match.group(2) - regex.append( - lambda _, identifier=identifier: f"(?P={identifier})", - ) - - else: - err = ConfigValueError( - f"{at}.format", - "Malformed format; variable segment referenced an identifier never bound.", - v, - ) - raise err + if identifier in new_segments: + err = ConfigValueError( + f"{at}.format", + f'Malformed format; variable segment tried to rebind the identifier "{identifier}", which was already bound in this format.', + v, + ) + raise err + new_segments.append(identifier) + if segment_type == "*": + regex.append( + lambda _, identifier=identifier: f"(?P<{identifier}>.+)", + ) else: - if match.group(2) in ancestors.segments: - err = ConfigValueError( - f"{at}.format", - "Malformed format; variable segment tried to rebind an identifier already bound in a parent.", - v, - ) - raise err - if match.group(2) in new_segments: - err = ConfigValueError( - f"{at}.format", - "Malformed format; variable segment tried to rebind an identifier already bound.", - v, - ) - raise err - identifier = match.group(2) - new_segments.append(identifier) - if match.group(1) == "*": - regex.append( - lambda _, identifier=identifier: f"(?P<{identifier}>.+)", - ) - else: - # Must be a ## type variable - match_len = len(match.group(1)) - regex.append( - lambda _, identifier=identifier, match_len=match_len: ( - f"(?P<{identifier}>[0-9]{{{match_len}}})" - ), - ) + # Must be a ## type variable + match_len = len(segment_type) + regex.append( + lambda _, identifier=identifier, match_len=match_len: ( + f"(?P<{identifier}>[0-9]{{{match_len}}})" + ), + ) - self.name = (*ancestors.name, from_file.pop("name")) + self.name = (*ancestors.name, name) self.segments = ancestors.segments + tuple(new_segments) self.build_regex = lambda d: "".join([f(d) for f in regex]) @@ -762,7 +717,7 @@ class Issue: """A single error detected in the system.""" file: PurePath - type = None + type = "" def display(self) -> str: """Display this particular instance of an error.""" @@ -778,7 +733,7 @@ class JDexIssue: """A single error detected in the JDex.""" file: PurePath - type = None + type = "" def display(self) -> str: """Display this particular instance of an error.""" @@ -816,13 +771,13 @@ class IssueFileWhereFolderExpected(Issue): def display(self) -> str: """Display this particular instance of an error.""" - return f"{self.file!s}\n (matched {_print_pattern(self.matched_pattern)})" + return f"{self.file!s}\n matched: {_print_pattern(self.matched_pattern)})" def explain(self) -> _Explanation: """Explain what this error is.""" return _Explanation( explanation="A file was found that matched the format of an expected child folder.", - fix="Your format should not mix folders and notes that share a naming scheme.", + fix="Your format should not mix folders and files that share a naming scheme.", ) @@ -856,7 +811,7 @@ class IssueFolderShouldBeEmpty(Issue): def display(self) -> str: """Display this particular instance of an error.""" - return f"{self.file!s}\n has {len(self.children)} children" + return f"{self.file!s}\n has {_pluralize(len(self.children), 'child', 'children')}" def explain(self) -> _Explanation: """Explain what this error is.""" @@ -876,7 +831,7 @@ class IssueDuplicateID(Issue): def display(self) -> str: """Display this particular instance of an error.""" - return f"{self.id}:\n " + "\n ".join([str(f.name) for f in self.files]) + return f"{self.id}:\n " + "\n ".join([str(f) for f in self.files]) def explain(self) -> _Explanation: """Explain what this error is.""" @@ -895,7 +850,7 @@ class IssueIDNotInJDex(Issue): def display(self) -> str: """Display this particular instance of an error.""" - return f"{self.file!s} [ID: {self.id}]" + return f"{self.id}:\n {self.file!s}" def explain(self) -> _Explanation: """Explain what this error is.""" @@ -917,9 +872,9 @@ class IssueIDDifferentFromJDex(Issue): def display(self) -> str: """Display this particular instance of an error.""" known = "\n".join( - f"{n.entry} [from {n.path.name}]" for n in self.known_jdex_entries + f"{n.entry} [from {n.path}]" for n in self.known_jdex_entries ) - return f"{self.file!s}\n ID: {self.id}\n Expected:\n {self.expected_jdex_entry}\n Actual:\n{textwrap.indent(known, ' ')}" + return f"{self.id}:\n Folder: {self.file!s}\n Expected JDex: {self.expected_jdex_entry}\n Actual JDex:\n{textwrap.indent(known, ' ')}" def explain(self) -> _Explanation: """Explain what this error is.""" @@ -938,7 +893,7 @@ class IssueEncounteredForbiddenFolder(Issue): def display(self) -> str: """Display this particular instance of an error.""" - return f"{self.file!s}\n (matched {_print_pattern(self.matched_pattern)})" + return f"{self.file!s}\n matched: {_print_pattern(self.matched_pattern)})" def explain(self) -> _Explanation: """Explain what this error is.""" @@ -966,7 +921,7 @@ class JDexIssueDuplicateID(JDexIssue): def display(self) -> str: """Display this particular instance of an error.""" - return f"{self.id}:\n " + "\n ".join([str(f.name) for f in self.files]) + return f"{self.id}:\n " + "\n ".join([str(f) for f in self.files]) def explain(self) -> _Explanation: """Explain what this error is.""" @@ -985,7 +940,7 @@ class JDexIssueFileWhereFolderExpected(JDexIssue): def display(self) -> str: """Display this particular instance of an error.""" - return f"{self.file!s}\n (matched {_print_pattern(self.matched_pattern)})" + return f"{self.file!s}\n matched: {_print_pattern(self.matched_pattern)})" def explain(self) -> _Explanation: """Explain what this error is.""" @@ -1004,7 +959,7 @@ class JDexIssueFolderWhereNoteExpected(JDexIssue): def display(self) -> str: """Display this particular instance of an error.""" - return f"{self.file!s}\n (matched {_print_pattern(self.matched_pattern)})" + return f"{self.file!s}\n matched: {_print_pattern(self.matched_pattern)})" def explain(self) -> _Explanation: """Explain what this error is.""" @@ -1072,7 +1027,7 @@ class JDexIssueEncounteredForbiddenFolder(JDexIssue): def display(self) -> str: """Display this particular instance of an error.""" - return f"{self.file!s}\n (matched {_print_pattern(self.matched_pattern)})" + return f"{self.file!s}\n matched: {_print_pattern(self.matched_pattern)})" def explain(self) -> _Explanation: """Explain what this error is.""" @@ -1091,7 +1046,7 @@ class JDexIssueEncounteredForbiddenNote(JDexIssue): def display(self) -> str: """Display this particular instance of an error.""" - return f"{self.file!s}\n (matched {_print_pattern(self.matched_pattern)})" + return f"{self.file!s}\n matched: {_print_pattern(self.matched_pattern)})" def explain(self) -> _Explanation: """Explain what this error is.""" @@ -1180,33 +1135,6 @@ class LintResults: ignored_errs: int -@dataclass -class _JDexAccumulator: - """Accumulator used by _get_jdex_entries to gather information about the JDex.""" - - errors: list[JDexIssue] - areas: dict[str, list[tuple[str, File]]] - categories: dict[str, list[tuple[str, File]]] - ids: dict[str, list[tuple[str, File]]] - headers: dict[str, list[tuple[str, File]]] - - def __init__(self) -> None: - self.errors = [] - self.areas = {} - self.categories = {} - self.ids = {} - self.headers = {} - - -@dataclass(frozen=True) -class _JDexResults: - """Canonical results from the JDex, featuring the ID and name of each area/category/ID.""" - - areas: dict[str, str] - categories: dict[str, str] - ids: dict[str, str] - - class _EnhancedJSONEncoder(json.JSONEncoder): def default(self, o: object) -> object: # Add JSON encoding for dataclasses and paths @@ -1229,7 +1157,7 @@ def _print_unmatched_patterns(ps: tuple[ContentPattern, ...]) -> str: def _sort_jdex_error(e: JDexIssue) -> tuple[str, tuple[tuple[str, ...], str]]: # Sort errors alphabetically by type, then by file affected # This is split from _sort_error for type-checking nonsense - if e.type is None: + if e.type == "": raise NotImplementedError return ( e.type, @@ -1240,7 +1168,7 @@ def _sort_jdex_error(e: JDexIssue) -> tuple[str, tuple[tuple[str, ...], str]]: def _sort_error(e: Issue) -> tuple[str, tuple[tuple[str, ...], str]]: # Sort errors alphabetically by type, then by file affected # This is split from _sort_jdex_error for type-checking nonsense - if e.type is None: + if e.type == "": raise NotImplementedError return ( e.type, @@ -1253,7 +1181,7 @@ def _sort_jdex_entry(e: JDexEntry) -> tuple[str, PurePath]: def _entry_is_ignored( - ignored: tuple[str] | None, + ignored: list[str] | None, f: os.DirEntry, ) -> bool: """Check if a given file/directory should be ignored.""" @@ -1283,49 +1211,11 @@ def _insert_concat_sorted(k, vs: list, d, key=None) -> None: # noqa: ANN001 d[k].sort(key=key) -def _process_single_file_jdex(path: Path) -> _JDexResults: - """Process a JDex located in a single file.""" - # Matches JDex areas in a single-file format - jdex_line_area_re = re.compile("([0-9])0-(?:\\1)9 (.+?)\\s*(//.*)?") - # Matches JDex categories in a single-file format - jdex_line_category_re = re.compile("([0-9][0-9]) (.+?)\\s*(//.*)?") - # Matches JDex ids in a single-file format - jdex_line_id_re = re.compile("([0-9][0-9].[0-9][0-9]) (.+?)\\s*(//.*)?") - - file_areas = {} - file_categories = {} - file_ids = {} - - with path.open() as jdex_it: - for entry in jdex_it: - area_match = jdex_line_area_re.fullmatch(entry.strip()) - if area_match: - file_areas[area_match.group(1)] = ( - f"{area_match.group(1)}0-09 {area_match.group(2)}" - ) - continue - category_match = jdex_line_category_re.fullmatch(entry.strip()) - if category_match: - file_categories[category_match.group(1)] = ( - f"{category_match.group(1)} {category_match.group(2)}" - ) - continue - id_match = jdex_line_id_re.fullmatch(entry.strip()) - if id_match: - file_ids[id_match.group(1)] = f"{id_match.group(1)} {id_match.group(2)}" - continue - return _JDexResults( - areas=file_areas, - categories=file_categories, - ids=file_ids, - ) - - def _get_jdex_entries_here_or_children( - ignored: tuple[str], + ignored: list[str], + root_path: PurePath, bound_segments: dict[str, str], path: os.PathLike, - relative_to: PurePath, tier: ConfigJDexTier | ConfigSystemJDex, ) -> tuple[dict[str, list[JDexEntry]], list[JDexIssue]]: # Compile regexes for children @@ -1340,132 +1230,137 @@ def _get_jdex_entries_here_or_children( accumulated_errors: list[JDexIssue] = [] has_content = False - with os.scandir(path) as contents: - for x in contents: - if _entry_is_ignored(ignored, x): - continue - has_content = True - for child_format, child in valid_children: - match = child_format.fullmatch(x.name) + + def process_dir_entry(x: os.DirEntry) -> None: + for child_format, child in valid_children: + match = child_format.fullmatch(x.name) + if match: + if child.format.forbidden: + accumulated_errors.append( + JDexIssueEncounteredForbiddenFolder( + PurePath(x).relative_to(root_path), + ContentPattern( + child.format.name, + child.format.raw_format, + ), + ), + ) + break + # Is a valid child folder + if x.is_file(): + # This is an error + accumulated_errors.append( + JDexIssueFileWhereFolderExpected( + PurePath(x).relative_to(root_path), + ContentPattern( + child.format.name, + child.format.raw_format, + ), + ), + ) + break + + # Walk child + (child_entries, child_errors) = _get_jdex_entries_here_or_children( + ignored, + root_path, + {**bound_segments, **match.groupdict()}, + PurePath(x), + child, + ) + for jid, entries in child_entries.items(): + _insert_concat_sorted( + jid, + entries, + accumulated_entries, + key=_sort_jdex_entry, + ) + accumulated_errors.extend(child_errors) + break + else: + for note_format, note in valid_notes: + match = note_format.fullmatch(x.name) if match: - if child.format.forbidden: + if note.format.forbidden: accumulated_errors.append( - JDexIssueEncounteredForbiddenFolder( - PurePath(x).relative_to(relative_to), + JDexIssueEncounteredForbiddenNote( + PurePath(x).relative_to(root_path), ContentPattern( - child.format.name, - child.format.raw_format, + note.format.name, + note.format.raw_format, ), ), ) break - # Is a valid child folder - if x.is_file(): + # Is a valid JDex note + if x.is_dir(): # This is an error accumulated_errors.append( - JDexIssueFileWhereFolderExpected( - PurePath(x).relative_to(relative_to), + JDexIssueFolderWhereNoteExpected( + PurePath(x).relative_to(root_path), ContentPattern( - child.format.name, - child.format.raw_format, + note.format.name, + note.format.raw_format, ), ), ) break - # Walk child - (child_entries, child_errors) = _get_jdex_entries_here_or_children( - ignored, - {**bound_segments, **match.groupdict()}, - PurePath(x), - relative_to, - child, - ) - for jid, entries in child_entries.items(): - _insert_concat_sorted( - jid, - entries, + # Create entry + for jid in note.ids: + _insert_append_sorted( + jid.id.build({**bound_segments, **match.groupdict()}), + JDexEntry( + jid.entry.build( + {**bound_segments, **match.groupdict()}, + ), + PurePath(x).relative_to(root_path), + ), accumulated_entries, key=_sort_jdex_entry, ) - accumulated_errors.extend(child_errors) break else: - for note_format, note in valid_notes: - match = note_format.fullmatch(x.name) - if match: - if note.format.forbidden: - accumulated_errors.append( - JDexIssueEncounteredForbiddenNote( - PurePath(x).relative_to(relative_to), - ContentPattern( - note.format.name, - note.format.raw_format, - ), - ), - ) - break - # Is a valid JDex note - if x.is_dir(): - # This is an error - accumulated_errors.append( - JDexIssueFolderWhereNoteExpected( - PurePath(x).relative_to(relative_to), - ContentPattern( - note.format.name, - note.format.raw_format, - ), - ), - ) - break - - # Create entry - for jid in note.ids: - _insert_append_sorted( - jid.id.build({**bound_segments, **match.groupdict()}), - JDexEntry( - jid.entry.build( - {**bound_segments, **match.groupdict()}, - ), - PurePath(x).relative_to(relative_to), - ), - accumulated_entries, - key=_sort_jdex_entry, + # If we got here, it matched no known child/note + if not getattr(tier, "allow_arbitrary_contents", False): + # This is an error + accumulated_errors.append( + JDexIssueArbitraryContentWhereNotAllowed( + PurePath(x).relative_to(root_path), + tuple( + ContentPattern(c.format.name, c.format.raw_format) + for c in tier.children ) - break - else: - # If we got here, it matched no known child/note - if not getattr(tier, "allow_arbitrary_contents", False): - # This is an error - accumulated_errors.append( - JDexIssueArbitraryContentWhereNotAllowed( - PurePath(x).relative_to(relative_to), - tuple( - ContentPattern(c.format.name, c.format.raw_format) - for c in tier.children - ) - + tuple( - ContentPattern(n.format.name, n.format.raw_format) - for n in tier.notes - ), + + tuple( + ContentPattern(n.format.name, n.format.raw_format) + for n in tier.notes ), - ) + ), + ) + + with os.scandir(path) as contents: + for x in contents: + if _entry_is_ignored(ignored, x): + continue + has_content = True + process_dir_entry(x) + if not has_content: # We have a fully empty JDex folder; it shouldn't exist if it's doing nothing. accumulated_errors.append( - JDexIssueEmptyFolder(PurePath(path).relative_to(relative_to)), + JDexIssueEmptyFolder(PurePath(path).relative_to(root_path)), ) return (accumulated_entries, accumulated_errors) def _process_jdex( - ignored: tuple[str], + ignored: list[str], jdex: ConfigSystemJDex, ) -> tuple[dict[str, list[JDexEntry]], list[JDexIssue]]: (jdex_entries_by_id, jdex_errors) = _get_jdex_entries_here_or_children( ignored + jdex.ignore, - {}, jdex.path, + {}, jdex.path, jdex, ) @@ -1487,130 +1382,128 @@ def _process_jdex( def _process_system_level_and_children( - ignored: tuple[str], + by_id_dict: dict[str, list[tuple[str | None, PurePath]]], + ignored: list[str], + root_path: PurePath, bound_segments: dict[str, str], path: os.PathLike, - relative_to: PurePath, tier: ConfigSystemRoot | ConfigSystemTier, - jdex: None | dict[str, list[JDexEntry]], - by_id_dict: dict[str, list[tuple[str | None, PurePath]]], ) -> tuple[dict[str, list[SystemFolder | SystemFile]], list[Issue]]: # Compile regexes for children valid_children = [ (re.compile(c.format.build_regex(bound_segments)), c) for c in tier.children ] - accumulated_errors = [] accumulated_structure = {} - has_content = False - children_that_should_not_be = [] - - with os.scandir(path) as contents: - for x in contents: - if _entry_is_ignored(ignored, x): - continue - has_content = True - for child_format, child in valid_children: - match = child_format.fullmatch(x.name) - if match: - if child.format.forbidden: - accumulated_errors.append( - IssueEncounteredForbiddenFolder( - PurePath(x).relative_to(relative_to), - ContentPattern( - child.format.name, - child.format.raw_format, - ), + content_in_should_be_empty = [] + + def process_dir_entry(x: os.DirEntry) -> None: + for child_format, child in valid_children: + match = child_format.fullmatch(x.name) + if match: + if child.format.forbidden: + accumulated_errors.append( + IssueEncounteredForbiddenFolder( + PurePath(x).relative_to(root_path), + ContentPattern( + child.format.name, + child.format.raw_format, ), - ) - break - # Is a valid child folder - child_id = child.id.build({**bound_segments, **match.groupdict()}) - if x.is_file(): - if child.can_be_file: - _insert_append_sorted( - child_id, - SystemFile( - PurePath(x).relative_to(relative_to), - ), - accumulated_structure, - key=lambda e: e.path, - ) - else: - # This is an error - accumulated_errors.append( - IssueFileWhereFolderExpected( - PurePath(x).relative_to(relative_to), - ContentPattern( - child.format.name, - child.format.raw_format, - ), - ), - ) - - else: - # Walk child - (child_structure, child_errors) = ( - _process_system_level_and_children( - ignored, + ), + ) + break + # Is a valid child folder + child_id = child.id.build({**bound_segments, **match.groupdict()}) + if not child.no_jdex_entry: + _insert_append_sorted( + child_id, + ( + child.jdex_entry.build( {**bound_segments, **match.groupdict()}, - PurePath(x), - relative_to, - child, - jdex, - by_id_dict, ) - ) + if child.jdex_entry + else x.name, + PurePath(x).relative_to(root_path), + ), + by_id_dict, + # Sort duplicates by their path, not their JDex entry + key=lambda e: e[1], + ) + if x.is_file(): + if child.can_be_file: _insert_append_sorted( child_id, - SystemFolder( - PurePath(x).relative_to(relative_to), - child_structure, + SystemFile( + PurePath(x).relative_to(root_path), ), accumulated_structure, key=lambda e: e.path, ) - accumulated_errors.extend(child_errors) - if not child.no_jdex_entry: - _insert_append_sorted( - child_id, - ( - child.jdex_entry.build( - {**bound_segments, **match.groupdict()}, - ) - if child.jdex_entry - else x.name, - PurePath(x).relative_to(relative_to), - ), - by_id_dict, - # Sort duplicates by their path, not their JDex entry - key=lambda e: e[1], - ) - break - else: - # If we got here, it matched no known child/note - if not getattr(tier, "allow_arbitrary_contents", False): - # If the tier has no children specified, it should be empty - if not tier.children: - children_that_should_not_be.append( - PurePath(x).relative_to(relative_to), - ) else: + # This is an error accumulated_errors.append( - IssueArbitraryContentWhereNotAllowed( - PurePath(x).relative_to(relative_to), - tuple( - ContentPattern(c.format.name, c.format.raw_format) - for c in tier.children + IssueFileWhereFolderExpected( + PurePath(x).relative_to(root_path), + ContentPattern( + child.format.name, + child.format.raw_format, ), ), ) - if children_that_should_not_be: + break + + # Walk child + (child_structure, child_errors) = _process_system_level_and_children( + by_id_dict, + ignored, + root_path, + {**bound_segments, **match.groupdict()}, + PurePath(x), + child, + ) + _insert_append_sorted( + child_id, + SystemFolder( + PurePath(x).relative_to(root_path), + child_structure, + ), + accumulated_structure, + key=lambda e: e.path, + ) + accumulated_errors.extend(child_errors) + break + else: + # If we got here, it matched no known child/note + if not getattr(tier, "allow_arbitrary_contents", False): + # If the tier has no children specified, it should be empty + if not tier.children: + content_in_should_be_empty.append( + PurePath(x).relative_to(root_path), + ) + else: + accumulated_errors.append( + IssueArbitraryContentWhereNotAllowed( + PurePath(x).relative_to(root_path), + tuple( + ContentPattern(c.format.name, c.format.raw_format) + for c in tier.children + ), + ), + ) + + with os.scandir(path) as contents: + for x in contents: + if _entry_is_ignored(ignored, x): + continue + has_content = True + process_dir_entry(x) + + if content_in_should_be_empty: accumulated_errors.append( IssueFolderShouldBeEmpty( - PurePath(path).relative_to(relative_to), - tuple(children_that_should_not_be), + PurePath(path).relative_to(root_path), + tuple(content_in_should_be_empty), ), ) if not has_content and ( @@ -1618,25 +1511,24 @@ def _process_system_level_and_children( ): # We have a fully empty folder; it shouldn't exist if it's doing nothing (unless it should be empty). accumulated_errors.append( - IssueEmptyFolder(PurePath(path).relative_to(relative_to)), + IssueEmptyFolder(PurePath(path).relative_to(root_path)), ) return (accumulated_structure, accumulated_errors) def _process_system_root( - ignored: tuple[str], + ignored: list[str], root: ConfigSystemRoot, jdex: None | dict[str, list[JDexEntry]], ) -> tuple[dict[str, list[SystemFolder | SystemFile]], list[Issue]]: by_id: dict[str, list[tuple[str | None, PurePath]]] = {} (root_structure, root_errors) = _process_system_level_and_children( + by_id, ignored + root.ignore, - {}, root.path, + {}, root.path, root, - jdex, - by_id, ) # Check for duplicate IDs @@ -1717,6 +1609,14 @@ def lint_system(config: Config) -> LintResults: ) +def _pluralize(num: int, word: str, weird_plural: str | None = None) -> str: + if num == 1: + return f"{num!s} {word}" + if weird_plural is None: + return f"{num!s} {word}s" + return f"{num!s} {weird_plural}" + + if __name__ == "__main__": parser = argparse.ArgumentParser( prog="jdlint", @@ -1799,11 +1699,16 @@ def lint_system(config: Config) -> LintResults: # Print JDex errors if any if jdex_errs_by_type: any_errors = True - print(f"{'':=^80}\n{'JDex Errors Found':^80}\n{'':=^80}") # noqa: T201 + total_errs = sum(len(errs) for errs in jdex_errs_by_type.values()) + print( + f"{'':=^80}\n{'JDex Errors Found:':^80}\n{_pluralize(total_errs, 'instance') + '; ' + _pluralize(len(jdex_errs_by_type), 'kind'):^80}\n{'':=^80}\n", + ) for errs in jdex_errs_by_type.values(): first_j_err = next(iter(errs)) # Just get the first error explanation = first_j_err.explain() - print(f"\n{explanation.explanation} ({first_j_err.type})") # noqa: T201 + print( # noqa: T201 + f"{first_j_err.type + ' (' + str(len(errs)) + ')':^80}\n{explanation.explanation}\n---", + ) print( # noqa: T201 textwrap.indent( "\n".join( @@ -1812,8 +1717,7 @@ def lint_system(config: Config) -> LintResults: " ", ), ) - print(explanation.fix) # noqa: T201 - print("\n") # noqa: T201 + print(f"---\n{explanation.fix}\n") # noqa: T201 # Print file errors if any if any(r.errors for r in results.roots.values()): @@ -1824,11 +1728,16 @@ def lint_system(config: Config) -> LintResults: errs_by_type = {} for e in root.errors: _insert_append_sorted(e.type, e, errs_by_type, key=_sort_error) - print(f"{'':=^80}\n{location + ' Errors Found:':^80}\n{'':=^80}") # noqa: T201 + total_errs = sum(len(errs) for errs in errs_by_type.values()) + print( # noqa: T201 + f"{'':=^80}\n{location + ' Errors Found:':^80}\n{_pluralize(total_errs, 'instance') + '; ' + _pluralize(len(errs_by_type), 'kind'):^80}\n{'':=^80}\n", + ) for errs in errs_by_type.values(): first_err = next(iter(errs)) # Just get the first error explanation = first_err.explain() - print(f"\n{first_err.type:^80}\n{explanation.explanation}\n---") # noqa: T201 + print( # noqa: T201 + f"{first_err.type + ' (' + str(len(errs)) + ')':^80}\n{explanation.explanation}\n---", + ) print( # noqa: T201 textwrap.indent( "\n".join( From 1d366c27aa00deb2aedfe0746098714ec5fee04d Mon Sep 17 00:00:00 2001 From: SiriusStarr <2049163+SiriusStarr@users.noreply.github.com> Date: Mon, 27 Jul 2026 18:58:52 -0700 Subject: [PATCH 20/23] Fix CI Yes, I know, never call a commit that, because it's cursed... --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c10ab41..db56a44 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: [3.11", "3.12", "3.13", "3.14"] + python-version: ["3.11", "3.12", "3.13", "3.14"] # Steps represent a sequence of tasks that will be executed as part of the job From 2fc48337e9cbd54e9c1aba0df97215f2c5f0323d Mon Sep 17 00:00:00 2001 From: SiriusStarr <2049163+SiriusStarr@users.noreply.github.com> Date: Tue, 28 Jul 2026 15:00:13 -0700 Subject: [PATCH 21/23] Add support for JSON JDexes --- configs/README.md | 25 +++- configs/json_jdex.toml | 110 +++++++++++++++ jdlint.py | 102 +++++++++++--- .../11 Me/11.01 Birth certificate/Content | 0 .../11 Me/11.02 Passpor/Content | 0 .../12.01 Insurance policies/Content | 0 .../12 Household/12.02 Missing/Content | 0 tests/json_jdex/jdex.jd.json | 41 ++++++ tests/json_jdex/jdlint.toml | 47 +++++++ tests/json_jdex/result.json | 129 ++++++++++++++++++ 10 files changed, 430 insertions(+), 24 deletions(-) create mode 100644 configs/json_jdex.toml create mode 100644 tests/json_jdex/files/A01 My life/10-19 Life admin/11 Me/11.01 Birth certificate/Content create mode 100644 tests/json_jdex/files/A01 My life/10-19 Life admin/11 Me/11.02 Passpor/Content create mode 100644 tests/json_jdex/files/A01 My life/10-19 Life admin/12 Household/12.01 Insurance policies/Content create mode 100644 tests/json_jdex/files/A01 My life/10-19 Life admin/12 Household/12.02 Missing/Content create mode 100644 tests/json_jdex/jdex.jd.json create mode 100644 tests/json_jdex/jdlint.toml create mode 100644 tests/json_jdex/result.json diff --git a/configs/README.md b/configs/README.md index 80ed350..5e09c40 100644 --- a/configs/README.md +++ b/configs/README.md @@ -19,7 +19,9 @@ well-documented with comments and demonstrate the full range of capabilities. You're strongly encouraged to read through the entirety of one of the configs -## LAS/SBS +## JDex Structure + +### Notes on Disk [Partially-nested](./partially_nested_jdex.toml) should work mostly out-of-the-box with the Life Admin System or Small Business System. You'll need @@ -30,11 +32,27 @@ demonstrate the existence of certain options. longer have "canon" downloads available for them, so they will likely need more tweaking to make work. +### Single File + +jdlint supports the official index specification as defined +[here](https://github.com/johnnydecimal/index-spec). + +[JSON JDex](./json_jdex.toml) and [Plaintext JDex](./plaintext_jdex.toml) +demonstrate loading from (respectively) the JSON and plaintext standards. + +If you have your JDex stored in e.g. some database, but are capable of getting +them exported to the JSON standard, this will allow you to still use jdlint. + +Note that this format is extremely restrictive to the exact standard, as the +standard lacks expressiveness for alternative formats. + +### None + [No JDex](./no_jdex.toml) is exactly what it says on the label; this is generally an inferior mode to run jdlint in, since it many checks are impossible, but it may be necessary depending on how you store your JDex. -## "SiriusStarr" +### "SiriusStarr" [SiriusStarr](./SiriusStarr.toml) is an example config for a very non-standard system that showcases the ability of jdlint to adapt to flexible system @@ -96,6 +114,9 @@ It will *not* match the following: * `12.35 .md` -- (Wildcard segments must match at least 1 character, not nothing) +**Note:** If you know regex, this is equivalent to `.+?` under the hood, i.e. +lazily matching one or more character. + ### Bound Segments Bound segments refer to variable segments that were defined earlier in the diff --git a/configs/json_jdex.toml b/configs/json_jdex.toml new file mode 100644 index 0000000..a2b4519 --- /dev/null +++ b/configs/json_jdex.toml @@ -0,0 +1,110 @@ +## ############################################################################# +# The linter section handles global settings +## ############################################################################# +[linter] +# Globally disable one or more rules by name, e.g. with `disable_rules = ["ID_DIFFERENT_FROM_JDEX"]` +disable_rules = [] +# Output machine-readable JSON instead of human-readable terminal output +json_output = false +# File patterns to ignore anywhere they appear; these support rudimentary globbing +ignore = [".stignore", ".stfolder", ".git*"] + +## ############################################################################# +# The system section handles defining your system format +## ############################################################################# +[system] +## ############################################################################# +# The system.jdex section defines how your JDex is stored on the disk +## ############################################################################# +[system.jdex] +# The path that the JSON file is located at. jdlint will automatically detect +# that it is a file, not a folder. +path = "~/jdex.jd.json" + +# Only one configuration option is available for single-file JDexes, which is a +# format for how to create entries out of the data in the file. Only two bound +# segments will be available, `/=id/` and `/=title`. You may use this to e.g. +# replace the space that normally comes between an ID and its title with an +# underscore, should you do that. +entry = "/=id/ /=title/" + +## ############################################################################# +# system.default defines the *default* structure used by any roots to check +## ############################################################################# +[system.default] +# At any level, we can define ".children" to specify one or more formats of +# folders to be allowed within the current level; here, we are defining the +# topmost level. +# +# Note that children are always matched sequentially as they are defined in the +# config file, meaning you can define a more narrow case first and then a more +# broad case later. +[[system.default.children]] +# The name to refer to the folder type as in errors/JSON output +name = "Area" +# The expected format of the folder +format = "/#A/0-/=A/9 /*Area/" +# The ID of this entry; this will be checked for uniqueness and matched to the +# JDex. Note that you can use bound segments in this. +id = "/=A/0-/=A/9" + +# Here we are defining the child folders of the top-level area folders +[[system.default.children.children]] +name = "Category" +format = "/=A//#C/ /*Category/" # Note that we're using bound segments from the parent area +id = "/=A//=C/" + +# Here, we define a folder without any children; that tells jdlint that it +# should be empty. This is useful for Headers and Inboxes. +[[system.default.children.children.children]] +name = "Header" +format = "/=A//=C/./#I/0 ■ /*Header/" +id = "/=A//=C/./=I/0" + +[[system.default.children.children.children]] +name = "Inbox" +format = "00.01 Inbox for the /*System/ System 📥" +id = "00.01" + +[[system.default.children.children.children]] +name = "Inbox" +format = "/=A/0.01 Inbox for area /=A/0-/=A/9 📥" +id = "/=A/0.01" + +[[system.default.children.children.children]] +name = "Inbox" +format = "/=A//=C/.01 Inbox for category /=A//=C/" +id = "/=A//=C/.01" + +# Here, we're disallowing inboxes (AC.01) that don't match the exact naming scheme above. +[[system.default.children.children.children]] +name = "Invalid Inbox" +format = "/=A//=C/.01 /*Inbox/" +id = "/=A//=C/.01" +forbidden = true # This just causes jdlint to fail anything that matches the format + +# Here, we're defining our terminal ID folders +[[system.default.children.children.children]] +name = "ID" +format = "/=A//=C/./##ID/ /*IDName/" +id = "/=A//=C/./=ID/" +can_be_file = true # This allows us to have a file as a terminal folder; you might like this for notes, for example +allow_arbitrary_contents = true # This tells jdlint to tolerate absolutely anything in this folder + +## ############################################################################# +# system.roots define folders that should be organized according to your JD +# system; for example, you might have ~/Documents as one root and ~/Dropbox +# as another. +## ############################################################################# +[[system.roots]] +# The name to refer to the root as in errors/JSON output +name = "Documents" +# The path to the files +path = "~/Documents" +# Any ignore patterns that apply only to that set of files +ignore = [".trash"] + +[[system.roots]] +name = "Dropbox" +path = "~/Dropbox" +ignore = [] diff --git a/jdlint.py b/jdlint.py index ea65720..89d6bb0 100755 --- a/jdlint.py +++ b/jdlint.py @@ -208,15 +208,6 @@ def __init__(self, at: str, from_file: dict) -> None: ).expanduser() self.ignore = _pop_ignore_list(at, from_file) - # Validate path - if not self.path.is_dir(): - err = ConfigValueError( - f"{at}.path", - "JDex path isn't a folder that exists!", - str(self.path), - ) - raise err - self.children = [ ConfigJDexTier( f"{at}.children[{i}]", @@ -234,6 +225,30 @@ def __init__(self, at: str, from_file: dict) -> None: for i, v in enumerate(_pop_default_empty_list(at, "notes", from_file)) ] + # Validate path + if not self.path.is_dir(): + if not self.path.is_file(): + # Something's weird + err = ConfigValueError( + f"{at}.path", + "JDex path isn't a folder or file that exists!", + str(self.path), + ) + raise err + + # We have a file-based JDex; that's fine + if self.children or self.notes or self.ignore: + err = ConfigConflictError( + at, + "Single file JDexes must not specify children or notes or ignore!", + ) + # Load format + self.entry = ConfigStaticFormat( + f"{at}.entry", + # This is the default info made available to all file JDexes + ConfigFormatAncestorInfo(("Single File JDex",), ("id", "title")), + _pop_nonempty_str_attribute(at, "entry", from_file), + ) _report_extra_keys(at, from_file, tuple(self.__dict__.keys())) @@ -678,7 +693,7 @@ def __init__( new_segments.append(identifier) if segment_type == "*": regex.append( - lambda _, identifier=identifier: f"(?P<{identifier}>.+)", + lambda _, identifier=identifier: f"(?P<{identifier}>.+?)", ) else: # Must be a ## type variable @@ -732,7 +747,7 @@ def explain(self) -> _Explanation: class JDexIssue: """A single error detected in the JDex.""" - file: PurePath + file: PurePath | None type = "" def display(self) -> str: @@ -915,7 +930,7 @@ class File: class JDexIssueDuplicateID(JDexIssue): """A JDex ID that has been used multiple times.""" - files: tuple[PurePath, ...] + files: tuple[PurePath | None, ...] id: str type: Literal["JDEX_DUPLICATE_ID"] = "JDEX_DUPLICATE_ID" @@ -1105,7 +1120,7 @@ class JDexEntry: """An entry in a JDex, including the path to the note that defined it.""" entry: str - path: PurePath + path: PurePath | None @dataclass(frozen=True) @@ -1161,7 +1176,7 @@ def _sort_jdex_error(e: JDexIssue) -> tuple[str, tuple[tuple[str, ...], str]]: raise NotImplementedError return ( e.type, - (e.file.parent.parts, e.file.name), + (e.file.parent.parts, e.file.name) if e.file else ((), ""), ) @@ -1177,7 +1192,7 @@ def _sort_error(e: Issue) -> tuple[str, tuple[tuple[str, ...], str]]: def _sort_jdex_entry(e: JDexEntry) -> tuple[str, PurePath]: - return (e.entry, e.path) + return (e.entry, e.path or PurePath()) def _entry_is_ignored( @@ -1211,6 +1226,40 @@ def _insert_concat_sorted(k, vs: list, d, key=None) -> None: # noqa: ANN001 d[k].sort(key=key) +def _get_jdex_entries_from_json( + jdex: ConfigSystemJDex, json: dict +) -> tuple[dict[str, list[JDexEntry]], list[JDexIssue]]: + accumulated_entries: dict[str, list[JDexEntry]] = {} + accumulated_errors: list[JDexIssue] = [] + for jid, v in json.items(): + _insert_append_sorted( + jid, + JDexEntry( + jdex.entry.build( + {"id": jid, "title": v["title"]}, + ), + None, + ), + accumulated_entries, + key=_sort_jdex_entry, + ) + return (accumulated_entries, accumulated_errors) + + +def _get_jdex_entries_from_file( + path: Path, + jdex: ConfigSystemJDex, +) -> tuple[dict[str, list[JDexEntry]], list[JDexIssue]]: + # Load file + as_text = Path.read_text(path) + try: + return _get_jdex_entries_from_json(jdex, json.loads(as_text)) + + except json.JSONDecodeError: + # This isn't valid json, so it must be plaintext + raise NotImplementedError + + def _get_jdex_entries_here_or_children( ignored: list[str], root_path: PurePath, @@ -1357,13 +1406,22 @@ def _process_jdex( ignored: list[str], jdex: ConfigSystemJDex, ) -> tuple[dict[str, list[JDexEntry]], list[JDexIssue]]: - (jdex_entries_by_id, jdex_errors) = _get_jdex_entries_here_or_children( - ignored + jdex.ignore, - jdex.path, - {}, - jdex.path, - jdex, - ) + # We need to first see if we have a single file, or a folder + if not getattr(jdex, "entry", False): + # Normal note-based JDex + (jdex_entries_by_id, jdex_errors) = _get_jdex_entries_here_or_children( + ignored + jdex.ignore, + jdex.path, + {}, + jdex.path, + jdex, + ) + else: + # Single file JDex + (jdex_entries_by_id, jdex_errors) = _get_jdex_entries_from_file( + jdex.path, + jdex, + ) # Check for duplicate ids duplicate_id_errors = [ diff --git a/tests/json_jdex/files/A01 My life/10-19 Life admin/11 Me/11.01 Birth certificate/Content b/tests/json_jdex/files/A01 My life/10-19 Life admin/11 Me/11.01 Birth certificate/Content new file mode 100644 index 0000000..e69de29 diff --git a/tests/json_jdex/files/A01 My life/10-19 Life admin/11 Me/11.02 Passpor/Content b/tests/json_jdex/files/A01 My life/10-19 Life admin/11 Me/11.02 Passpor/Content new file mode 100644 index 0000000..e69de29 diff --git a/tests/json_jdex/files/A01 My life/10-19 Life admin/12 Household/12.01 Insurance policies/Content b/tests/json_jdex/files/A01 My life/10-19 Life admin/12 Household/12.01 Insurance policies/Content new file mode 100644 index 0000000..e69de29 diff --git a/tests/json_jdex/files/A01 My life/10-19 Life admin/12 Household/12.02 Missing/Content b/tests/json_jdex/files/A01 My life/10-19 Life admin/12 Household/12.02 Missing/Content new file mode 100644 index 0000000..e69de29 diff --git a/tests/json_jdex/jdex.jd.json b/tests/json_jdex/jdex.jd.json new file mode 100644 index 0000000..800f10a --- /dev/null +++ b/tests/json_jdex/jdex.jd.json @@ -0,0 +1,41 @@ +{ + "A01": { + "type": "system", + "title": "My life" + }, + "10-19": { + "type": "area", + "title": "Life admin" + }, + "11": { + "type": "category", + "title": "Me" + }, + "11.01": { + "type": "id", + "title": "Birth certificate" + }, + "11.02": { + "type": "id", + "title": "Passport", + "metadata": { + "relatesTo": [ + "11.01" + ], + "expiryDate": "2028-04-15" + } + }, + "12": { + "type": "category", + "title": "Household" + }, + "12.01": { + "type": "id", + "title": "Insurance policies", + "metadata": { + "url": [ + "https://example.com/policy" + ] + } + } +} \ No newline at end of file diff --git a/tests/json_jdex/jdlint.toml b/tests/json_jdex/jdlint.toml new file mode 100644 index 0000000..2d73c5d --- /dev/null +++ b/tests/json_jdex/jdlint.toml @@ -0,0 +1,47 @@ +[linter] +json_output = true +ignore = [".placeholder"] + +[[system.roots]] +name = "Files" +path = "files" + +[system.jdex] +path = "jdex.jd.json" +entry = "/=id/ /=title/" + +[[system.default.children]] +name = "System" +format = "/*SYS/ /*System/" +id = "/=SYS/" + +[[system.default.children.children]] +name = "Area" +format = "/#A/0-/=A/9 /*Area/" +id = "/=A/0-/=A/9" + +[[system.default.children.children.children]] +name = "10 Category" +format = "10 /*Category/" +id = "10" +no_jdex_entry = true + +[[system.default.children.children.children]] +name = "Category" +format = "/=A//#C/ /*Category/" +id = "/=A//=C/" + +[[system.default.children.children.children.children]] +name = "ID" +format = "/=A//=C/.12 Myslef" +id = "/=A//=C/.12" +jdex_entry = "/=A//=C/.12 Myself" +can_be_file = true +allow_arbitrary_contents = true + +[[system.default.children.children.children.children]] +name = "ID" +format = "/=A//=C/./##ID/ /*IDName/" +id = "/=A//=C/./=ID/" +can_be_file = true +allow_arbitrary_contents = true diff --git a/tests/json_jdex/result.json b/tests/json_jdex/result.json new file mode 100644 index 0000000..41894ec --- /dev/null +++ b/tests/json_jdex/result.json @@ -0,0 +1,129 @@ +{ + "jdex": { + "errors": [], + "path": "jdex.jd.json", + "entries": { + "A01": [ + { + "entry": "A01 My life", + "path": null + } + ], + "10-19": [ + { + "entry": "10-19 Life admin", + "path": null + } + ], + "11": [ + { + "entry": "11 Me", + "path": null + } + ], + "11.01": [ + { + "entry": "11.01 Birth certificate", + "path": null + } + ], + "11.02": [ + { + "entry": "11.02 Passport", + "path": null + } + ], + "12": [ + { + "entry": "12 Household", + "path": null + } + ], + "12.01": [ + { + "entry": "12.01 Insurance policies", + "path": null + } + ] + } + }, + "roots": { + "Files": { + "errors": [ + { + "type": "ID_DIFFERENT_FROM_JDEX", + "id": "11.02", + "file": "A01 My life/10-19 Life admin/11 Me/11.02 Passpor", + "expected_jdex_entry": "11.02 Passpor", + "known_jdex_entries": [ + { + "entry": "11.02 Passport", + "path": null + } + ] + }, + { + "type": "ID_NOT_IN_JDEX", + "id": "12.02", + "file": "A01 My life/10-19 Life admin/12 Household/12.02 Missing" + } + ], + "path": "files", + "structure": { + "A01": [ + { + "path": "A01 My life", + "children": { + "10-19": [ + { + "path": "A01 My life/10-19 Life admin", + "children": { + "11": [ + { + "path": "A01 My life/10-19 Life admin/11 Me", + "children": { + "11.01": [ + { + "path": "A01 My life/10-19 Life admin/11 Me/11.01 Birth certificate", + "children": {} + } + ], + "11.02": [ + { + "path": "A01 My life/10-19 Life admin/11 Me/11.02 Passpor", + "children": {} + } + ] + } + } + ], + "12": [ + { + "path": "A01 My life/10-19 Life admin/12 Household", + "children": { + "12.01": [ + { + "path": "A01 My life/10-19 Life admin/12 Household/12.01 Insurance policies", + "children": {} + } + ], + "12.02": [ + { + "path": "A01 My life/10-19 Life admin/12 Household/12.02 Missing", + "children": {} + } + ] + } + } + ] + } + } + ] + } + } + ] + } + } + }, + "ignored_errs": 0 +} \ No newline at end of file From 4825de9dbe297103b34bef499e0b55cc740f10d6 Mon Sep 17 00:00:00 2001 From: SiriusStarr <2049163+SiriusStarr@users.noreply.github.com> Date: Tue, 28 Jul 2026 15:45:28 -0700 Subject: [PATCH 22/23] Add support for plaintext JDexes --- configs/README.md | 4 + configs/plaintext_jdex.toml | 110 +++++++++++++++++ jdlint.py | 26 +++- .../11 Me/11.01 Birth certificate/Content | 0 .../11 Me/11.02 Passpor/Content | 0 .../12.01 Insurance policies/Content | 0 .../12 Household/12.02 Missing/Content | 0 tests/plaintext_jdex/jdex.txt | 13 ++ tests/plaintext_jdex/jdlint.toml | 42 +++++++ tests/plaintext_jdex/result.json | 116 ++++++++++++++++++ 10 files changed, 310 insertions(+), 1 deletion(-) create mode 100644 configs/plaintext_jdex.toml create mode 100644 tests/plaintext_jdex/files/10-19 Life admin/11 Me/11.01 Birth certificate/Content create mode 100644 tests/plaintext_jdex/files/10-19 Life admin/11 Me/11.02 Passpor/Content create mode 100644 tests/plaintext_jdex/files/10-19 Life admin/12 Household/12.01 Insurance policies/Content create mode 100644 tests/plaintext_jdex/files/10-19 Life admin/12 Household/12.02 Missing/Content create mode 100644 tests/plaintext_jdex/jdex.txt create mode 100644 tests/plaintext_jdex/jdlint.toml create mode 100644 tests/plaintext_jdex/result.json diff --git a/configs/README.md b/configs/README.md index 5e09c40..91169bc 100644 --- a/configs/README.md +++ b/configs/README.md @@ -46,6 +46,10 @@ them exported to the JSON standard, this will allow you to still use jdlint. Note that this format is extremely restrictive to the exact standard, as the standard lacks expressiveness for alternative formats. +Additionally, note that jdlint does not check for adherence *to* the standard +and assumes any file you give it complies with it. It thus does not check for, +for example, orphans. + ### None [No JDex](./no_jdex.toml) is exactly what it says on the label; this is diff --git a/configs/plaintext_jdex.toml b/configs/plaintext_jdex.toml new file mode 100644 index 0000000..3f345c2 --- /dev/null +++ b/configs/plaintext_jdex.toml @@ -0,0 +1,110 @@ +## ############################################################################# +# The linter section handles global settings +## ############################################################################# +[linter] +# Globally disable one or more rules by name, e.g. with `disable_rules = ["ID_DIFFERENT_FROM_JDEX"]` +disable_rules = [] +# Output machine-readable JSON instead of human-readable terminal output +json_output = false +# File patterns to ignore anywhere they appear; these support rudimentary globbing +ignore = [".stignore", ".stfolder", ".git*"] + +## ############################################################################# +# The system section handles defining your system format +## ############################################################################# +[system] +## ############################################################################# +# The system.jdex section defines how your JDex is stored on the disk +## ############################################################################# +[system.jdex] +# The path that the JSON file is located at. jdlint will automatically detect +# that it is a file, not a folder. +path = "~/jdex.txt" + +# Only one configuration option is available for single-file JDexes, which is a +# format for how to create entries out of the data in the file. Only two bound +# segments will be available, `/=id/` and `/=title`. You may use this to e.g. +# replace the space that normally comes between an ID and its title with an +# underscore, should you do that. +entry = "/=id/ /=title/" + +## ############################################################################# +# system.default defines the *default* structure used by any roots to check +## ############################################################################# +[system.default] +# At any level, we can define ".children" to specify one or more formats of +# folders to be allowed within the current level; here, we are defining the +# topmost level. +# +# Note that children are always matched sequentially as they are defined in the +# config file, meaning you can define a more narrow case first and then a more +# broad case later. +[[system.default.children]] +# The name to refer to the folder type as in errors/JSON output +name = "Area" +# The expected format of the folder +format = "/#A/0-/=A/9 /*Area/" +# The ID of this entry; this will be checked for uniqueness and matched to the +# JDex. Note that you can use bound segments in this. +id = "/=A/0-/=A/9" + +# Here we are defining the child folders of the top-level area folders +[[system.default.children.children]] +name = "Category" +format = "/=A//#C/ /*Category/" # Note that we're using bound segments from the parent area +id = "/=A//=C/" + +# Here, we define a folder without any children; that tells jdlint that it +# should be empty. This is useful for Headers and Inboxes. +[[system.default.children.children.children]] +name = "Header" +format = "/=A//=C/./#I/0 ■ /*Header/" +id = "/=A//=C/./=I/0" + +[[system.default.children.children.children]] +name = "Inbox" +format = "00.01 Inbox for the /*System/ System 📥" +id = "00.01" + +[[system.default.children.children.children]] +name = "Inbox" +format = "/=A/0.01 Inbox for area /=A/0-/=A/9 📥" +id = "/=A/0.01" + +[[system.default.children.children.children]] +name = "Inbox" +format = "/=A//=C/.01 Inbox for category /=A//=C/" +id = "/=A//=C/.01" + +# Here, we're disallowing inboxes (AC.01) that don't match the exact naming scheme above. +[[system.default.children.children.children]] +name = "Invalid Inbox" +format = "/=A//=C/.01 /*Inbox/" +id = "/=A//=C/.01" +forbidden = true # This just causes jdlint to fail anything that matches the format + +# Here, we're defining our terminal ID folders +[[system.default.children.children.children]] +name = "ID" +format = "/=A//=C/./##ID/ /*IDName/" +id = "/=A//=C/./=ID/" +can_be_file = true # This allows us to have a file as a terminal folder; you might like this for notes, for example +allow_arbitrary_contents = true # This tells jdlint to tolerate absolutely anything in this folder + +## ############################################################################# +# system.roots define folders that should be organized according to your JD +# system; for example, you might have ~/Documents as one root and ~/Dropbox +# as another. +## ############################################################################# +[[system.roots]] +# The name to refer to the root as in errors/JSON output +name = "Documents" +# The path to the files +path = "~/Documents" +# Any ignore patterns that apply only to that set of files +ignore = [".trash"] + +[[system.roots]] +name = "Dropbox" +path = "~/Dropbox" +ignore = [] diff --git a/jdlint.py b/jdlint.py index 89d6bb0..86a798c 100755 --- a/jdlint.py +++ b/jdlint.py @@ -1246,6 +1246,30 @@ def _get_jdex_entries_from_json( return (accumulated_entries, accumulated_errors) +def _get_jdex_entries_from_text( + jdex: ConfigSystemJDex, text: str +) -> tuple[dict[str, list[JDexEntry]], list[JDexIssue]]: + accumulated_entries: dict[str, list[JDexEntry]] = {} + accumulated_errors: list[JDexIssue] = [] + id_regex = re.compile("^\\s*([0-9]{2}(?:[.-][0-9]{2})?) ([^/\\n]+?)\\s*(?:$|/.*)") + + for line in text.splitlines(): + match = id_regex.fullmatch(line) + if match: + _insert_append_sorted( + match.group(1), + JDexEntry( + jdex.entry.build( + {"id": match.group(1), "title": match.group(2)}, + ), + None, + ), + accumulated_entries, + key=_sort_jdex_entry, + ) + return (accumulated_entries, accumulated_errors) + + def _get_jdex_entries_from_file( path: Path, jdex: ConfigSystemJDex, @@ -1257,7 +1281,7 @@ def _get_jdex_entries_from_file( except json.JSONDecodeError: # This isn't valid json, so it must be plaintext - raise NotImplementedError + return _get_jdex_entries_from_text(jdex, as_text) def _get_jdex_entries_here_or_children( diff --git a/tests/plaintext_jdex/files/10-19 Life admin/11 Me/11.01 Birth certificate/Content b/tests/plaintext_jdex/files/10-19 Life admin/11 Me/11.01 Birth certificate/Content new file mode 100644 index 0000000..e69de29 diff --git a/tests/plaintext_jdex/files/10-19 Life admin/11 Me/11.02 Passpor/Content b/tests/plaintext_jdex/files/10-19 Life admin/11 Me/11.02 Passpor/Content new file mode 100644 index 0000000..e69de29 diff --git a/tests/plaintext_jdex/files/10-19 Life admin/12 Household/12.01 Insurance policies/Content b/tests/plaintext_jdex/files/10-19 Life admin/12 Household/12.01 Insurance policies/Content new file mode 100644 index 0000000..e69de29 diff --git a/tests/plaintext_jdex/files/10-19 Life admin/12 Household/12.02 Missing/Content b/tests/plaintext_jdex/files/10-19 Life admin/12 Household/12.02 Missing/Content new file mode 100644 index 0000000..e69de29 diff --git a/tests/plaintext_jdex/jdex.txt b/tests/plaintext_jdex/jdex.txt new file mode 100644 index 0000000..0d6d034 --- /dev/null +++ b/tests/plaintext_jdex/jdex.txt @@ -0,0 +1,13 @@ +10-19 Life admin // which I can comment like this + + 11 Me /* or like this */ + /* multiline comments + are allowed on their own lines + */ + 11.01 Birth certificate + - expiryDate: 2028-04-15 + 11.02 Passport + + 12 Household + 12.01 Insurance policies + - url: https://example.com/policy diff --git a/tests/plaintext_jdex/jdlint.toml b/tests/plaintext_jdex/jdlint.toml new file mode 100644 index 0000000..d1d0b34 --- /dev/null +++ b/tests/plaintext_jdex/jdlint.toml @@ -0,0 +1,42 @@ +[linter] +json_output = true +ignore = [".placeholder"] + +[[system.roots]] +name = "Files" +path = "files" + +[system.jdex] +path = "jdex.txt" +entry = "/=id/ /=title/" + +[[system.default.children]] +name = "Area" +format = "/#A/0-/=A/9 /*Area/" +id = "/=A/0-/=A/9" + +[[system.default.children.children]] +name = "10 Category" +format = "10 /*Category/" +id = "10" +no_jdex_entry = true + +[[system.default.children.children]] +name = "Category" +format = "/=A//#C/ /*Category/" +id = "/=A//=C/" + +[[system.default.children.children.children]] +name = "ID" +format = "/=A//=C/.12 Myslef" +id = "/=A//=C/.12" +jdex_entry = "/=A//=C/.12 Myself" +can_be_file = true +allow_arbitrary_contents = true + +[[system.default.children.children.children]] +name = "ID" +format = "/=A//=C/./##ID/ /*IDName/" +id = "/=A//=C/./=ID/" +can_be_file = true +allow_arbitrary_contents = true diff --git a/tests/plaintext_jdex/result.json b/tests/plaintext_jdex/result.json new file mode 100644 index 0000000..2bd23f0 --- /dev/null +++ b/tests/plaintext_jdex/result.json @@ -0,0 +1,116 @@ +{ + "jdex": { + "errors": [], + "path": "jdex.txt", + "entries": { + "10-19": [ + { + "entry": "10-19 Life admin", + "path": null + } + ], + "11": [ + { + "entry": "11 Me", + "path": null + } + ], + "11.01": [ + { + "entry": "11.01 Birth certificate", + "path": null + } + ], + "11.02": [ + { + "entry": "11.02 Passport", + "path": null + } + ], + "12": [ + { + "entry": "12 Household", + "path": null + } + ], + "12.01": [ + { + "entry": "12.01 Insurance policies", + "path": null + } + ] + } + }, + "roots": { + "Files": { + "errors": [ + { + "type": "ID_DIFFERENT_FROM_JDEX", + "id": "11.02", + "file": "10-19 Life admin/11 Me/11.02 Passpor", + "expected_jdex_entry": "11.02 Passpor", + "known_jdex_entries": [ + { + "entry": "11.02 Passport", + "path": null + } + ] + }, + { + "type": "ID_NOT_IN_JDEX", + "id": "12.02", + "file": "10-19 Life admin/12 Household/12.02 Missing" + } + ], + "path": "files", + "structure": { + "10-19": [ + { + "path": "10-19 Life admin", + "children": { + "11": [ + { + "path": "10-19 Life admin/11 Me", + "children": { + "11.01": [ + { + "path": "10-19 Life admin/11 Me/11.01 Birth certificate", + "children": {} + } + ], + "11.02": [ + { + "path": "10-19 Life admin/11 Me/11.02 Passpor", + "children": {} + } + ] + } + } + ], + "12": [ + { + "path": "10-19 Life admin/12 Household", + "children": { + "12.01": [ + { + "path": "10-19 Life admin/12 Household/12.01 Insurance policies", + "children": {} + } + ], + "12.02": [ + { + "path": "10-19 Life admin/12 Household/12.02 Missing", + "children": {} + } + ] + } + } + ] + } + } + ] + } + } + }, + "ignored_errs": 0 +} \ No newline at end of file From 9f80dfea16c23fdc73a661926dea565c158aeca8 Mon Sep 17 00:00:00 2001 From: SiriusStarr <2049163+SiriusStarr@users.noreply.github.com> Date: Tue, 28 Jul 2026 15:47:39 -0700 Subject: [PATCH 23/23] Fix linter errors --- configs/README.md | 7 +++++-- jdlint.py | 11 +++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/configs/README.md b/configs/README.md index 91169bc..8cdcaed 100644 --- a/configs/README.md +++ b/configs/README.md @@ -2,8 +2,11 @@ * [jdlint.toml](#jdlinttoml) * [Introduction](#introduction) - * [LAS/SBS](#lassbs) - * ["SiriusStarr"](#siriusstarr) + * [JDex Structure](#jdex-structure) + * [Notes on Disk](#notes-on-disk) + * [Single File](#single-file) + * [None](#none) + * ["SiriusStarr"](#siriusstarr) * [Formats](#formats) * [Literal Segments](#literal-segments) * [Variable Segments](#variable-segments) diff --git a/jdlint.py b/jdlint.py index 86a798c..7609a1a 100755 --- a/jdlint.py +++ b/jdlint.py @@ -242,10 +242,11 @@ def __init__(self, at: str, from_file: dict) -> None: at, "Single file JDexes must not specify children or notes or ignore!", ) + raise ConfigConflictError # Load format self.entry = ConfigStaticFormat( f"{at}.entry", - # This is the default info made available to all file JDexes + # This is the default info made available to a file JDex ConfigFormatAncestorInfo(("Single File JDex",), ("id", "title")), _pop_nonempty_str_attribute(at, "entry", from_file), ) @@ -1227,7 +1228,8 @@ def _insert_concat_sorted(k, vs: list, d, key=None) -> None: # noqa: ANN001 def _get_jdex_entries_from_json( - jdex: ConfigSystemJDex, json: dict + jdex: ConfigSystemJDex, + json: dict, ) -> tuple[dict[str, list[JDexEntry]], list[JDexIssue]]: accumulated_entries: dict[str, list[JDexEntry]] = {} accumulated_errors: list[JDexIssue] = [] @@ -1247,7 +1249,8 @@ def _get_jdex_entries_from_json( def _get_jdex_entries_from_text( - jdex: ConfigSystemJDex, text: str + jdex: ConfigSystemJDex, + text: str, ) -> tuple[dict[str, list[JDexEntry]], list[JDexIssue]]: accumulated_entries: dict[str, list[JDexEntry]] = {} accumulated_errors: list[JDexIssue] = [] @@ -1280,7 +1283,7 @@ def _get_jdex_entries_from_file( return _get_jdex_entries_from_json(jdex, json.loads(as_text)) except json.JSONDecodeError: - # This isn't valid json, so it must be plaintext + # This isn't valid JSON, so it must be plaintext return _get_jdex_entries_from_text(jdex, as_text)