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..ea3e885 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 @@ -112,8 +118,10 @@ lint_file_length_rule() { f=$(_lint_names 'file-length-limit' .golangci.yaml .golangci.yml .golangci.toml .golangci.json) \ && printf 'revive file-length-limit in %s' "$f" ;; typescript|javascript) + # eslint.config.ldd.mjs is the plugin's recommended fragment: a repository + # that adopts it keeps it beside eslint.config.js and imports it. local configs=(eslint.config.js eslint.config.mjs eslint.config.cjs eslint.config.ts eslint.config.mts eslint.config.cts - .eslintrc .eslintrc.js .eslintrc.cjs .eslintrc.json .eslintrc.yml .eslintrc.yaml) + eslint.config.ldd.mjs .eslintrc .eslintrc.js .eslintrc.cjs .eslintrc.json .eslintrc.yml .eslintrc.yaml) if f=$(_lint_names 'sonarjs/max-lines([^-a-zA-Z]|$)' "${configs[@]}"); then printf 'sonarjs/max-lines in %s' "$f" elif f=$(_lint_names '(^|[^-/a-zA-Z])max-lines([^-a-zA-Z]|$)' "${configs[@]}"); then @@ -126,12 +134,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..cce23a0 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" @@ -178,6 +189,39 @@ run_lint file-length expect_exit 0 && expect_not "held by " && expect_has "src/over$FX_EXT: $((MAX_LINES + 1)) code lines" && ok finish +begin "the recommended config enables every rule the routing table routes to a design rule" +if [[ -z "$FX_ROUTED_FILE" ]]; then skip "this plugin's routing table names no linter rule"; finish +else + table=$(grep -l '^| Linter failure | Route |' "$(dirname "$LINT")"/../skills/refactoring/*.md | head -1) + missing=""; checked=0 + while IFS= read -r id; do + case " $FX_ROUTED_SKIP " in *" $id "*) continue ;; esac + checked=$((checked + 1)) + case "$id" in + *'*') re="(^|[^A-Za-z0-9/@-])${id%\*}" ;; + *) re="(^|[^A-Za-z0-9/@-])${id}([^A-Za-z0-9/-]|\$)" ;; + esac + grep -qE -- "$re" "$(dirname "$LINT")/../setup/$FX_ROUTED_FILE" || missing="$missing $id" + done < <(awk -F'|' ' + /^\| Linter failure \| Route \|/ { t = 1; next } + t && /^\|---/ { next } + t && /^\|/ { if ($3 ~ /rules\/R/) { c = $2; while (match(c, /`[^`]+`/)) { print substr(c, RSTART + 1, RLENGTH - 2); c = substr(c, RSTART + RLENGTH) } } next } + t { t = 0 }' "$table" | grep -E -- "$FX_RULE_ID_RE" | LC_ALL=C sort -u) + { [[ -n "$table" ]] && (( checked > 0 )) || bad "no routed rule id found in the routing table"; } \ + && { [[ -z "$missing" ]] || bad "setup/$FX_ROUTED_FILE lacks:$missing"; } && [[ -z "$missing" ]] && ok + finish +fi + +begin "the recommended fragment, kept as its own file and imported, hands file length to the linter" +if [[ -z "$FX_ADOPTED_CONFIG" ]]; then skip "this language's setup is copied into the linter's own config"; finish +else + cp "$(dirname "$LINT")/../setup/$FX_ADOPTED_CONFIG" "$REPO/$FX_ADOPTED_CONFIG" + write_src "src/over$FX_EXT" $((MAX_LINES + 50)) + run_lint file-length + expect_exit 0 && expect_has "held by " && expect_has "in $FX_ADOPTED_CONFIG" && expect_no_finding && ok + finish +fi + begin ".ldd-lint.yaml sets the limit when the linter holds none" printf 'file_max_lines: 100 # this repository\n' > "$REPO/.ldd-lint.yaml" write_src "src/a$FX_EXT" 101 @@ -316,6 +360,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..d679c76 --- /dev/null +++ b/core/setup/eslint.config.ldd.mjs @@ -0,0 +1,115 @@ +// 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 before their own entries. Later entries win, so every limit the +// repository's config already sets stays the repository's: +// +// import ldd from './eslint.config.ldd.mjs' +// export default [...ldd, ...yourConfig] +// +// 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. The @typescript-eslint rules +// below need type information: the repository's config sets typescript-eslint's +// parser with parserOptions.projectService for these files, or ESLint stops with +// an error. 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, and so is +// no-restricted-syntax on import.meta.env outside the config module (R8): the +// config module's path is the repository's. +// +// 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 }], + // R3's limits: more than 50 lines or more than 2 nesting levels + 'sonarjs/max-lines-per-function': ['error', { maximum: 50 }], + 'sonarjs/nested-control-flow': ['error', { maximumNestingLevel: 2 }], + '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', + 'react/no-array-index-key': 'error', + 'no-magic-numbers': ['error', { + ignore: [-1, 0, 1, 2], + ignoreArrayIndexes: true, + ignoreDefaultValues: true, + ignoreClassFieldInitialValues: true, + detectObjects: false, + }], + // R2 self-validating types: parse at the boundary + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/no-non-null-assertion': 'error', + '@typescript-eslint/no-unsafe-argument': 'error', + '@typescript-eslint/no-unsafe-assignment': 'error', + '@typescript-eslint/no-unsafe-call': 'error', + '@typescript-eslint/no-unsafe-member-access': 'error', + '@typescript-eslint/no-unsafe-return': 'error', + '@typescript-eslint/no-unnecessary-condition': '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', + 'react-hooks/set-state-in-effect': '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', + 'no-magic-numbers': '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..be5a0ac --- /dev/null +++ b/core/setup/golangci.yaml @@ -0,0 +1,86 @@ +# 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. CI needs main in the clone for it (fetch-depth: 0 in +# GitHub Actions). Remove the ratchet once the old findings are gone. +# gocyclo stands for the routing table's "gocyclo / cyclop": enable one of the two. + +version: "2" + +issues: + new-from-merge-base: main + +linters: + # The standard set (errcheck, govet, ineffassign, staticcheck, unused) stays on; + # the linters below are added to it. + default: standard + 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..bca37e5 --- /dev/null +++ b/core/setup/ruff.toml @@ -0,0 +1,58 @@ +# 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 -z --name-only --diff-filter=d "$(git merge-base HEAD main)" -- '*.py' \ +# | xargs -0 sh -c '[ "$#" -eq 0 ] || ruff check "$@"' sh +# The sh guard keeps an empty change list from checking the whole repository, +# and -z with -0 keeps a path with spaces whole. + +[lint] +# PLR1702 (nested blocks) is a preview rule. Preview mode also turns on preview +# behavior of the stable rules a repository already selects; a repository that +# does not want that drops these two lines and PLR1702. +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 = 2 # R3: more than 2 nesting levels + +[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 "