From 605ae53cf6a39c4fd641260673ed7f91d3548db2 Mon Sep 17 00:00:00 2001 From: Dan Mordechay Date: Tue, 20 Oct 2026 14:43:15 +0300 Subject: [PATCH 1/3] setup: recommended linter configs for Go, Python and TypeScript Add three core templates under setup/, rendered into every plugin: golangci.yaml, ruff.toml and eslint.config.ldd.mjs. Each enables the linter rules that the routing tables route to a design rule, at the limits the rules name, and takes the file-length limit from {{.FileMaxLines}}. Each says how to adopt it on an existing codebase: golangci-lint's new-from-merge-base, ESLint's bulk suppressions, ruff on changed files. The plugin never writes them into a repository. - The gap linter's threshold line names the rule and the setup file that would hold a file-length limit. For Python it says that ruff has no file-length rule and the gap linter holds it; it no longer proposes adding pylint, a tool the repository may not use. - The ldd-lint matrix checks that the named setup file sets the rule at the scalar's value. When golangci-lint, ruff or node is on PATH, it also runs each tool's own check of its file. - Fix: ldd-lint.sh did not parse under bash 3.2 (macOS /bin/bash). bash 3.2 misreads a case pattern inside a process substitution; the case moves into a function, dir_of. - The READMEs and docs/quick-start.md say "zero configuration to run" and name the recommended setup. Co-Authored-By: Claude Opus 5.5 --- core/README.md | 17 +- core/includes/scripts/ldd-lint-langs.sh | 37 +++- core/scripts/ldd-lint.sh | 17 +- core/scripts/ldd-lint_test.sh | 25 ++- core/setup/eslint.config.ldd.mjs | 93 +++++++++ core/setup/golangci.yaml | 82 ++++++++ core/setup/ruff.toml | 53 +++++ docs/quick-start.md | 7 +- go-linter-driven-development/CHANGELOG.md | 13 ++ go-linter-driven-development/README.md | 4 +- .../scripts/ldd-detect_test.sh | 5 + .../scripts/ldd-lint.sh | 54 +++-- .../scripts/ldd-lint_test.sh | 187 ++++++++++++++++- .../scripts/ldd-scope_test.sh | 5 + .../setup/eslint.config.ldd.mjs | 93 +++++++++ .../setup/golangci.yaml | 82 ++++++++ go-linter-driven-development/setup/ruff.toml | 53 +++++ lang/generic/passthrough/CHANGELOG.md | 13 ++ lang/generic/passthrough/README.md | 5 + lang/generic/scripts/ldd-fixture.sh | 7 + lang/go/passthrough/CHANGELOG.md | 13 ++ lang/go/passthrough/README.md | 4 +- lang/go/scripts/ldd-fixture.sh | 5 + lang/python/passthrough/CHANGELOG.md | 13 ++ lang/python/passthrough/README.md | 12 +- lang/python/scripts/ldd-fixture.sh | 5 + lang/ts-react/passthrough/CHANGELOG.md | 13 ++ lang/ts-react/passthrough/README.md | 5 + lang/ts-react/scripts/ldd-fixture.sh | 5 + linter-driven-development/CHANGELOG.md | 13 ++ linter-driven-development/README.md | 5 + .../scripts/ldd-detect_test.sh | 7 + linter-driven-development/scripts/ldd-lint.sh | 54 +++-- .../scripts/ldd-lint_test.sh | 189 +++++++++++++++++- .../scripts/ldd-scope_test.sh | 7 + .../setup/eslint.config.ldd.mjs | 93 +++++++++ linter-driven-development/setup/golangci.yaml | 82 ++++++++ linter-driven-development/setup/ruff.toml | 53 +++++ python-linter-driven-development/CHANGELOG.md | 13 ++ python-linter-driven-development/README.md | 12 +- .../scripts/ldd-detect_test.sh | 5 + .../scripts/ldd-lint.sh | 54 +++-- .../scripts/ldd-lint_test.sh | 187 ++++++++++++++++- .../scripts/ldd-scope_test.sh | 5 + .../setup/eslint.config.ldd.mjs | 93 +++++++++ .../setup/golangci.yaml | 82 ++++++++ .../setup/ruff.toml | 53 +++++ .../CHANGELOG.md | 13 ++ ts-react-linter-driven-development/README.md | 5 + .../scripts/ldd-detect_test.sh | 5 + .../scripts/ldd-lint.sh | 54 +++-- .../scripts/ldd-lint_test.sh | 187 ++++++++++++++++- .../scripts/ldd-scope_test.sh | 5 + .../setup/eslint.config.ldd.mjs | 93 +++++++++ .../setup/golangci.yaml | 82 ++++++++ .../setup/ruff.toml | 53 +++++ 56 files changed, 2353 insertions(+), 78 deletions(-) create mode 100644 core/setup/eslint.config.ldd.mjs create mode 100644 core/setup/golangci.yaml create mode 100644 core/setup/ruff.toml create mode 100644 go-linter-driven-development/setup/eslint.config.ldd.mjs create mode 100644 go-linter-driven-development/setup/golangci.yaml create mode 100644 go-linter-driven-development/setup/ruff.toml create mode 100644 linter-driven-development/setup/eslint.config.ldd.mjs create mode 100644 linter-driven-development/setup/golangci.yaml create mode 100644 linter-driven-development/setup/ruff.toml create mode 100644 python-linter-driven-development/setup/eslint.config.ldd.mjs create mode 100644 python-linter-driven-development/setup/golangci.yaml create mode 100644 python-linter-driven-development/setup/ruff.toml create mode 100644 ts-react-linter-driven-development/setup/eslint.config.ldd.mjs create mode 100644 ts-react-linter-driven-development/setup/golangci.yaml create mode 100644 ts-react-linter-driven-development/setup/ruff.toml diff --git a/core/README.md b/core/README.md index 07f0e7a..49136ae 100644 --- a/core/README.md +++ b/core/README.md @@ -190,6 +190,16 @@ differently from the other bindings: matrix reads the binding's `scripts/ldd-fixture.sh` for the source suffix, the comment forms, a test file's name and two linter configs per row: one that names the file-length rule and one that names a rule of a similar name. +- `setup/golangci.yaml`, `setup/ruff.toml` and `setup/eslint.config.ldd.mjs`, the + recommended linter configurations, are core templates rendered into every plugin, + so the generic plugin carries all three for the languages it reviews. Each enables + the linter rules the bindings' routing tables route to a design rule, at the + limits the rules name, and takes the file-length limit from `{{.FileMaxLines}}`. + Each names its linter on purpose and carries a `residue-exempt:` marker. The gap + linter's threshold line names the file and rule that would hold a limit, and the + `ldd-lint` matrix checks that the file sets that rule at the scalar's value; when + `golangci-lint`, `ruff` or `node` is on PATH, the matrix also has each tool check + its file. - `scripts/check-repo-brain_test.sh` is a core template whose cases read one include, `scripts/repo-brain-fixture.sh`: the fixture rows (marker, code file, second package, sub-project) for every language the rendered gate's adapter serves. The @@ -220,11 +230,11 @@ Hard residue: none. No plugin-name or command-prefix literal, golangci reference, source-file glob, nolint directive, scalar word or retired Go idiom is left in core/. -Soft residue by token (130 lines): +Soft residue by token (131 lines): | Token | Lines | Files | |---|---:|---:| -| interface | 85 | 24 | +| interface | 86 | 25 | | Go (the word) | 13 | 8 | | Go code fence | 12 | 1 | | Go stdlib | 9 | 5 | @@ -234,7 +244,7 @@ Soft residue by token (130 lines): | race detector | 2 | 2 | | sync. | 1 | 1 | -Soft residue by file (130 lines): +Soft residue by file (131 lines): | File | Lines | Tokens | |---|---:|---:| @@ -273,5 +283,6 @@ Soft residue by file (130 lines): | `rules/R2-self-validating-types.md` | 1 | 1 | | `rules/R7-test-placement.md` | 1 | 1 | | `scripts/ldd-scope_test.sh` | 1 | 1 | +| `setup/golangci.yaml` | 1 | 1 | | `skills/linter-driven-development/SKILL.md` | 1 | 1 | diff --git a/core/includes/scripts/ldd-lint-langs.sh b/core/includes/scripts/ldd-lint-langs.sh index 4a7509b..54c3325 100644 --- a/core/includes/scripts/ldd-lint-langs.sh +++ b/core/includes/scripts/ldd-lint-langs.sh @@ -21,9 +21,15 @@ # nothing. A named rule belongs to the linter: # its limit, its counting and whether it is on # are the repository's decision. -# lint_file_length_snippet the linter setting that would hold the -# limit, or the .ldd-lint.yaml entry when no -# linter rule is known: text to propose +# lint_setup_files one line per recommended config under the +# plugin's setup/: file TAB the tool that checks it +# lint_setup_check run that tool's own check of +# the file; exit 0 when the tool accepts it +# lint_file_length_proposal what would hold the limit in the +# repository's linter: the rule, named after +# the plugin's recommended config under setup/ +# that sets it, or the .ldd-lint.yaml entry when +# the language's linter has no such rule lint_comment_style() { case "$1" in @@ -126,12 +132,27 @@ lint_file_length_rule() { return 0 } -lint_file_length_snippet() { +lint_setup_files() { + printf 'golangci.yaml\tgolangci-lint\n' + printf 'ruff.toml\truff\n' + printf 'eslint.config.ldd.mjs\tnode\n' +} + +lint_setup_check() { # + case "$1" in + golangci.yaml) golangci-lint config verify -c "$2/$1" ;; + ruff.toml) printf 'x = 1\n' > "$3/ldd_setup_check.py" && (cd "$3" && ruff check --no-cache --config "$2/$1" ldd_setup_check.py) ;; + eslint.config.ldd.mjs) node --check "$2/$1" ;; + *) return 2 ;; + esac +} + +lint_file_length_proposal() { case "$1" in - go) printf "revive in .golangci.yaml: - name: file-length-limit, arguments: [{max: %s, skip-comments: true, skip-blank-lines: true}]" "$2" ;; - typescript|javascript) printf "the eslint config: 'sonarjs/max-lines': ['error', { maximum: %s }]" "$2" ;; - python) printf "pyproject.toml: [tool.pylint.format] max-module-lines = %s (pylint counts every line, comments and blank lines too)" "$2" ;; - *) printf "no linter rule is known for %s; a .ldd-lint.yaml entry file_max_lines: %s would state this limit" "$1" "$2" ;; + go) printf "a setting to propose, never to add on your own: revive file-length-limit, as the plugin's setup/golangci.yaml sets it" ;; + typescript|javascript) printf "a setting to propose, never to add on your own: sonarjs/max-lines, as the plugin's setup/eslint.config.ldd.mjs sets it" ;; + python) printf "ruff has no file-length rule, so this script holds the limit; a .ldd-lint.yaml entry file_max_lines: states another" ;; + *) printf "no linter rule is known for %s; a .ldd-lint.yaml entry file_max_lines: states another limit" "$1" ;; esac } # ================== end linter table ================== \ No newline at end of file diff --git a/core/scripts/ldd-lint.sh b/core/scripts/ldd-lint.sh index 90d76ae..5710d7f 100755 --- a/core/scripts/ldd-lint.sh +++ b/core/scripts/ldd-lint.sh @@ -162,6 +162,16 @@ trap 'rm -f "$SCOPE" "$INVENTORY"' EXIT list_candidates | by_language > "$SCOPE" if [[ -n "$FILES_LIST" ]]; then list_repo | by_language > "$INVENTORY"; else cp "$SCOPE" "$INVENTORY"; fi +# dir_of — the directory part of a relative path, "." for a file at +# the root. A function, because bash 3.2 misreads a case pattern's ")" inside +# a process substitution. +dir_of() { + case "$1" in + */*) printf '%s\n' "${1%/*}" ;; + *) printf '.\n' ;; + esac +} + # measured — exit 0 when the file counts for this language: not under # an excluded directory, not a test, not generated. Every path the script # hands to find, head or awk starts with "./", and a directory is cut from a @@ -193,7 +203,7 @@ run_file_length() { # — every measured file of that language say "threshold: $id file length — set by $OVERRIDE_FILE ($max code lines)" else max="$DEFAULT_FILE_MAX_LINES"; src="profile default" - say "threshold: $id file length — profile default ($max code lines); the repository's linter does not hold it; a setting to propose, never to add on your own: $(lint_file_length_snippet "$id" "$max")" + say "threshold: $id file length — profile default ($max code lines); the repository's linter does not hold it; $(lint_file_length_proposal "$id")" fi while IFS=$'\t' read -r _ f; do measured "$f" || continue @@ -217,7 +227,7 @@ run_package_size() { # — every directory that holds a measured file of th awk -F'\t' -v id="$id" '$1 == id { print $2 }' "$INVENTORY" | while IFS= read -r f; do [[ -n "$skip" ]] && printf '%s\n' "$f" | grep -qE -- "$skip" && continue measured "$f" || continue - case "$f" in */*) printf '%s\n' "${f%/*}" ;; *) printf '.\n' ;; esac + dir_of "$f" done | LC_ALL=C sort | uniq -c | awk '{ n = $1; sub(/^[ \t]*[0-9]+[ \t]/, ""); print $0 "\t" n }' > "$counts" while IFS= read -r d; do n=$(awk -F'\t' -v d="$d" '$1 == d { print $2; exit }' "$counts") @@ -228,8 +238,7 @@ run_package_size() { # — every directory that holds a measured file of th finding "$d" "$n source files (warning at $warn, $src)" fi done < <(awk -F'\t' -v id="$id" '$1 == id { print $2 }' "$SCOPE" | while IFS= read -r f; do - measured "$f" || continue - case "$f" in */*) printf '%s\n' "${f%/*}" ;; *) printf '.\n' ;; esac + measured "$f" && dir_of "$f" done | LC_ALL=C sort -u) rm -f "$counts" } diff --git a/core/scripts/ldd-lint_test.sh b/core/scripts/ldd-lint_test.sh index 3b81d47..d0ee69b 100644 --- a/core/scripts/ldd-lint_test.sh +++ b/core/scripts/ldd-lint_test.sh @@ -87,6 +87,8 @@ fill_dir() { {{include "scripts/ldd-fixture.sh"}} +{{include "scripts/ldd-lint-langs.sh"}} + # ========================= cases ========================= run_row_cases() { # runs every case against the current row @@ -146,7 +148,16 @@ finish begin "the plugin's default names a linter setting to propose, never to add" write_src "src/a$FX_EXT" 1 run_lint file-length -expect_exit 0 && expect_has "profile default ($MAX_LINES code lines)" && expect_has "a setting to propose, never to add on your own: " && ok +SETUP_DIR="$(dirname "$LINT")/../setup" +if [[ -n "$FX_SETUP_FILE" ]]; then + expect_exit 0 && expect_has "profile default ($MAX_LINES code lines)" \ + && expect_has "a setting to propose, never to add on your own: " \ + && expect_has "$FX_LENGTH_RULE, as the plugin's setup/$FX_SETUP_FILE sets it" \ + && { grep -q -- "$FX_LENGTH_RULE" "$SETUP_DIR/$FX_SETUP_FILE" && grep -q -- "$MAX_LINES" "$SETUP_DIR/$FX_SETUP_FILE" || bad "setup/$FX_SETUP_FILE does not set $FX_LENGTH_RULE at $MAX_LINES"; } \ + && grep -q -- "$FX_LENGTH_RULE" "$SETUP_DIR/$FX_SETUP_FILE" && ok +else + expect_exit 0 && expect_has "profile default ($MAX_LINES code lines)" && expect_has "a .ldd-lint.yaml entry file_max_lines" && ok +fi finish begin "a file-length rule in the linter config hands file length to the linter" @@ -316,6 +327,18 @@ for opt in --root --files --glob --test-re; do finish done +# --- the recommended configs under setup/, checked by their own linters when +# the machine has them --- +SETUP_DIR="$(dirname "$LINT")/../setup" + +while IFS=$'\t' read -r setup_file setup_tool; do + begin "setup/$setup_file passes its own linter's check" + if ! command -v "$setup_tool" >/dev/null 2>&1; then skip "$setup_tool is not on PATH"; finish; continue; fi + OUT=$(lint_setup_check "$setup_file" "$SETUP_DIR" "$REPO" 2>&1); CODE=$?; ERR="" + expect_exit 0 && ok + finish +done < <(lint_setup_files) + # ========================= summary ========================= echo echo "ldd-lint_test: $pass passed, $failed failed, $skipped skipped" diff --git a/core/setup/eslint.config.ldd.mjs b/core/setup/eslint.config.ldd.mjs new file mode 100644 index 0000000..f31218c --- /dev/null +++ b/core/setup/eslint.config.ldd.mjs @@ -0,0 +1,93 @@ +// Recommended ESLint flat-config fragment of the linter-driven-development +// plugins: every rule the plugin's routing table routes to one of the twelve +// design rules, at the limits the rules name. Each rule line says which rule its +// failures feed. The plugin never writes this file into a repository: the +// repository's owners copy it, or the parts they want, beside eslint.config.js +// and spread it into the exported array: +// +// import ldd from './eslint.config.ldd.mjs' +// export default [...yourConfig, ...ldd] +// +// residue-exempt: an ESLint configuration names its plugins and rules on purpose. +// It needs eslint-plugin-sonarjs, eslint-plugin-react, eslint-plugin-react-hooks, +// eslint-plugin-promise, eslint-plugin-import and typescript-eslint; a repository +// that does not use one of them leaves its rules out. Limits a repository already +// sets stay the repository's: the linter is the authority for every threshold it +// holds. Rules this file leaves out (style, imports, formatting) are the +// repository's own choice. +// +// Adopt it on an existing codebase with ESLint's bulk suppressions: run +// `npx eslint --suppress-all` once to record today's violations, so only new ones +// fail; `npx eslint --prune-suppressions` drops the entries that are fixed. + +import importPlugin from 'eslint-plugin-import' +import promise from 'eslint-plugin-promise' +import react from 'eslint-plugin-react' +import reactHooks from 'eslint-plugin-react-hooks' +import sonarjs from 'eslint-plugin-sonarjs' +import tseslint from 'typescript-eslint' + +export default [ + { + files: ['**/*.{ts,tsx}'], + plugins: { + '@typescript-eslint': tseslint.plugin, + import: importPlugin, + promise, + react, + 'react-hooks': reactHooks, + sonarjs, + }, + rules: { + // R3 storifying + 'sonarjs/cognitive-complexity': ['error', 15], + 'sonarjs/cyclomatic-complexity': ['error', { threshold: 10 }], + 'sonarjs/expression-complexity': ['error', { max: 5 }], + 'sonarjs/max-lines-per-function': ['error', { maximum: 200 }], + 'sonarjs/nested-control-flow': ['error', { maximumNestingLevel: 4 }], + 'sonarjs/no-nested-conditional': 'error', + 'sonarjs/no-nested-functions': 'error', + 'sonarjs/elseif-without-else': 'error', + 'react/no-unstable-nested-components': 'error', + // R1 primitive obsession + 'max-params': ['error', 4], + 'sonarjs/no-duplicate-string': ['error', { threshold: 3 }], + 'sonarjs/max-union-size': ['error', { threshold: 4 }], + 'sonarjs/no-identical-functions': 'error', + // R2 self-validating types: parse at the boundary + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/no-non-null-assertion': 'error', + // R5 vertical slice: one juicy module per file; counts code lines only + 'sonarjs/max-lines': ['error', { maximum: {{.FileMaxLines}} }], + 'react/no-multi-comp': ['error', { ignoreStateless: false }], + // R8 no globals + 'import/no-mutable-exports': 'error', + // R10 concurrency safety + 'react-hooks/exhaustive-deps': 'error', + '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/no-misused-promises': 'error', + 'promise/catch-or-return': 'error', + // R11 conditional dispatch + '@typescript-eslint/switch-exhaustiveness-check': 'error', + 'sonarjs/no-nested-switch': 'error', + 'sonarjs/max-switch-cases': 'error', + 'sonarjs/no-small-switch': 'error', + // R12 mutation discipline + 'no-param-reassign': ['error', { props: true }], + 'sonarjs/prefer-read-only-props': 'error', + 'react/no-direct-mutation-state': 'error', + }, + }, + { + // Tests are long and repeat literals by design. + files: ['**/*.test.{ts,tsx}', '**/*.spec.{ts,tsx}'], + rules: { + 'sonarjs/max-lines-per-function': 'off', + 'sonarjs/no-duplicate-string': 'off', + 'sonarjs/no-nested-functions': 'off', + 'max-params': 'off', + 'react/no-multi-comp': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + }, + }, +] diff --git a/core/setup/golangci.yaml b/core/setup/golangci.yaml new file mode 100644 index 0000000..6e32efb --- /dev/null +++ b/core/setup/golangci.yaml @@ -0,0 +1,82 @@ +# Recommended golangci-lint v2 configuration of the linter-driven-development +# plugins: every linter rule the plugin's routing table routes to one of the +# twelve design rules, at the limits the rules name. Each linter line says which +# rule its failures feed. The plugin never writes this file into a repository: +# the repository's owners copy it, or the parts they want, into .golangci.yaml. +# residue-exempt: a golangci-lint configuration names its linters on purpose. +# +# Limits a repository already sets stay the repository's: the linter is the +# authority for every threshold it holds. Rules this file leaves out (style, +# naming, formatting) are the repository's own choice. +# +# Adopt it on an existing codebase with the ratchet below: only issues in code +# changed since the merge base with main fail, so old code is not a wall of +# findings on day one. Remove the ratchet once the old findings are gone. + +version: "2" + +issues: + new-from-merge-base: main + +linters: + default: none + enable: + - gocyclo # R3 storifying: cyclomatic complexity + - gocognit # R3 storifying: cognitive complexity + - funlen # R3 storifying: function length + - nestif # R3 storifying: nesting depth + - maintidx # R3 storifying, R1 types: maintainability index + - dupl # R1 types: duplicated logic; a duplicated switch is R11 + - exhaustive # R11 conditional dispatch: missing enum cases + - gochecknoglobals # R8 no globals + - gochecknoinits # R8 no globals + - ireturn # R6 test-only interfaces: a constructor that returns an interface + - govet # R10 concurrency: copylocks + - revive # R1 types, R5 vertical slice: the rules below + - errcheck # mechanical: an error that is not handled + - wrapcheck # mechanical: an error without the context of its layer + - goconst # mechanical; an enum-shaped string is R1's "Name enum strings" + settings: + gocyclo: + min-complexity: 10 + gocognit: + min-complexity: 15 + funlen: + lines: 50 + statements: 40 + nestif: + min-complexity: 2 + maintidx: + under: 20 + dupl: + threshold: 100 + exhaustive: + # A default branch does not count as handling a case: every new case is + # handled where the switch is. + default-signifies-exhaustive: false + govet: + enable: + - copylocks + goconst: + min-len: 3 + min-occurrences: 3 + revive: + rules: + - name: argument-limit # R1: more than 4 parameters wants a parameter object + arguments: [4] + - name: function-result-limit # R1: more than 3 results wants a named result type + arguments: [3] + - name: file-length-limit # R5: one juicy type per file; counts code lines only + arguments: [{max: {{.FileMaxLines}}, skip-comments: true, skip-blank-lines: true}] + - name: early-return # mechanical: invert the condition and return early + exclusions: + rules: + # Tests are long and repeat literals by design. + - path: _test\.go + linters: + - funlen + - gocognit + - gocyclo + - maintidx + - dupl + - goconst diff --git a/core/setup/ruff.toml b/core/setup/ruff.toml new file mode 100644 index 0000000..1ffb4c6 --- /dev/null +++ b/core/setup/ruff.toml @@ -0,0 +1,53 @@ +# Recommended ruff configuration of the linter-driven-development plugins: +# every rule the plugin's routing table routes to one of the twelve design +# rules, at the limits the rules name. Each rule says which rule its failures +# feed. The plugin never writes this file into a repository: the repository's +# owners copy it, or the parts they want, into ruff.toml, or into +# pyproject.toml with each table name prefixed by "tool.ruff." ([lint] becomes +# [tool.ruff.lint]). +# residue-exempt: a ruff configuration names its rule codes on purpose. +# +# Limits a repository already sets stay the repository's: the linter is the +# authority for every threshold it holds. Rules this file leaves out (style, +# docstrings, imports) are the repository's own choice. ruff has no file-length +# rule: the plugin's gap linter, scripts/ldd-lint.sh file-length, holds that +# limit at {{.FileMaxLines}} code lines. +# +# Adopt it on an existing codebase one change at a time: run ruff on the files +# changed since the merge base with main, so old code is not a wall of +# findings on day one: +# git diff --name-only --diff-filter=d "$(git merge-base HEAD main)" -- '*.py' | xargs ruff check + +[lint] +# PLR1702 (nested blocks) is a preview rule. +preview = true +explicit-preview-rules = true +extend-select = [ + "C901", # R3 storifying: complexity + "PLR0911", # R3 storifying: too many returns + "PLR0912", # R3 storifying: too many branches + "PLR0915", # R3 storifying: too many statements + "PLR1702", # R3 storifying: nesting + "PLR0913", # R1 types: too many arguments wants a parameter object + "PLR2004", # R1 types: a magic value; an enum-shaped string is "Name enum strings" + "FBT001", # R11 conditional dispatch: a boolean positional parameter + "FBT002", # R11 conditional dispatch: a boolean default parameter + "PLW0603", # R8 no globals: a global statement + "B006", # R12 mutation discipline: a mutable default argument + "B008", # R12 mutation discipline: a call in a default argument +] + +[lint.mccabe] +max-complexity = 8 + +[lint.pylint] +max-args = 4 # self and cls are not counted +max-branches = 12 +max-returns = 6 +max-statements = 50 +max-nested-blocks = 5 + +[lint.per-file-ignores] +# Tests repeat literals and take many fixtures by design. +"**/test_*.py" = ["PLR2004", "PLR0913", "FBT001", "FBT002"] +"**/*_test.py" = ["PLR2004", "PLR0913", "FBT001", "FBT002"] diff --git a/docs/quick-start.md b/docs/quick-start.md index 3b60ecf..9d92c40 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.md @@ -25,8 +25,11 @@ they share it. /plugin install go-linter-driven-development@ai-coding-rules ``` -No configuration. The plugin finds your test and lint commands in the Taskfile, -Makefile, README or CLAUDE.md. The first time the workflow starts, Claude asks for +No configuration to run. The plugin finds your test and lint commands in the +Taskfile, Makefile, README or CLAUDE.md. The plugin's `setup/` directory holds a +recommended linter configuration for Go, Python and TypeScript; copying it, or the +parts you want, makes the design triggers fire. The plugin never writes it into +your repository. The first time the workflow starts, Claude asks for permission to use the skill. Choose "Yes, and don't ask again for this directory". ## 1. A feature or a bug fix, from zero diff --git a/go-linter-driven-development/CHANGELOG.md b/go-linter-driven-development/CHANGELOG.md index 36bff47..ef2e41a 100644 --- a/go-linter-driven-development/CHANGELOG.md +++ b/go-linter-driven-development/CHANGELOG.md @@ -7,6 +7,15 @@ Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versio ### Added +- **A recommended linter setup.** `setup/golangci.yaml`, `setup/ruff.toml` and + `setup/eslint.config.ldd.mjs` enable every linter rule the routing table routes to + a design rule, at the limits the rules name, with a way to adopt each on an + existing codebase: golangci-lint's merge-base ratchet, ESLint's bulk + suppressions, and ruff on changed files. The plugin never writes them into a + repository. The gap linter's threshold line names the file and rule that would + hold a file-length limit; for Python, where ruff has no file-length rule, it says + that the gap linter holds it. + - **A gap linter for the design triggers no linter holds.** `scripts/ldd-lint.sh` measures lines per source file (`file-length`) and source files per directory (`package-size`). Where the repository's linter config names a file-length rule, @@ -33,6 +42,10 @@ Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versio ### Fixed +- **The gap linter runs under bash 3.2.** macOS's `/bin/bash` stopped + `ldd-lint.sh` with a syntax error: bash 3.2 misreads a `case` pattern inside a + process substitution. The script now runs under bash 3.2 and bash 5. + - **A value-taking option given last no longer hangs the review scripts.** When `ldd-detect.sh` or `ldd-scope.sh` got an option such as `--root` or `--base` as its last argument, the script looped forever. It now stops with "