blockwatch-cli 0.11.3__tar.gz → 0.12.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/CHANGELOG.md +32 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/Cargo.lock +1 -1
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/Cargo.toml +1 -1
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/PKG-INFO +1 -1
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/README.md +1 -1
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/docs/ci.md +2 -2
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/docs/cli.md +4 -3
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/docs/validators/keep-sorted.md +2 -1
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/docs/validators/keep-unique.md +2 -1
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/docs/validators/same-as.md +3 -2
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/setup.py +1 -1
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/block_parser.rs +13 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/blocks.rs +5 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/cli.rs +8 -7
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/flags.rs +35 -3
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/bash.rs +2 -1
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/cmake.rs +7 -2
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/css.rs +5 -4
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/json.rs +4 -2
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/lean.rs +2 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/lua.rs +6 -1
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/markdown.rs +87 -36
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/mdx.rs +18 -6
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/mod.rs +56 -31
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/ocaml.rs +2 -2
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/php.rs +3 -3
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/ruby.rs +7 -1
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/rust.rs +6 -5
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/scala.rs +15 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/sql.rs +6 -5
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/toml.rs +8 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/yaml.rs +24 -4
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/lib.rs +17 -1
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/paths_glob.rs +1 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/sarif.rs +9 -15
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/symbol_path.rs +35 -4
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/symbols.rs +11 -3
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/tag_parser.rs +31 -4
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/validators/check_ai.rs +20 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/validators/check_lua.rs +33 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/validators/keep_unique.rs +19 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/validators/line_pattern.rs +8 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/validators/mod.rs +149 -31
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/validators/same_as.rs +22 -2
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/virtual_blocks.rs +33 -5
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/general.rs +25 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/.agents/.claude-plugin/plugin.json +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/.agents/skills/blockwatch/SKILL.md +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/.claude-plugin/marketplace.json +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/.github/dependabot.yml +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/.github/workflows/blockwatch.yml +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/.github/workflows/codeql.yml +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/.github/workflows/markdownlint.yml +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/.github/workflows/pypi.yml +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/.github/workflows/release.yml +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/.github/workflows/rust.yml +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/.github/workflows/trusted-checks.yml +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/.gitignore +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/.markdownlint-cli2.yaml +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/.pre-commit-config.yaml +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/.pre-commit-hooks.yaml +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/AGENTS.md +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/CLAUDE.md +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/LICENSE +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/SECURITY.md +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/blockwatch.toml +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/clippy.toml +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/codecov.yml +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/demo.gif +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/dist-workspace.toml +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/docs/agents.md +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/docs/symbols.md +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/docs/validators/README.md +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/docs/validators/affects.md +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/docs/validators/check-ai.md +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/docs/validators/check-lua.md +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/docs/validators/line-count.md +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/docs/validators/line-pattern.md +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/pypi/pyproject.toml +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/pyproject.toml +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/release.toml +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/scripts/check_language_count.lua +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/scripts/check_latest_gpt_nano_model.lua +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/config.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/diff_parser.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/fs.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/c.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/c_sharp.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/cpp.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/dart.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/dockerfile.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/elixir.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/go.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/graphql.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/groovy.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/hcl.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/html.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/java.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/javascript.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/json.scm +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/kotlin.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/makefile.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/nix.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/proto.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/python.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/starlark.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/swift.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/tsx.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/typescript.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/language_parsers/xml.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/main.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/path_arguments.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/repo_path.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/report.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/settings.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/skill.md +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/validators/affects.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/validators/keep_sorted.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/validators/line_count.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/src/violation_address.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/affects.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/check_ai.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/check_lua.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/common/empty.toml +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/common/mod.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/config.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/encoding.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/keep_sorted.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/keep_unique.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/line_count.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/line_pattern.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/list.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/path_arguments.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/repository_root.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/same_as.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/sarif.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/skill.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/suppressions.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/symbol_references.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/affects/cross_file_source.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/affects/cross_file_target.md +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/affects/custom_ext_source.javascript +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/affects/custom_ext_target.javascript +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/affects/dangling.md +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/affects/missing_symbol.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/affects/more_added_than_deleted.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/affects/multiline_tag_source.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/affects/multiline_tag_target.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/affects/nested_mapping_source.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/affects/nested_mapping_target.yaml +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/affects/sibling.md +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/affects/symbol_source.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/affects/symbol_target.json +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/affects/table_source.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/affects/table_target.toml +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/affects/tag_and_content_source.ts +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/affects/tag_and_content_target.ts +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/affects/whole_file_source.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/affects/whole_file_target.json +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/check_ai/two_blocks.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/check_lua/affects.lua +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/check_lua/affects.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/check_lua/affects_drift.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/check_lua/blocking_io.lua +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/check_lua/blocking_io.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/check_lua/echo.lua +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/check_lua/fail.lua +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/check_lua/mode.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/check_lua/os.lua +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/check_lua/pathfmt/report_path.lua +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/check_lua/pathfmt/source.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/check_lua/pattern.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/check_lua/pattern_many_matches.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/check_lua/pattern_no_match.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/check_lua/success.lua +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/check_lua/success_and_failure.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/config/glob_target/config.toml +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/config/glob_target/package.json +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/config/glob_target/plugins/blue/package.json +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/config/glob_target/plugins/green/package.json +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/config/glob_target/plugins/red/package.json +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/general/block_selection.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/general/custom_file_extension.javascript +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/general/custom_file_extension.python +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/general/disable_enable.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/general/paths/invalid.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/general/paths/subdir/nested_invalid.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/general/paths/valid.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/general/severity.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/hidden/.github/workflows/ci.yml +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/keep_sorted/orderings.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/keep_unique/basic_and_pattern.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/line_count/equality_and_greater_than.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/line_pattern/forbidden_text.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/line_pattern/hex_and_word_patterns.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/list/.hg/.gitkeep +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/list/a.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/list/b.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/positions/unicode_columns.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/same_as/custom_ext_source.javascript +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/same_as/custom_ext_target.javascript +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/same_as/package.json +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/same_as/paths/alpha/item.txt +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/same_as/paths/beta/item.txt +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/same_as/paths_and_block_source.md +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/same_as/paths_source.md +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/same_as/sibling.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/same_as/source.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/same_as/split_table_source.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/same_as/split_table_target.toml +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/same_as/symbol_mismatch_source.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/same_as/symbol_source.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/same_as/target.md +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/same_as/whole_file_mismatch_source.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/same_as/whole_file_source.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/same_as/whole_file_target.json +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/sarif/advisory.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/sarif/clean.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/sarif/sorted.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/sarif/unnamed.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/suppressions/commit_msg.txt +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/suppressions/commit_msg_gone_a.txt +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/suppressions/commit_msg_gone_b.txt +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/suppressions/commit_msg_lowercase_trailer.txt +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/suppressions/commit_msg_malformed.txt +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/suppressions/commit_msg_with_noise.txt +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/suppressions/dangling.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/suppressions/sorted.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/suppressions/symbol.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/suppressions/symbol_target.json +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/suppressions/two_rules.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/suppressions/unnamed.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/symbol_references/escapes_source.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/symbol_references/escapes_target.json +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/symbol_references/filter_source.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/symbol_references/filter_target.yaml +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/symbol_references/json_source.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/symbol_references/json_target.json +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/symbol_references/jsonc_source.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/symbol_references/jsonc_target.jsonc +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/symbol_references/mixed_list_source.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/symbol_references/mixed_list_target.json +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/symbol_references/toml_source.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/symbol_references/toml_target.toml +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/symbol_references/yaml_source.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/symbol_references/yaml_target.yaml +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/symbol_references/yml_source.rs +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/symbol_references/yml_target.yml +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/verbosity/affects/pair.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/verbosity/clean/counted.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/verbosity/clean/noop.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/verbosity/clean/notes.txt +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/verbosity/clean/sorted.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/verbosity/diff/two_blocks.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/verbosity/failing/unsorted.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/testdata/verbosity/mixed/both_kinds.py +0 -0
- {blockwatch_cli-0.11.3 → blockwatch_cli-0.12.0}/tests/verbosity.rs +0 -0
|
@@ -12,6 +12,38 @@ Releases up to and including v0.3.11 predate this file. Their notes live on the
|
|
|
12
12
|
|
|
13
13
|
## [Unreleased] - ReleaseDate
|
|
14
14
|
|
|
15
|
+
## [0.12.0] - 2026-10-15
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- A `blockwatch-suppress:` line with no address now fails the run with `invalid suppression address`. Before,
|
|
20
|
+
blockwatch ignored it.
|
|
21
|
+
- Messages now show a non-ASCII key in a symbol path as written, such as `data.toml#/täble/key`. Before, they showed
|
|
22
|
+
`data.toml#/t%C3%A4ble/key`. An `affects` or `same-as` violation on such a key gets a new hash, so update any
|
|
23
|
+
`--suppress` address with four parts that points at it.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- `--suppress-from` no longer crashes on a line with a non-ASCII character, such as `❌` or `—`, near its start.
|
|
28
|
+
Before, such a line could make blockwatch exit with code 101, even when the code had no violations
|
|
29
|
+
([#172](https://github.com/mennanov/blockwatch/issues/172)).
|
|
30
|
+
- blockwatch no longer crashes on a file that holds a tag and one of these:
|
|
31
|
+
- in Scala, a `/*` comment that is never closed;
|
|
32
|
+
- in Markdown, a `[//]:` line with no title, such as `[//]: #`, or with a quote in its destination, such as
|
|
33
|
+
`[//]: it's (text)`.
|
|
34
|
+
- In Markdown, a tag in a `[//]:` title that starts on the next line is now reported on that line. Before, it was
|
|
35
|
+
reported on the `[//]:` line.
|
|
36
|
+
- A Lua error in a `check-lua` script now shows the script and the line, such as `check.lua:3:`. Before, it showed a
|
|
37
|
+
file of blockwatch's own source code: `src/validators/check_lua.rs:309:3:`.
|
|
38
|
+
- `blockwatch list`, the JSON violations and the SARIF log now come out in the same order on every run. Files are
|
|
39
|
+
sorted by path, and the violations in a file by line, column and validator. Before, the order changed from run to
|
|
40
|
+
run.
|
|
41
|
+
- A pattern with its `value` group in one branch, such as `^(?:none|(?P<value>\w+))$`, now skips a match of the other
|
|
42
|
+
branch, such as `none`. This applies to `keep-sorted-pattern`, `keep-unique`, `same-as-pattern`, `check-ai-pattern`
|
|
43
|
+
and `check-lua-pattern`. Before, such a match gave its whole text as a value, so `keep-unique` could report a false
|
|
44
|
+
duplicate ([#173](https://github.com/mennanov/blockwatch/issues/173)). A `target-pattern` match like that now stops
|
|
45
|
+
the run. Before, the block's content was the whole match.
|
|
46
|
+
|
|
15
47
|
## [0.11.3] - 2026-10-15
|
|
16
48
|
|
|
17
49
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: blockwatch-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.12.0
|
|
4
4
|
Summary: Language agnostic linter that keeps your code and documentation in sync and valid
|
|
5
5
|
Keywords: linter,sync,drift,comments,agents
|
|
6
6
|
Home-Page: https://github.com/mennanov/blockwatch
|
|
@@ -19,7 +19,7 @@ To use [pre-commit](https://pre-commit.com), add the hook to `.pre-commit-config
|
|
|
19
19
|
|
|
20
20
|
```yaml
|
|
21
21
|
- repo: https://github.com/mennanov/blockwatch
|
|
22
|
-
rev: v0.
|
|
22
|
+
rev: v0.12.0 # Use latest release
|
|
23
23
|
hooks:
|
|
24
24
|
- id: blockwatch
|
|
25
25
|
```
|
|
@@ -59,7 +59,7 @@ The `blockwatch` hook runs before the commit message exists, so it cannot see a
|
|
|
59
59
|
|
|
60
60
|
```yaml
|
|
61
61
|
- repo: https://github.com/mennanov/blockwatch
|
|
62
|
-
rev: v0.
|
|
62
|
+
rev: v0.12.0 # Use latest release
|
|
63
63
|
hooks:
|
|
64
64
|
- id: blockwatch-commit-msg
|
|
65
65
|
```
|
|
@@ -394,6 +394,7 @@ same-as-pattern = 'os\.environ\["(?P<value>[A-Z_]+)"\]'
|
|
|
394
394
|
- **A match can span lines with `(?s)`.** `(?s)^CREATE TABLE users \(.*?^\);` matches the whole statement. Write
|
|
395
395
|
`.*?`, not `.*`. A `.*` runs to the last `);` in the file, and that still counts as one match.
|
|
396
396
|
- **A `value` group picks the content**, as in the `*-pattern` attributes. Without one, the content is the whole match.
|
|
397
|
+
A match of a branch without the group stops the run, such as `MODEL = none` for `^MODEL = (?:none|"(?P<value>.+)")`.
|
|
397
398
|
|
|
398
399
|
A virtual block is a block of the file it wraps:
|
|
399
400
|
|
|
@@ -554,15 +555,15 @@ Nothing else about the run changes: the same violations are found, and the exit
|
|
|
554
555
|
"tool": {
|
|
555
556
|
"driver": {
|
|
556
557
|
"name": "blockwatch",
|
|
557
|
-
"version": "0.
|
|
558
|
-
"semanticVersion": "0.
|
|
558
|
+
"version": "0.12.0",
|
|
559
|
+
"semanticVersion": "0.12.0",
|
|
559
560
|
"informationUri": "https://github.com/mennanov/blockwatch",
|
|
560
561
|
"rules": [
|
|
561
562
|
{
|
|
562
563
|
"id": "keep-sorted",
|
|
563
564
|
"name": "keep-sorted",
|
|
564
565
|
"shortDescription": { "text": "Requires the lines of a block to stay in order." },
|
|
565
|
-
"helpUri": "https://github.com/mennanov/blockwatch/blob/v0.
|
|
566
|
+
"helpUri": "https://github.com/mennanov/blockwatch/blob/v0.12.0/docs/validators/keep-sorted.md"
|
|
566
567
|
}
|
|
567
568
|
]
|
|
568
569
|
}
|
|
@@ -93,7 +93,8 @@ A separator must sit between two digits — `_1`, `1_`, `1__0` and `1_.0` are ty
|
|
|
93
93
|
## Notes
|
|
94
94
|
|
|
95
95
|
- Blank lines and lines the pattern does not match are ignored, not treated as out of order. A line whose match is empty
|
|
96
|
-
is ignored as well.
|
|
96
|
+
is ignored as well. So is a line that matches a branch without the `value` group: `^(?:none|(?P<value>\w+))$`
|
|
97
|
+
ignores the line `none`.
|
|
97
98
|
- Comparison is on the trimmed line, so indentation does not affect ordering. A pattern is applied to the trimmed line
|
|
98
99
|
too, so `^` and `$` anchor to the entry rather than to the indentation.
|
|
99
100
|
- An unrecognized `keep-sorted` or `keep-sorted-format` value is a hard error, not a violation.
|
|
@@ -36,7 +36,8 @@ ids = [
|
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
Without a `value` group the whole match is compared. Lines that do not match are skipped, as are
|
|
39
|
-
lines whose match is empty.
|
|
39
|
+
lines whose match is empty. So is a line that matches a branch without the `value` group: `^(?:none|(?P<value>\w+))$`
|
|
40
|
+
skips the line `none`.
|
|
40
41
|
|
|
41
42
|
## Notes
|
|
42
43
|
|
|
@@ -56,8 +56,9 @@ Usually, though, the two places don't hold the same text. Then each block says *
|
|
|
56
56
|
## Pick values with `same-as-pattern`
|
|
57
57
|
|
|
58
58
|
Each block has its own regex. Every match is a value: the `(?P<value>…)` group if there is one, otherwise the whole
|
|
59
|
-
match. A line can hold several values. Lines without a match are skipped, and so are empty matches.
|
|
60
|
-
|
|
59
|
+
match. A line can hold several values. Lines without a match are skipped, and so are empty matches. So is a match of a
|
|
60
|
+
branch without the `value` group: `^(?:none|(?P<value>\w+))$` skips the line `none`. This lets you compare blocks
|
|
61
|
+
written in different formats:
|
|
61
62
|
|
|
62
63
|
```rust
|
|
63
64
|
// <block same-as="README.md:supported-env-vars" same-as-pattern="BLOCKWATCH_AI_[A-Z_]+">
|
|
@@ -6,7 +6,7 @@ setup(
|
|
|
6
6
|
name='blockwatch-pre-commit-placeholder',
|
|
7
7
|
version='0.0.0',
|
|
8
8
|
# <block name="blockwatch-cli-version" same-as-pattern="blockwatch-cli==(?P<value>\d+\.\d+\.\d+)">
|
|
9
|
-
install_requires=['blockwatch-cli==0.
|
|
9
|
+
install_requires=['blockwatch-cli==0.12.0'],
|
|
10
10
|
# </block>
|
|
11
11
|
# Without it, setuptools takes the directories under src/ for Python packages.
|
|
12
12
|
packages=[],
|
|
@@ -206,6 +206,10 @@ impl<'source, I: Iterator<Item = Comment>> PartialBlocksIterator<'source, I> {
|
|
|
206
206
|
fn take_adjacent_comment(&mut self, comment: &Comment) -> Option<Comment> {
|
|
207
207
|
let contents = self.contents;
|
|
208
208
|
self.comments.next_if(|next| {
|
|
209
|
+
#[expect(
|
|
210
|
+
clippy::string_slice,
|
|
211
|
+
reason = "parsers give comment ranges of whole characters"
|
|
212
|
+
)]
|
|
209
213
|
contents[comment.source_range.end..next.source_range.start]
|
|
210
214
|
.trim()
|
|
211
215
|
.is_empty()
|
|
@@ -217,6 +221,10 @@ impl<'source, I: Iterator<Item = Comment>> PartialBlocksIterator<'source, I> {
|
|
|
217
221
|
fn join_comments(comment: Comment, next: Comment, contents: &str) -> Comment {
|
|
218
222
|
// The text between the comments is copied from the source, so an offset into the joined text
|
|
219
223
|
// maps onto the source as it does for a single comment.
|
|
224
|
+
#[expect(
|
|
225
|
+
clippy::string_slice,
|
|
226
|
+
reason = "parsers give comment ranges of whole characters"
|
|
227
|
+
)]
|
|
220
228
|
let between = &contents[comment.source_range.end..next.source_range.start];
|
|
221
229
|
Comment {
|
|
222
230
|
position_range: comment.position_range.start..next.position_range.end,
|
|
@@ -315,9 +323,14 @@ impl BlockStart {
|
|
|
315
323
|
/// Columns count characters, so the offset is converted by counting the characters it skips
|
|
316
324
|
/// rather than the bytes: the two differ on any line holding a multi-byte character.
|
|
317
325
|
fn source_position_at(position_in_comment: usize, comment: &Comment) -> Position {
|
|
326
|
+
#[expect(
|
|
327
|
+
clippy::string_slice,
|
|
328
|
+
reason = "a tag starts at a `<` and ends after a `>`"
|
|
329
|
+
)]
|
|
318
330
|
let text_before = &comment.comment_text[..position_in_comment];
|
|
319
331
|
match text_before.rfind('\n') {
|
|
320
332
|
// Past the comment's first line, so the column is measured from that line's start.
|
|
333
|
+
#[expect(clippy::string_slice, reason = "`line_break` is the index of a `\\n`")]
|
|
321
334
|
Some(line_break) => Position::new(
|
|
322
335
|
comment.position_range.start.line + text_before.matches('\n').count(),
|
|
323
336
|
text_before[line_break + 1..].chars().count() + 1,
|
|
@@ -166,6 +166,10 @@ impl Content {
|
|
|
166
166
|
/// Returns the content's text. `source` must be the text of the block's file.
|
|
167
167
|
pub(crate) fn text<'a>(&'a self, source: &'a str) -> &'a str {
|
|
168
168
|
match &self.text {
|
|
169
|
+
#[expect(
|
|
170
|
+
clippy::string_slice,
|
|
171
|
+
reason = "a comment, a symbol or a match sets `range`"
|
|
172
|
+
)]
|
|
169
173
|
ContentText::Source(range) => &source[range.clone()],
|
|
170
174
|
ContentText::Decoded(text) => text,
|
|
171
175
|
}
|
|
@@ -1002,6 +1006,7 @@ pub(crate) fn parser_for_file_path<'p>(
|
|
|
1002
1006
|
let file_name = file_path.file_name()?.to_str()?;
|
|
1003
1007
|
|
|
1004
1008
|
for (i, _) in file_name.match_indices('.').rev() {
|
|
1009
|
+
#[expect(clippy::string_slice, reason = "`i` is the index of a `.`")]
|
|
1005
1010
|
let extension = &file_name[i + 1..];
|
|
1006
1011
|
let ext_os = OsString::from(extension);
|
|
1007
1012
|
|
|
@@ -16,7 +16,7 @@ use crate::violation_address::ViolationAddress;
|
|
|
16
16
|
use crate::virtual_blocks::VirtualBlock;
|
|
17
17
|
use anyhow::Context;
|
|
18
18
|
use clap::Parser;
|
|
19
|
-
use std::collections::HashMap;
|
|
19
|
+
use std::collections::{BTreeMap, HashMap};
|
|
20
20
|
use std::io::{IsTerminal, Read, Write};
|
|
21
21
|
use std::path::PathBuf;
|
|
22
22
|
use std::process::ExitCode;
|
|
@@ -147,7 +147,7 @@ fn run_validators(args: &flags::Args) -> anyhow::Result<ExitCode> {
|
|
|
147
147
|
/// both leave one behind, and neither says anything about the code being checked.
|
|
148
148
|
fn apply_suppressions(
|
|
149
149
|
suppressed_addresses: &[ViolationAddress],
|
|
150
|
-
violations: &mut
|
|
150
|
+
violations: &mut BTreeMap<RepoPath, Vec<Violation>>,
|
|
151
151
|
) {
|
|
152
152
|
if suppressed_addresses.is_empty() {
|
|
153
153
|
return;
|
|
@@ -329,7 +329,7 @@ fn read_diff_from_stdin(
|
|
|
329
329
|
/// Writes the violations to stderr in the requested format. Returns true if any violation has error
|
|
330
330
|
/// severity.
|
|
331
331
|
fn process_violations(
|
|
332
|
-
violations: &
|
|
332
|
+
violations: &BTreeMap<RepoPath, Vec<Violation>>,
|
|
333
333
|
format: flags::OutputFormat,
|
|
334
334
|
) -> anyhow::Result<bool> {
|
|
335
335
|
let has_error_severity = violations.values().flatten().any(|violation| {
|
|
@@ -348,9 +348,10 @@ fn process_violations(
|
|
|
348
348
|
Ok(has_error_severity)
|
|
349
349
|
}
|
|
350
350
|
|
|
351
|
-
/// Writes the violations to stderr as one JSON object of diagnostics grouped by file
|
|
352
|
-
|
|
353
|
-
|
|
351
|
+
/// Writes the violations to stderr as one JSON object of diagnostics grouped by file, with the
|
|
352
|
+
/// files sorted by path.
|
|
353
|
+
fn write_json_violations(violations: &BTreeMap<RepoPath, Vec<Violation>>) -> anyhow::Result<()> {
|
|
354
|
+
let diagnostics: BTreeMap<&RepoPath, Vec<_>> = violations
|
|
354
355
|
.iter()
|
|
355
356
|
.map(|(file_path, file_violations)| {
|
|
356
357
|
let file_diagnostics = file_violations
|
|
@@ -364,7 +365,7 @@ fn write_json_violations(violations: &HashMap<RepoPath, Vec<Violation>>) -> anyh
|
|
|
364
365
|
}
|
|
365
366
|
|
|
366
367
|
/// Writes the violations to stderr as a SARIF log.
|
|
367
|
-
fn write_sarif_violations(violations: &
|
|
368
|
+
fn write_sarif_violations(violations: &BTreeMap<RepoPath, Vec<Violation>>) -> anyhow::Result<()> {
|
|
368
369
|
write_json(std::io::stderr(), &sarif::SarifLog::new(violations))
|
|
369
370
|
}
|
|
370
371
|
|
|
@@ -377,13 +377,15 @@ fn parse_suppressions(content: &str, path: &Path) -> anyhow::Result<Vec<Violatio
|
|
|
377
377
|
let mut addresses = Vec::new();
|
|
378
378
|
for (line_idx, line) in content.lines().enumerate() {
|
|
379
379
|
let trimmed = line.trim();
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
380
|
+
// Bytes, because the prefix's length can fall inside a multi-byte character.
|
|
381
|
+
if trimmed.len() < SUPPRESS_TRAILER_PREFIX.len()
|
|
382
|
+
|| !trimmed.as_bytes()[..SUPPRESS_TRAILER_PREFIX.len()]
|
|
383
|
+
.eq_ignore_ascii_case(SUPPRESS_TRAILER_PREFIX.as_bytes())
|
|
383
384
|
{
|
|
384
385
|
// Non-matching line.
|
|
385
386
|
continue;
|
|
386
387
|
}
|
|
388
|
+
#[expect(clippy::string_slice, reason = "the prefix just matched is ASCII")]
|
|
387
389
|
let address_str = trimmed[SUPPRESS_TRAILER_PREFIX.len()..].trim();
|
|
388
390
|
let address = ViolationAddress::parse(address_str).with_context(|| {
|
|
389
391
|
format!(
|
|
@@ -600,6 +602,36 @@ mod tests {
|
|
|
600
602
|
Ok(())
|
|
601
603
|
}
|
|
602
604
|
|
|
605
|
+
#[test]
|
|
606
|
+
fn multibyte_character_across_prefix_length_is_ignored() -> anyhow::Result<()> {
|
|
607
|
+
// `é` is 2 bytes: 19 and 20. The trailer prefix is 20 bytes long.
|
|
608
|
+
let content = format!(
|
|
609
|
+
"{0}é\n\
|
|
610
|
+
{0}é and more text\n\
|
|
611
|
+
blockwatch-suppress: docs/cli.md:cli-docs:keep-sorted\n",
|
|
612
|
+
"a".repeat(19)
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
assert_eq!(
|
|
616
|
+
parse_suppressions(&content, Path::new("commit_msg.txt"))?
|
|
617
|
+
.iter()
|
|
618
|
+
.map(ToString::to_string)
|
|
619
|
+
.collect::<Vec<_>>(),
|
|
620
|
+
vec!["docs/cli.md:cli-docs:keep-sorted".to_string()]
|
|
621
|
+
);
|
|
622
|
+
Ok(())
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
#[test]
|
|
626
|
+
fn trailer_without_address_fails() {
|
|
627
|
+
let error = parse_suppressions("blockwatch-suppress:\n", Path::new("commit_msg.txt"))
|
|
628
|
+
.expect_err("a trailer without an address must be rejected");
|
|
629
|
+
assert!(
|
|
630
|
+
error.to_string().contains("invalid suppression address"),
|
|
631
|
+
"unexpected error: {error}"
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
|
|
603
635
|
#[test]
|
|
604
636
|
fn suppress_from_with_nonexistent_file_fails_validation() -> anyhow::Result<()> {
|
|
605
637
|
let args = parse(&["blockwatch", "--suppress-from", "nonexistent_file.txt"])?;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
use crate::language_parsers::{
|
|
2
2
|
CommentsParser, LanguageParser, LanguageParserImpl, TreeSitterCommentsParser, comment_visitor,
|
|
3
|
+
node_text,
|
|
3
4
|
};
|
|
4
5
|
|
|
5
6
|
/// Returns a [`LanguageParser`] for Bash.
|
|
@@ -15,7 +16,7 @@ fn comments_parser() -> anyhow::Result<impl CommentsParser> {
|
|
|
15
16
|
if node.kind() != "comment" {
|
|
16
17
|
return None;
|
|
17
18
|
}
|
|
18
|
-
let comment =
|
|
19
|
+
let comment = node_text(node, source);
|
|
19
20
|
if comment.starts_with("#!") {
|
|
20
21
|
// Skip shebang.
|
|
21
22
|
None
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
use crate::language_parsers::{
|
|
2
2
|
CommentsParser, LanguageParser, LanguageParserImpl, TreeSitterCommentsParser, comment_visitor,
|
|
3
|
+
node_text,
|
|
3
4
|
};
|
|
4
5
|
use tree_sitter::Node;
|
|
5
6
|
|
|
@@ -13,8 +14,8 @@ fn comments_parser() -> anyhow::Result<impl CommentsParser> {
|
|
|
13
14
|
let parser = TreeSitterCommentsParser::new(
|
|
14
15
|
&cmake_language,
|
|
15
16
|
comment_visitor(|node, source_code| match node.kind() {
|
|
16
|
-
"line_comment" => Some(source_code
|
|
17
|
-
"bracket_comment" => Some(bracket_comment_text(node,
|
|
17
|
+
"line_comment" => Some(node_text(node, source_code).replacen('#', " ", 1)),
|
|
18
|
+
"bracket_comment" => Some(bracket_comment_text(node, node_text(node, source_code))),
|
|
18
19
|
_ => None,
|
|
19
20
|
}),
|
|
20
21
|
);
|
|
@@ -31,6 +32,10 @@ fn bracket_comment_text(node: &Node, comment: &str) -> String {
|
|
|
31
32
|
.map(|child| child.start_byte() - node.start_byte()..child.end_byte() - node.start_byte())
|
|
32
33
|
.unwrap_or(0..0);
|
|
33
34
|
let mut result = " ".repeat(content_range.start);
|
|
35
|
+
#[expect(
|
|
36
|
+
clippy::string_slice,
|
|
37
|
+
reason = "`content_range` is a tree-sitter node's range"
|
|
38
|
+
)]
|
|
34
39
|
result.push_str(&comment[content_range.clone()]);
|
|
35
40
|
result.push_str(&" ".repeat(comment.len() - content_range.end));
|
|
36
41
|
result
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
use crate::language_parsers::{
|
|
2
2
|
CommentsParser, LanguageParser, LanguageParserImpl, TreeSitterCommentsParser,
|
|
3
|
-
c_style_multiline_comment_processor, comment_visitor,
|
|
3
|
+
c_style_multiline_comment_processor, comment_visitor, node_text,
|
|
4
4
|
};
|
|
5
5
|
|
|
6
6
|
/// Returns a [`LanguageParser`] for CSS.
|
|
@@ -14,9 +14,10 @@ fn comments_parser() -> anyhow::Result<impl CommentsParser> {
|
|
|
14
14
|
&css_language,
|
|
15
15
|
comment_visitor(|node, source_code| {
|
|
16
16
|
if node.kind() == "comment" {
|
|
17
|
-
Some(c_style_multiline_comment_processor(
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
Some(c_style_multiline_comment_processor(node_text(
|
|
18
|
+
node,
|
|
19
|
+
source_code,
|
|
20
|
+
)))
|
|
20
21
|
} else {
|
|
21
22
|
None
|
|
22
23
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
use crate::language_parsers::{
|
|
1
|
+
use crate::language_parsers::{
|
|
2
|
+
LanguageParser, LanguageParserImpl, c_style_comments_parser, node_text,
|
|
3
|
+
};
|
|
2
4
|
use crate::symbols::QuerySymbolsParser;
|
|
3
5
|
use anyhow::Context;
|
|
4
6
|
|
|
@@ -16,7 +18,7 @@ pub(super) fn parser() -> anyhow::Result<impl LanguageParser> {
|
|
|
16
18
|
/// # Errors
|
|
17
19
|
/// Returns an error if a string literal is not valid JSON, e.g. because it holds a raw tab.
|
|
18
20
|
fn decode_node(node: &tree_sitter::Node, source: &str) -> anyhow::Result<String> {
|
|
19
|
-
let raw =
|
|
21
|
+
let raw = node_text(node, source);
|
|
20
22
|
match node.kind() {
|
|
21
23
|
"string" => serde_json::from_str::<String>(raw)
|
|
22
24
|
.with_context(|| format!("failed to unescape JSON string: {raw}")),
|
|
@@ -55,7 +55,9 @@ impl CommentsParser for LeanCommentsParser {
|
|
|
55
55
|
comments
|
|
56
56
|
.into_iter()
|
|
57
57
|
.map(move |((opening, closing), range)| {
|
|
58
|
+
#[expect(clippy::string_slice, reason = "the scanner reads whole characters")]
|
|
58
59
|
let text = &source_code[range.clone()];
|
|
60
|
+
#[expect(clippy::string_slice, reason = "comment markers are ASCII")]
|
|
59
61
|
Comment {
|
|
60
62
|
position_range: position_at(range.start)..position_at(range.end),
|
|
61
63
|
comment_text: format!(
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
use crate::language_parsers::{
|
|
2
2
|
CommentsParser, LanguageParser, LanguageParserImpl, TreeSitterCommentsParser, comment_visitor,
|
|
3
|
+
node_text,
|
|
3
4
|
};
|
|
4
5
|
use tree_sitter::Node;
|
|
5
6
|
|
|
@@ -16,7 +17,7 @@ fn comments_parser() -> anyhow::Result<impl CommentsParser> {
|
|
|
16
17
|
if node.kind() != "comment" {
|
|
17
18
|
return None;
|
|
18
19
|
}
|
|
19
|
-
Some(lua_comment_text(node,
|
|
20
|
+
Some(lua_comment_text(node, node_text(node, source_code)))
|
|
20
21
|
}),
|
|
21
22
|
);
|
|
22
23
|
Ok(parser)
|
|
@@ -32,6 +33,10 @@ fn lua_comment_text(node: &Node, comment: &str) -> String {
|
|
|
32
33
|
None => 0..0,
|
|
33
34
|
};
|
|
34
35
|
let mut result = " ".repeat(content_range.start);
|
|
36
|
+
#[expect(
|
|
37
|
+
clippy::string_slice,
|
|
38
|
+
reason = "`content_range` is a tree-sitter node's range"
|
|
39
|
+
)]
|
|
35
40
|
result.push_str(&comment[content_range.clone()]);
|
|
36
41
|
result.push_str(&" ".repeat(comment.len() - content_range.end));
|
|
37
42
|
result
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
use crate::language_parsers::{
|
|
2
2
|
Comment, CommentsParser, LanguageParser, LanguageParserImpl, TreeSitterCommentsParser,
|
|
3
|
-
blank_preserving_line_breaks, xml_style_comments_parser,
|
|
3
|
+
blank_preserving_line_breaks, node_text, xml_style_comments_parser,
|
|
4
4
|
};
|
|
5
5
|
use tree_sitter::StreamingIterator;
|
|
6
6
|
|
|
@@ -73,12 +73,12 @@ impl MdCommentsParser {
|
|
|
73
73
|
.first()
|
|
74
74
|
.expect("Empty Tree-sitter region query match")
|
|
75
75
|
.node;
|
|
76
|
-
let region = &contents
|
|
76
|
+
let region = node_text(&node, contents);
|
|
77
77
|
|
|
78
78
|
// `[//]:` comments are direct nodes of the Markdown tree, so their positions are
|
|
79
79
|
// already the source positions.
|
|
80
80
|
if node.kind() == "link_reference_definition" {
|
|
81
|
-
if let Some(text) = link_reference_definition_comment_text(
|
|
81
|
+
if let Some(text) = link_reference_definition_comment_text(&node, contents) {
|
|
82
82
|
comments.push(Comment::from_node(&node, contents, text));
|
|
83
83
|
}
|
|
84
84
|
continue;
|
|
@@ -151,40 +151,33 @@ impl CommentsParser for MdCommentsParser {
|
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
///
|
|
155
|
-
///
|
|
156
|
-
///
|
|
157
|
-
fn link_reference_definition_comment_text(
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
let mut result = String::with_capacity(comment.len());
|
|
174
|
-
result.push_str(&comment[..prefix_idx]);
|
|
175
|
-
// Replace "[//]:" with spaces.
|
|
176
|
-
result.push_str(" ");
|
|
177
|
-
// Replace everything before the open delimiter with spaces (including the delimiter).
|
|
178
|
-
result.push_str(" ".repeat(open_idx - (prefix_idx + 5) + 1).as_str());
|
|
179
|
-
// Copy the comment's content.
|
|
180
|
-
result.push_str(&comment[open_idx + 1..close_idx]);
|
|
181
|
-
// Replace the close delimiter with a space.
|
|
182
|
-
result.push(' ');
|
|
183
|
-
if close_idx + 1 < comment.len() {
|
|
184
|
-
// Copy the rest of the comment after the close delimiter.
|
|
185
|
-
result.push_str(&comment[close_idx + 1..]);
|
|
154
|
+
/// Returns the text of `node`, a link reference definition in `contents`, with everything but its
|
|
155
|
+
/// title blanked, so the length is preserved. Returns `None` when `node` is not a `[//]:` comment,
|
|
156
|
+
/// or has no title.
|
|
157
|
+
fn link_reference_definition_comment_text(
|
|
158
|
+
node: &tree_sitter::Node,
|
|
159
|
+
contents: &str,
|
|
160
|
+
) -> Option<String> {
|
|
161
|
+
let mut cursor = node.walk();
|
|
162
|
+
let mut label = None;
|
|
163
|
+
let mut title = None;
|
|
164
|
+
for child in node.named_children(&mut cursor) {
|
|
165
|
+
match child.kind() {
|
|
166
|
+
"link_label" => label = Some(child),
|
|
167
|
+
"link_title" => title = Some(child),
|
|
168
|
+
_ => {}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if node_text(&label?, contents) != "[//]" {
|
|
172
|
+
return None;
|
|
186
173
|
}
|
|
187
|
-
|
|
174
|
+
// A title is read from the tree, not found in the text, because its delimiters, such as `'`,
|
|
175
|
+
// can also appear in the destination.
|
|
176
|
+
let title = title?;
|
|
177
|
+
let mut text = contents.as_bytes()[node.byte_range()].to_vec();
|
|
178
|
+
blank_preserving_line_breaks(&mut text[..title.start_byte() - node.start_byte()]);
|
|
179
|
+
blank_preserving_line_breaks(&mut text[title.end_byte() - node.start_byte()..]);
|
|
180
|
+
Some(String::from_utf8(text).expect("only whole characters are blanked"))
|
|
188
181
|
}
|
|
189
182
|
|
|
190
183
|
#[cfg(test)]
|
|
@@ -279,6 +272,64 @@ Text
|
|
|
279
272
|
Ok(())
|
|
280
273
|
}
|
|
281
274
|
|
|
275
|
+
#[test]
|
|
276
|
+
fn link_reference_comment_with_quote_in_destination_reads_title() -> anyhow::Result<()> {
|
|
277
|
+
let mut parser = parser()?;
|
|
278
|
+
let content = r#"[//]: it's (<block name="a">)
|
|
279
|
+
Text
|
|
280
|
+
|
|
281
|
+
[//]: # (</block>)
|
|
282
|
+
"#;
|
|
283
|
+
let blocks = parser
|
|
284
|
+
.parse_blocks(content)
|
|
285
|
+
.collect::<anyhow::Result<Vec<_>>>()?;
|
|
286
|
+
|
|
287
|
+
assert_eq!(blocks.len(), 1);
|
|
288
|
+
assert_eq!(blocks[0].attributes["name"], "a");
|
|
289
|
+
assert_eq!(blocks[0].content.text(content), "Text\n\n");
|
|
290
|
+
Ok(())
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
#[test]
|
|
294
|
+
fn link_reference_comment_with_title_on_next_line_reports_tag_line() -> anyhow::Result<()> {
|
|
295
|
+
let mut parser = parser()?;
|
|
296
|
+
let content = r#"[//]: #
|
|
297
|
+
(<block name="a">)
|
|
298
|
+
Text
|
|
299
|
+
|
|
300
|
+
[//]: # (</block>)
|
|
301
|
+
"#;
|
|
302
|
+
let blocks = parser
|
|
303
|
+
.parse_blocks(content)
|
|
304
|
+
.collect::<anyhow::Result<Vec<_>>>()?;
|
|
305
|
+
|
|
306
|
+
assert_eq!(
|
|
307
|
+
blocks[0].start_tag_position_range,
|
|
308
|
+
Position::new(2, 2)..Position::new(2, 18)
|
|
309
|
+
);
|
|
310
|
+
Ok(())
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
#[test]
|
|
314
|
+
fn link_reference_without_title_or_with_another_label_is_ignored() -> anyhow::Result<()> {
|
|
315
|
+
let mut parser = parser()?;
|
|
316
|
+
let content = r#"<!-- <block name="a"> -->
|
|
317
|
+
Text
|
|
318
|
+
|
|
319
|
+
[//]: #
|
|
320
|
+
[other]: /url (<block name="b">)
|
|
321
|
+
|
|
322
|
+
<!-- </block> -->
|
|
323
|
+
"#;
|
|
324
|
+
let blocks = parser
|
|
325
|
+
.parse_blocks(content)
|
|
326
|
+
.collect::<anyhow::Result<Vec<_>>>()?;
|
|
327
|
+
|
|
328
|
+
assert_eq!(blocks.len(), 1);
|
|
329
|
+
assert_eq!(blocks[0].attributes["name"], "a");
|
|
330
|
+
Ok(())
|
|
331
|
+
}
|
|
332
|
+
|
|
282
333
|
#[test]
|
|
283
334
|
fn inline_comment_after_a_code_span_holding_non_ascii_reports_a_character_column()
|
|
284
335
|
-> anyhow::Result<()> {
|