|
9 | 9 |
|
10 | 10 | from robot.errors import DataError |
11 | 11 |
|
12 | | -from robocop.linter.utils.misc import ROBOT_VERSION |
13 | | - |
14 | 12 | try: |
15 | 13 | from robot.api import Languages # RF 6.0 |
16 | 14 | except ImportError: |
17 | 15 | Languages = None |
18 | | -import pathspec |
19 | 16 | import typer |
20 | 17 | from typing_extensions import Self |
21 | 18 |
|
22 | | -from robocop import exceptions, files |
23 | | -from robocop.cache import RobocopCache |
| 19 | +from robocop import exceptions |
24 | 20 | from robocop.formatter import formatters |
25 | 21 | from robocop.formatter.skip import SkipConfig |
26 | | -from robocop.formatter.utils import misc # TODO merge with linter misc |
27 | 22 | from robocop.linter import rules |
28 | 23 | from robocop.linter.rules import ( |
29 | 24 | AfterRunChecker, |
|
32 | 27 | RuleSeverity, |
33 | 28 | ) |
34 | 29 | from robocop.linter.utils.misc import compile_rule_pattern |
35 | | -from robocop.linter.utils.version_matching import Version |
| 30 | +from robocop.version_handling import ROBOT_VERSION, Version |
36 | 31 |
|
37 | | -CONFIG_NAMES = frozenset(("robocop.toml", "pyproject.toml", "robot.toml")) |
38 | 32 | DEFAULT_INCLUDE = frozenset(("*.robot", "*.resource")) |
39 | 33 | DEFAULT_EXCLUDE = frozenset((".direnv", ".eggs", ".git", ".svn", ".hg", ".nox", ".tox", ".venv", "venv", "dist")) |
40 | 34 |
|
41 | 35 | DEFAULT_ISSUE_FORMAT = "{source}:{line}:{col} [{severity}] {rule_id} {desc} ({name})" |
42 | 36 |
|
43 | 37 | if TYPE_CHECKING: |
44 | 38 | import re |
45 | | - from collections.abc import Generator |
46 | 39 |
|
47 | 40 | from robocop.linter.rules import Rule |
48 | 41 |
|
@@ -167,10 +160,10 @@ def validate_target_version(value: str | TargetVersion | None) -> int | None: |
167 | 160 | raise typer.BadParameter( |
168 | 161 | f"Invalid target Robot Framework version: '{value}' is not one of {versions}" |
169 | 162 | ) from None |
170 | | - if target_version > misc.ROBOT_VERSION.major: |
| 163 | + if target_version > ROBOT_VERSION.major: |
171 | 164 | raise typer.BadParameter( |
172 | 165 | f"Target Robot Framework version ({target_version}) should not be higher than " |
173 | | - f"installed version ({misc.ROBOT_VERSION})." |
| 166 | + f"installed version ({ROBOT_VERSION})." |
174 | 167 | ) from None |
175 | 168 | return target_version |
176 | 169 |
|
@@ -442,7 +435,7 @@ class FormatterConfig: |
442 | 435 | configure: list[str] | None = field(default_factory=list) |
443 | 436 | force_order: bool | None = False |
444 | 437 | allow_disabled: bool | None = False |
445 | | - target_version: int | str | None = field(default=misc.ROBOT_VERSION.major, compare=False) |
| 438 | + target_version: int | str | None = field(default=ROBOT_VERSION.major, compare=False) |
446 | 439 | skip_config: SkipConfig = field(default_factory=SkipConfig) |
447 | 440 | overwrite: bool | None = None |
448 | 441 | diff: bool | None = False |
@@ -720,7 +713,7 @@ class Config: |
720 | 713 | languages: Languages | None = field(default=None, compare=False) |
721 | 714 | verbose: bool | None = field(default_factory=bool) |
722 | 715 | silent: bool | None = field(default_factory=bool) |
723 | | - target_version: int | str | None = misc.ROBOT_VERSION.major |
| 716 | + target_version: int | str | None = ROBOT_VERSION.major |
724 | 717 | _hash: int | None = None |
725 | 718 | config_source: str = "cli" |
726 | 719 |
|
@@ -892,247 +885,3 @@ def hash(self) -> str: |
892 | 885 | hasher.update(language_str.encode("utf-8")) |
893 | 886 | self._hash = hasher.hexdigest() |
894 | 887 | return self._hash |
895 | | - |
896 | | - |
897 | | -class GitIgnoreResolver: |
898 | | - def __init__(self): |
899 | | - self.cached_ignores: dict[Path, list[pathspec.PathSpec] | None] = {} |
900 | | - self.ignore_dirs: set[Path] = set() |
901 | | - |
902 | | - def path_excluded(self, path: Path, gitignores: list[tuple[Path, pathspec.PathSpec]]) -> bool: |
903 | | - """Find path gitignores and check if file is excluded.""" |
904 | | - if not gitignores: |
905 | | - return False |
906 | | - for gitignore_path, gitignore in gitignores: |
907 | | - relative_path = files.get_relative_path(path, gitignore_path) |
908 | | - path = str(relative_path) |
909 | | - # fixes a bug in pathspec where directory needs to end with / to be ignored by pattern |
910 | | - if relative_path.is_dir() and path != ".": |
911 | | - path = f"{path}{os.sep}" |
912 | | - if gitignore.match_file(path): |
913 | | - return True |
914 | | - return False |
915 | | - |
916 | | - def read_gitignore(self, path: Path) -> pathspec.PathSpec: |
917 | | - """Return a PathSpec loaded from the file.""" |
918 | | - with path.open(encoding="utf-8") as gf: |
919 | | - lines = gf.readlines() |
920 | | - return pathspec.PathSpec.from_lines(pathspec.patterns.GitWildMatchPattern, lines) |
921 | | - |
922 | | - def resolve_path_ignores(self, path: Path) -> list[tuple[Path, pathspec.PathSpec]]: |
923 | | - """ |
924 | | - Visit all parent directories and find all gitignores. |
925 | | -
|
926 | | - Gitignores are cached for multiple sources. |
927 | | -
|
928 | | - Args: |
929 | | - path: path to file/directory |
930 | | -
|
931 | | - Returns: |
932 | | - PathSpec from merged gitignores. |
933 | | -
|
934 | | - """ |
935 | | - # TODO: respect nogitignore flag |
936 | | - if path.is_file(): |
937 | | - path = path.parent |
938 | | - gitignores = [] |
939 | | - search_paths = (parent for parent in [path, *path.parents]) |
940 | | - for parent_path in search_paths: |
941 | | - if parent_path in self.ignore_dirs: # dir that does not have .gitignore (marked as such) |
942 | | - gitignores.extend([self.cached_ignores[path] for path in search_paths if path in self.cached_ignores]) |
943 | | - break |
944 | | - if parent_path in self.cached_ignores: |
945 | | - gitignores.append(self.cached_ignores[parent_path]) |
946 | | - # if any parent is cached, we can retrieve any parent with gitignore from cache and return early |
947 | | - gitignores.extend([self.cached_ignores[path] for path in search_paths if path in self.cached_ignores]) |
948 | | - break |
949 | | - if (gitignore_path := parent_path / ".gitignore").is_file(): |
950 | | - gitignore = self.read_gitignore(gitignore_path) |
951 | | - self.cached_ignores[parent_path] = (parent_path, gitignore) |
952 | | - gitignores.append((parent_path, gitignore)) |
953 | | - else: |
954 | | - self.ignore_dirs.add(parent_path) |
955 | | - if (parent_path / ".git").is_dir(): |
956 | | - break |
957 | | - return gitignores |
958 | | - |
959 | | - |
960 | | -class ConfigManager: |
961 | | - """ |
962 | | - Finds and loads configuration files for each file. |
963 | | -
|
964 | | - Config provided from cli takes priority. ``--config`` option overrides any found configuration file. |
965 | | - """ |
966 | | - |
967 | | - def __init__( |
968 | | - self, |
969 | | - sources: list[str] | None = None, |
970 | | - config: Path | None = None, |
971 | | - root: str | None = None, |
972 | | - ignore_git_dir: bool = False, |
973 | | - ignore_file_config: bool = False, |
974 | | - skip_gitignore: bool = False, |
975 | | - force_exclude: bool = False, |
976 | | - overwrite_config: Config | None = None, |
977 | | - ): |
978 | | - """ |
979 | | - Initialize ConfigManager. |
980 | | -
|
981 | | - Args: |
982 | | - sources: List of sources with Robot Framework files. |
983 | | - config: Path to configuration file. |
984 | | - root: Root of the project. Can be supplied if it's known beforehand (for example by IDE plugin) |
985 | | - Otherwise it will be automatically found. |
986 | | - ignore_git_dir: Flag for project root discovery to decide if directories with `.git` should be ignored. |
987 | | - ignore_file_config: If set to True, Robocop will not load found configuration files |
988 | | - skip_gitignore: Do not load .gitignore files when looking for the files to parse |
989 | | - force_exclude: Enforce exclusions, even for paths passed directly in the command-line |
990 | | - overwrite_config: Overwrite existing configuration file with the Config class |
991 | | -
|
992 | | - """ |
993 | | - self.cached_configs: dict[Path, Config] = {} |
994 | | - self.overwrite_config = overwrite_config |
995 | | - self.ignore_git_dir = ignore_git_dir |
996 | | - self.ignore_file_config = ignore_file_config |
997 | | - self.force_exclude = force_exclude |
998 | | - self.skip_gitignore = skip_gitignore |
999 | | - self.gitignore_resolver = GitIgnoreResolver() |
1000 | | - self.overridden_config = ( |
1001 | | - config is not None |
1002 | | - ) # TODO: what if both cli and --config? should take --config then apply cli |
1003 | | - self.root = root or Path.cwd() |
1004 | | - self.sources = sources |
1005 | | - self.default_config: Config = self.get_default_config(config) |
1006 | | - self._paths: dict[Path, Config] | None = None |
1007 | | - self._cache: RobocopCache | None = None |
1008 | | - |
1009 | | - @property |
1010 | | - def cache(self) -> RobocopCache: |
1011 | | - """Get the file cache, initializing it lazily if needed.""" |
1012 | | - if self._cache is None: |
1013 | | - cache_config = self.default_config.cache |
1014 | | - self._cache = RobocopCache( |
1015 | | - cache_dir=cache_config.cache_dir if cache_config else None, |
1016 | | - enabled=cache_config.enabled if cache_config else True, |
1017 | | - verbose=self.default_config.verbose or False, |
1018 | | - ) |
1019 | | - return self._cache |
1020 | | - |
1021 | | - @property |
1022 | | - def paths(self) -> Generator[tuple[Path, Config], None, None]: |
1023 | | - # TODO: what if we provide the same path twice - tests |
1024 | | - if self._paths is None: |
1025 | | - self._paths = {} |
1026 | | - sources = self.sources if self.sources else self.default_config.sources |
1027 | | - ignore_file_filters = not self.force_exclude and bool(sources) |
1028 | | - self.resolve_paths(sources, ignore_file_filters=ignore_file_filters) |
1029 | | - yield from self._paths.items() |
1030 | | - |
1031 | | - def get_default_config(self, config_path: Path | None) -> Config: |
1032 | | - """Get default config either from --config option or from the cli.""" |
1033 | | - if config_path: |
1034 | | - configuration = files.read_toml_config(config_path) |
1035 | | - config = Config.from_toml(configuration, config_path.resolve()) |
1036 | | - elif not self.ignore_file_config: |
1037 | | - sources = [Path(path).resolve() for path in self.sources] if self.sources else [Path.cwd()] |
1038 | | - directories = files.get_common_parent_dirs(sources) |
1039 | | - config = self.find_config_in_dirs(directories, default=None) |
1040 | | - if not config: |
1041 | | - config = Config() |
1042 | | - self.cached_configs.update(dict.fromkeys(directories, config)) |
1043 | | - else: |
1044 | | - config = Config() |
1045 | | - config.overwrite_from_config(self.overwrite_config) |
1046 | | - return config |
1047 | | - |
1048 | | - def is_git_project_root(self, path: Path) -> bool: |
1049 | | - """Check if current directory contains .git directory and might be a project root.""" |
1050 | | - if self.ignore_git_dir: |
1051 | | - return False |
1052 | | - return (path / ".git").is_dir() |
1053 | | - |
1054 | | - def find_closest_config(self, source: Path, default: Config | None) -> Config: |
1055 | | - """Look in the directory and its parents for the closest valid configuration file.""" |
1056 | | - return self.find_config_in_dirs(source.parents, default) |
1057 | | - |
1058 | | - def find_config_in_dirs(self, directories: list[Path], default: Config | None) -> Config: |
1059 | | - seen = [] # if we find config, mark all visited directories with resolved config |
1060 | | - for check_dir in directories: |
1061 | | - if check_dir in self.cached_configs: |
1062 | | - return self.cached_configs[check_dir] |
1063 | | - seen.append(check_dir) |
1064 | | - for config_filename in CONFIG_NAMES: |
1065 | | - if (config_path := (check_dir / config_filename)).is_file(): |
1066 | | - configuration = files.read_toml_config(config_path) |
1067 | | - if configuration: |
1068 | | - config = Config.from_toml(configuration, config_path) |
1069 | | - config.overwrite_from_config(self.overwrite_config) # TODO those two lines together |
1070 | | - self.cached_configs.update(dict.fromkeys(seen, config)) |
1071 | | - if config.verbose: |
1072 | | - print(f"Loaded {config_path} configuration file.") |
1073 | | - return config |
1074 | | - if self.is_git_project_root(check_dir): |
1075 | | - break |
1076 | | - |
1077 | | - if default: |
1078 | | - self.cached_configs.update(dict.fromkeys(seen, default)) |
1079 | | - return default |
1080 | | - |
1081 | | - def get_config_for_source_file(self, source_file: Path) -> Config: |
1082 | | - """ |
1083 | | - Find the closest config to the source file or directory. |
1084 | | -
|
1085 | | - If it was loaded before it will be returned from the cache. Otherwise, we will load it and save it to cache |
1086 | | - first. |
1087 | | -
|
1088 | | - Args: |
1089 | | - source_file: Path to Robot Framework source file or directory. |
1090 | | -
|
1091 | | - """ |
1092 | | - if self.overridden_config or self.ignore_file_config: |
1093 | | - return self.default_config |
1094 | | - return self.find_closest_config(source_file, self.default_config) |
1095 | | - |
1096 | | - def resolve_paths( |
1097 | | - self, |
1098 | | - sources: list[str | Path], |
1099 | | - ignore_file_filters: bool = False, |
1100 | | - ) -> None: |
1101 | | - """ |
1102 | | - Find all files to parse and their corresponding configs. |
1103 | | -
|
1104 | | - Initially sources can be ["."] (if not path provided, assume current working directory). |
1105 | | - It can be also any list of paths, for example ["tests/", "file.robot"]. |
1106 | | -
|
1107 | | - Args: |
1108 | | - sources: list of sources from CLI or configuration file. |
1109 | | - gitignores: list of gitignore pathspec and their locations for path resolution. |
1110 | | - ignore_file_filters: force robocop to parse file even if it's excluded in the configuration |
1111 | | -
|
1112 | | - """ |
1113 | | - source_gitignore = None |
1114 | | - config = None |
1115 | | - for source in sources: |
1116 | | - source_not_resolved = Path(source) |
1117 | | - source = source_not_resolved.resolve() |
1118 | | - if source in self._paths: |
1119 | | - continue |
1120 | | - if not source.exists(): |
1121 | | - if source_not_resolved.is_symlink(): # i.e. dangling symlink |
1122 | | - continue |
1123 | | - raise exceptions.FatalError(f"File '{source}' does not exist") |
1124 | | - if config is None: # first file in the directory |
1125 | | - config = self.get_config_for_source_file(source) |
1126 | | - if not ignore_file_filters: |
1127 | | - if config.file_filters.path_excluded(source_not_resolved): |
1128 | | - continue |
1129 | | - if source.is_file() and not config.file_filters.path_included(source_not_resolved): |
1130 | | - continue |
1131 | | - if not self.skip_gitignore: |
1132 | | - source_gitignore = self.gitignore_resolver.resolve_path_ignores(source_not_resolved) |
1133 | | - if self.gitignore_resolver.path_excluded(source_not_resolved, source_gitignore): |
1134 | | - continue |
1135 | | - if source.is_dir(): |
1136 | | - self.resolve_paths(source.iterdir()) |
1137 | | - elif source.is_file(): |
1138 | | - self._paths[source] = config |
0 commit comments