Skip to content

Commit

Permalink
config: use gitignore-style patterns for exclude
Browse files Browse the repository at this point in the history
  • Loading branch information
nmoroze committed Apr 1, 2024
1 parent 8ec468c commit d0791cc
Show file tree
Hide file tree
Showing 7 changed files with 91 additions and 21 deletions.
9 changes: 5 additions & 4 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
The following example shows all supported fields:

```toml
# paths to exclude when searching directories. defaults to empty list.
exclude = ["ignore_me/", "ignore.tcl"]
# patterns to exclude when searching directories. defaults to empty list.
# follows gitignore pattern format: https://git-scm.com/docs/gitignore#_pattern_format
exclude = ["ignore_me/", "ignore*.tcl", "/ignore_from_here"]
# lint violations to ignore. defaults to empty list.
# can also supply an inline table with a path and a list of violations to ignore under that path.
ignore = [
Expand Down Expand Up @@ -61,8 +62,8 @@ Each configuration field supports at least one command line argument that can be
configuration arguments:
--ignore "rule1, rule2, ..."
--extend-ignore "rule1, rule2, ..."
--exclude "path1, path2, ..."
--extend-exclude "path1, path2, ..."
--exclude "pattern1, pattern2, ..."
--extend-exclude "pattern1, pattern2, ..."
--style-indent <indent>
--style-line-length <line_length>
--style-max-blank-lines <max_blank_lines>
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ requires-python = ">=3.8"
dependencies = [
"ply==3.11",
"schema==0.7.5",
"tomli~=2.0.1; python_version < '3.11'"
"tomli~=2.0.1; python_version < '3.11'",
"pathspec==0.11.2"
]

[project.optional-dependencies]
Expand Down
2 changes: 1 addition & 1 deletion src/tclint/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def _str2list(s):
_VALIDATORS = {
# note: it's ok if paths don't exist - allows for generic
# configurations with directories like .git/ excluded
"exclude": And(Use(_str2list), [Use(pathlib.Path)]),
"exclude": Use(_str2list),
"ignore": And(
Use(_str2list),
[
Expand Down
40 changes: 27 additions & 13 deletions src/tclint/tclint.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import sys
from typing import List, Optional

import pathspec

from tclint.config import get_config, setup_config_cli_args, Config, ConfigError
from tclint.parser import Parser, TclSyntaxError
from tclint.checks import get_checkers
Expand All @@ -27,32 +29,38 @@


def resolve_sources(
paths: List[pathlib.Path], exclude: Optional[List[pathlib.Path]] = None
paths: List[pathlib.Path], exclude_patterns: List[str], exclude_root: pathlib.Path
) -> List[Optional[pathlib.Path]]:
"""Resolves paths passed via CLI to a list of filepaths to lint.
`paths` is a list of paths that may be files or directories. Files are
returned verbatim if they exist, and directories are recursively searched
for files that have the extension .tcl, .xdc, or .sdc. Paths that match or
are underneath a path provided in `exclude` are ignored.
for files that have the extension .tcl, .xdc, or .sdc. Paths that match a
pattern in `exclude_patterns` are ignored (based on gitignore pattern
format, see https://git-scm.com/docs/gitignore#_pattern_format).
Raises FileNotFoundError if a supplied path does not exist.
"""
# Extensions that may indicate tcl files
# TODO: make configurable
EXTENSIONS = [".tcl", ".xdc", ".sdc"]

if exclude is None:
exclude = []
exclude = [path.resolve() for path in exclude]
exclude_root = exclude_root.resolve()
exclude_spec = pathspec.PathSpec.from_lines("gitwildmatch", exclude_patterns)

def is_excluded(path):
resolved_path = path.resolve()
for exclude_path in exclude:
# if the current path is under an excluded path it should be ignored
if utils.is_relative_to(resolved_path, exclude_path):
return True

abspath = path.resolve()
try:
relpath = os.path.relpath(abspath, start=exclude_root)
except ValueError:
print(
"Warning: processing files on different drive from where tclint was"
" run, 'exclude' config may not behave as expected"
)
relpath = abspath

if exclude_spec.match_file(relpath):
return True
return False

sources: List[Optional[pathlib.Path]] = []
Expand Down Expand Up @@ -183,7 +191,13 @@ def main():
config.apply_cli_args(args)

try:
sources = resolve_sources(args.source, exclude=config.exclude)
# TODO: we should eventually allow tclint to find a config by walking up
# directories, at which point exclude_root should be the parent dir of
# the config file, unless -c is used (eslint rules)
exclude_root = pathlib.Path.cwd()
sources = resolve_sources(
args.source, exclude_patterns=config.exclude, exclude_root=exclude_root
)
except FileNotFoundError as e:
print(f"Invalid path provided: {e}")
return EXIT_INPUT_ERROR
Expand Down
2 changes: 1 addition & 1 deletion tests/data/tclint.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# paths to exclude when searching directories. defaults to empty list.
exclude = ["ignore_me/", "ignore.tcl"]
exclude = ["ignore_me/", "ignore*.tcl", "/ignore_from_here"]
# lint violations to ignore. defaults to empty list.
# can also supply an inline table with a path and a list of violations to ignore under that path.
ignore = [
Expand Down
2 changes: 1 addition & 1 deletion tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def test_example_config():

global_ = config.get_for_path(pathlib.Path())

assert global_.exclude == list(map(pathlib.Path, ["ignore_me/", "ignore.tcl"]))
assert global_.exclude == ["ignore_me/", "ignore*.tcl", "/ignore_from_here"]
assert global_.ignore == [
Rule("spacing"),
{"path": pathlib.Path("files_with_bad_indent/"), "rules": [Rule("indent")]},
Expand Down
54 changes: 54 additions & 0 deletions tests/test_tclint.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import os
import pathlib
import subprocess

import pytest

from tclint import tclint

MY_DIR = pathlib.Path(__file__).parent.resolve()

tests = []
Expand Down Expand Up @@ -88,3 +91,54 @@ def test_read_stdin():
output = stdout.decode("utf-8").strip()
assert output == "(stdin):1:1: expected indent of 0 spaces, got 1 [indent]"
assert stderr == b""


def test_resolve_sources(tmp_path_factory):
tmp_path = tmp_path_factory.mktemp("a")
(tmp_path / "src").mkdir()
(tmp_path / "src" / "ignore").mkdir()
(tmp_path / "src" / "ignore" / "bar.tcl").touch()
(tmp_path / "src" / "ignore1.tcl").touch()
(tmp_path / "src" / "ignore2.tcl").touch()
to_include = tmp_path / "src" / "foo.tcl"
to_include.touch()

cwd = os.getcwd()
os.chdir(tmp_path)

sources = tclint.resolve_sources(
[pathlib.Path(".")],
exclude_patterns=[
# test bare pattern matches anywhere in tree
"ignore",
# test glob
"ignore*.tcl",
# test pattern with "/" only matches from root of tree
"/foo.tcl",
],
exclude_root=tmp_path,
)

assert len(sources) == 1
assert sources[0] == to_include.relative_to(tmp_path)

# test absolute path outside of exclude_root doesn't get matched by ignore
# pattern starting with "/"
other_dir = tmp_path_factory.mktemp("b")
in_path = other_dir / "foo.tcl"
in_path.touch()
sources = tclint.resolve_sources(
[in_path], exclude_patterns=[str(in_path)], exclude_root=tmp_path
)
assert len(sources) == 1
assert sources[0] == in_path

# test that we can match outside of exclude root with explicit relative path
top_src = tmp_path / "top.tcl"
top_src.touch()
sources = tclint.resolve_sources(
[top_src], exclude_patterns=["../top.tcl"], exclude_root=tmp_path / "src"
)
assert len(sources) == 0

os.chdir(cwd)

0 comments on commit d0791cc

Please sign in to comment.