habit-hooks-ruby 1.5.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.
- habit_hooks_ruby-1.5.0/.gitignore +26 -0
- habit_hooks_ruby-1.5.0/CLAUDE.md +171 -0
- habit_hooks_ruby-1.5.0/PKG-INFO +289 -0
- habit_hooks_ruby-1.5.0/README.md +265 -0
- habit_hooks_ruby-1.5.0/docs/ruby-plugin.spec.md +268 -0
- habit_hooks_ruby-1.5.0/pyproject.toml +52 -0
- habit_hooks_ruby-1.5.0/src/habit_hooks_ruby/__init__.py +1 -0
- habit_hooks_ruby-1.5.0/src/habit_hooks_ruby/config.toml +47 -0
- habit_hooks_ruby-1.5.0/src/habit_hooks_ruby/guides/oversized-block.md +9 -0
- habit_hooks_ruby-1.5.0/src/habit_hooks_ruby/guides/parse-error.md +7 -0
- habit_hooks_ruby-1.5.0/src/habit_hooks_ruby/sensors/rubocop.toml +1 -0
- habit_hooks_ruby-1.5.0/src/habit_hooks_ruby/sensors/rubocop_report.py +110 -0
- habit_hooks_ruby-1.5.0/src/habit_hooks_ruby/sensors/rubocop_sensor.py +159 -0
- habit_hooks_ruby-1.5.0/tests/conftest.py +43 -0
- habit_hooks_ruby-1.5.0/tests/test_a_crashing_rubocop_fails_the_run.py +192 -0
- habit_hooks_ruby-1.5.0/tests/test_the_rubocop_pipeline_maps_cops_to_smells.py +141 -0
- habit_hooks_ruby-1.5.0/tests/test_the_rubocop_sensor_runs_the_real_tool.py +191 -0
- habit_hooks_ruby-1.5.0/tests/test_the_ruby_files_leave_installed_gems_alone.py +89 -0
- habit_hooks_ruby-1.5.0/tests/test_the_sensor_hands_rubocop_literal_filenames.py +99 -0
- habit_hooks_ruby-1.5.0/tests/test_the_sensor_runs_the_rubocop_it_is_handed.py +92 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Per-plugin Node tool deps (installed via pnpm install; see pnpm-workspace.yaml)
|
|
2
|
+
node_modules/
|
|
3
|
+
dist
|
|
4
|
+
coverage
|
|
5
|
+
.DS_Store
|
|
6
|
+
.idea
|
|
7
|
+
.claude-channel/
|
|
8
|
+
*.tgz
|
|
9
|
+
*.log
|
|
10
|
+
.vscode/
|
|
11
|
+
.venv/
|
|
12
|
+
__pycache__/
|
|
13
|
+
*.pyc
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
.spec-runs/
|
|
16
|
+
|
|
17
|
+
# Workflow orchestration script (run from ~/.claude, never a repo deliverable)
|
|
18
|
+
.claude/workflows-build-overnight.js
|
|
19
|
+
|
|
20
|
+
# Agent worktrees (created by the harness inside the checkout)
|
|
21
|
+
.claude/worktrees/
|
|
22
|
+
/scratches/
|
|
23
|
+
|
|
24
|
+
# unsupervised-issues run signals (live mode switch + stop flag)
|
|
25
|
+
.unsupervised-issues.mode
|
|
26
|
+
.unsupervised-issues.stop
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# habit-hooks-ruby notes
|
|
2
|
+
|
|
3
|
+
## Architecture
|
|
4
|
+
|
|
5
|
+
### The project's `.rubocop.yml` decides everything
|
|
6
|
+
|
|
7
|
+
The sensor passes no `--only` and no `--config`. RuboCop finds the project's
|
|
8
|
+
config by its own upward walk, so a project's habit-hooks run is the run it gets
|
|
9
|
+
from RuboCop directly. There is no shipped fallback config, and the plugin never
|
|
10
|
+
writes one into a project. The README carries a suggested `.rubocop.yml` as
|
|
11
|
+
documentation only — unlike jscpd or pmd, RuboCop's config already exists in
|
|
12
|
+
almost every real Ruby project, so this plugin has no fallback case to cover.
|
|
13
|
+
|
|
14
|
+
`--force-exclusion` is the one flag the sensor adds, and it is not optional.
|
|
15
|
+
RuboCop applies `AllCops: Exclude:` to the files it *discovers*, but treats a
|
|
16
|
+
file named on the command line as a deliberate request and lints it anyway.
|
|
17
|
+
Since habit-hooks always names a scope, without that flag a project's own
|
|
18
|
+
exclusions would stop meaning anything the moment this tool ran.
|
|
19
|
+
|
|
20
|
+
### An unmapped cop is forwarded, not dropped
|
|
21
|
+
|
|
22
|
+
This is the same exception to "a sensor emits vocabulary smells only" that the
|
|
23
|
+
root `CLAUDE.md` makes for eslint: a cop that fired is one the project's own
|
|
24
|
+
`.rubocop.yml` turned on, so forwarding it forwards the project's own decision.
|
|
25
|
+
|
|
26
|
+
The smell key is the cop name verbatim, `Style/StringLiterals`. Nothing downstream breaks on one: the
|
|
27
|
+
guide lookup misses, the finding renders through `uncoached.md`, and the root
|
|
28
|
+
`uncoached` key (default `suggest`) decides whether it fails the run.
|
|
29
|
+
|
|
30
|
+
Two cops are unmapped **on purpose** rather than by omission, so do not "fix"
|
|
31
|
+
them by adding rows. `Metrics/ClassLength` and `Metrics/ModuleLength` measure a
|
|
32
|
+
class or module, and the vocabulary has no class-scoped smell for them to back.
|
|
33
|
+
They stay **uncoached** — forwarded under their own names, rendered through
|
|
34
|
+
`uncoached.md`, `suggest` until a class and module scoped coach exists.
|
|
35
|
+
|
|
36
|
+
The three complexity cops all map to `high-complexity` (human decision). They
|
|
37
|
+
are correlated but independent — `PerceivedComplexity` weights nesting and
|
|
38
|
+
`AbcSize` counts assignments and calls, so either fires without
|
|
39
|
+
`CyclomaticComplexity` — and an unmapped complexity cop coached a genuinely
|
|
40
|
+
tangled method through `uncoached.md` when the real guide applied verbatim. The
|
|
41
|
+
old objection, one method reported three times over, is dissolved by the
|
|
42
|
+
mapper: this sensor groups every cop that fired on a smell into one finding's
|
|
43
|
+
issue list, which is never deduped (issue #140), so a method tripping two cops
|
|
44
|
+
keeps both measurements inside a single coaching block.
|
|
45
|
+
|
|
46
|
+
## Gotchas
|
|
47
|
+
|
|
48
|
+
### RuboCop reads a file argument as options, then as a glob
|
|
49
|
+
|
|
50
|
+
A scope filename is exact, and RuboCop reads its file arguments two other ways
|
|
51
|
+
before it reads them as names. A filename beginning with `-` is parsed as short
|
|
52
|
+
options — `-c` among them, which takes the rest as its `--config` value — and an
|
|
53
|
+
argument containing a `*` is handed to `Dir[]`
|
|
54
|
+
(`TargetFinder#process_explicit_path`), so a literal star sweeps in every file
|
|
55
|
+
it matches. `run_rubocop` therefore puts `--` between its flags and the files,
|
|
56
|
+
and `literal_spelling_of` escapes the glob metacharacters.
|
|
57
|
+
|
|
58
|
+
The escaping is narrower than it looks, and has to be. RuboCop globs only an
|
|
59
|
+
argument containing a `*`; every other one it takes verbatim, backslashes
|
|
60
|
+
included, so escaping a `?` in a starless path would name a file that does not
|
|
61
|
+
exist and the run would die on `Error: No such file or directory`. The other
|
|
62
|
+
metacharacters are escaped only inside a starred argument, where `Dir[]` is
|
|
63
|
+
reading the whole thing as a pattern. The literal-star behavioural test runs
|
|
64
|
+
only where a filesystem allows a `*` in a name — Windows forbids it — so it
|
|
65
|
+
skips there through
|
|
66
|
+
`tests/platform_probe.A_FILESYSTEM_THAT_ALLOWS_A_STAR_IN_A_FILENAME`.
|
|
67
|
+
|
|
68
|
+
### RuboCop's exit code lies when the binstub never reached RuboCop
|
|
69
|
+
|
|
70
|
+
`ruff_sensor` can trust ruff's exit code. `rubocop` is a RubyGems
|
|
71
|
+
binstub beginning `#!/usr/bin/env ruby`, so it is only as good as the `ruby`
|
|
72
|
+
that answers first. Point it at an interpreter it is not installed into (a
|
|
73
|
+
version manager left off `PATH`, the wrong bundle, macOS's system Ruby 2.6) and
|
|
74
|
+
it dies in `find_spec_for_exe` with a Ruby traceback and **exit 1** — the code
|
|
75
|
+
reserved for "I found offences". Judged on the code alone, that is a clean file
|
|
76
|
+
from a tool that never started, which is the false-clean class issue #88 exists
|
|
77
|
+
for.
|
|
78
|
+
|
|
79
|
+
So the report is the evidence, not the code. `--format json` prints its envelope
|
|
80
|
+
on every run RuboCop completed, down to `"files": []` when it inspected nothing,
|
|
81
|
+
so no envelope means no run whatever it exited with. `report()` answers `None`
|
|
82
|
+
for anything that is not a report, and it requires the `files` key rather than
|
|
83
|
+
merely valid JSON. `rubocop_crashed()` takes the parsed report as an argument
|
|
84
|
+
instead of re-deriving it, so the rule lives in one place and `main` cannot
|
|
85
|
+
drift from what the tests exercise.
|
|
86
|
+
|
|
87
|
+
Ask this of any wrapped tool reached through an interpreter shim rather than a
|
|
88
|
+
binary. The cost of getting it wrong is silence, and silence reads as success.
|
|
89
|
+
|
|
90
|
+
The envelope check has one concern: a cop that *raises exceptions*.
|
|
91
|
+
By default, RuboCop rescues the exception, reports the crash on stderr,
|
|
92
|
+
and exits 1 with a valid envelope listing that file's offences as `[]`.
|
|
93
|
+
This isn't what habit-hooks needs to detect the crash. So `--raise-cop-error`
|
|
94
|
+
is used to make a RuboCop crash become an `Error:` and exit 2, which
|
|
95
|
+
habit-hooks interprets as a failed run.
|
|
96
|
+
|
|
97
|
+
`tests/test_the_sensor_runs_the_rubocop_it_is_handed.py` has to hand `ruby` back
|
|
98
|
+
on a directory of its own for the same reason. The binstub and its interpreter
|
|
99
|
+
live in one directory, so taking `rubocop` off `PATH` takes `ruby` with it, and
|
|
100
|
+
the test would then be proving the crash rather than the lookup.
|
|
101
|
+
|
|
102
|
+
### RuboCop globs every ancestor directory looking for a gemspec
|
|
103
|
+
|
|
104
|
+
`TargetRuby` settles which Ruby to parse as by trying, in order,
|
|
105
|
+
`TargetRubyVersion` in the config, then any `*.gemspec`, then `.ruby-version`.
|
|
106
|
+
The gemspec step is a `Dir.glob` up every ancestor directory to the filesystem
|
|
107
|
+
root, and **`.ruby-version` does not prevent it**, because RuboCop looks for the
|
|
108
|
+
gemspec first. A Rails app, which has no gemspec, therefore sends RuboCop
|
|
109
|
+
climbing out of the project on every run. A gem stops the climb by having a
|
|
110
|
+
gemspec; everything else stops it by pinning `TargetRubyVersion`.
|
|
111
|
+
|
|
112
|
+
That is RuboCop's own behaviour and the sensor reproduces it rather than
|
|
113
|
+
papering over it, per the precedence rule in the root `CLAUDE.md`. It matters
|
|
114
|
+
for the tests because the spec harness runs each case in
|
|
115
|
+
`<repo>/.spec-runs/tmpXXXX/`: a case pinning neither climbs through this
|
|
116
|
+
checkout and out into the home directory, where a sandboxed dev machine denies
|
|
117
|
+
the glob outright and the sensor fails for a reason that has nothing to do with
|
|
118
|
+
the case. Every case in `docs/ruby-plugin.spec.md` and
|
|
119
|
+
`tests/installed_projects.ruby_project` pins `TargetRubyVersion`.
|
|
120
|
+
|
|
121
|
+
It is the RuboCop counterpart of the root `CLAUDE.md`'s `GIT_CEILING_DIRECTORIES`
|
|
122
|
+
rule and of jscpd's `.gitignore` walk: a wrapped tool that searches upward has
|
|
123
|
+
to be given a floor, or it finds ours.
|
|
124
|
+
|
|
125
|
+
### A Rails project's rubocop is the only one that can read its config
|
|
126
|
+
|
|
127
|
+
A `.rubocop.yml` naming cops from an extension gem is a hard RuboCop error when
|
|
128
|
+
that gem is not loadable:
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
Error: `Rails/*` has been extracted to the `rubocop-rails` gem.
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Nearly every Rails repo pins rubocop plus `rubocop-rails` / `rubocop-rspec` /
|
|
135
|
+
`rubocop-performance` in its `Gemfile`, and those gems load only under the
|
|
136
|
+
project's own bundle. This is the likeliest way the sensor fails in practice,
|
|
137
|
+
and it is why the plugin's rubocop detector declares
|
|
138
|
+
`search_paths = ["bin"]` (`src/habit_hooks_ruby/config.toml`):
|
|
139
|
+
`bundle binstubs rubocop` writes `bin/rubocop`, and every lookup for that
|
|
140
|
+
tool — `missing_tools` clearing it, `sensors/named_tools.py` resolving the
|
|
141
|
+
recipe's `${detector:rubocop}` — searches the project's `bin` ahead of the
|
|
142
|
+
default path.
|
|
143
|
+
|
|
144
|
+
The lookup belongs in the core and not in this sensor. `missing_tools`
|
|
145
|
+
clears a tool by asking `tool_executable`, and `sensors/spawn.py` spawns the
|
|
146
|
+
file it answered with, so a second answer here would let setup clear a rubocop
|
|
147
|
+
the run then does not use. And `bin` is deliberately **not** on the default
|
|
148
|
+
search path (`project_paths.tool_search_path`), only on this detector's:
|
|
149
|
+
`node_modules/.bin` and `.venv/bin` are directories an install keeps to
|
|
150
|
+
itself, where a `bin` may hold a project's own scripts, and a script named
|
|
151
|
+
after a tool should not outrank a real install beside it. Naming the
|
|
152
|
+
directory on the detector keeps it in play for exactly the tools that live
|
|
153
|
+
in it.
|
|
154
|
+
|
|
155
|
+
## Testing
|
|
156
|
+
|
|
157
|
+
`tests/conftest.py` puts `src/habit_hooks_ruby/sensors` on `sys.path` so the
|
|
158
|
+
helper loads as a loose top-level module, which is how
|
|
159
|
+
`${python} ${dir}/rubocop_sensor.py` loads it. Reaching it as
|
|
160
|
+
`habit_hooks_ruby.sensors.rubocop_sensor` is a path no run ever takes.
|
|
161
|
+
|
|
162
|
+
A missing rubocop is a `pytest.fail`, not a skip, matching every other plugin: a
|
|
163
|
+
machine without it is a suite that has quietly stopped gating, not a machine
|
|
164
|
+
this plugin does not apply to.
|
|
165
|
+
|
|
166
|
+
The split between `test_the_rubocop_pipeline_maps_cops_to_smells.py` and
|
|
167
|
+
`test_the_rubocop_sensor_runs_the_real_tool.py` is worth keeping. The first says
|
|
168
|
+
what RuboCop's output *becomes* and runs no subprocess; the second proves
|
|
169
|
+
RuboCop still produces output of the shape the first assumes. A RuboCop upgrade
|
|
170
|
+
that renames `cop_name` or restructures `location` fails the second alone, which
|
|
171
|
+
is the signal you want.
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: habit-hooks-ruby
|
|
3
|
+
Version: 1.5.0
|
|
4
|
+
Summary: The Ruby Habit Hooks plugin
|
|
5
|
+
Project-URL: Homepage, https://habit-hooks.com
|
|
6
|
+
Project-URL: Repository, https://github.com/habit-hooks/habit-hooks
|
|
7
|
+
Project-URL: Issues, https://github.com/habit-hooks/habit-hooks/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/habit-hooks/habit-hooks/blob/main/CHANGELOG.md
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: ai-coding-agents,ci,code-quality,code-review,code-smells,coding-agents,developer-tools,linter,refactoring,rubocop,ruby,static-analysis,technical-debt
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
20
|
+
Classifier: Topic :: Software Development :: Testing
|
|
21
|
+
Classifier: Topic :: Utilities
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# habit-hooks-ruby
|
|
26
|
+
|
|
27
|
+
The Ruby Habit Hooks plugin: wraps [`rubocop`](https://rubocop.org/) for
|
|
28
|
+
code-smell detection. Instead of a bare offence, each finding is
|
|
29
|
+
coached by listing the smell, guide for fixing, then a list of file locations.
|
|
30
|
+
This causes the agent to focus on fixing the smell rather than the metric.
|
|
31
|
+
|
|
32
|
+
```text
|
|
33
|
+
── high-complexity (1 issue) ──
|
|
34
|
+
|
|
35
|
+
High cyclomatic complexity means one function makes too many decisions at once. The count is the symptom; tangled responsibilities are the cause.
|
|
36
|
+
|
|
37
|
+
**Untangle the decisions:**
|
|
38
|
+
1. Lift guards out first — turn precondition checks into early returns so the happy path stays flat. Much of the count is preconditions wrapped around the real work.
|
|
39
|
+
2. Change the shape of what remains: an `if`/`else` chain switching on one value is often a lookup table or polymorphism in disguise; a nested loop is often a filter/map pipeline.
|
|
40
|
+
3. If the branches are genuinely separate jobs, extract one function per branch, each named for the responsibility it handles.
|
|
41
|
+
|
|
42
|
+
Useful tip: describe each branch in one sentence. Two branches with the same sentence belong together; a branch you cannot name cleanly wants its own function.
|
|
43
|
+
|
|
44
|
+
**AVOID**: merging conditions with and/or, or rewriting branches as ternaries, just to lower the score — the decisions remain, only the counter moves. You are done when a first-time reader can hold the whole function in their head.
|
|
45
|
+
|
|
46
|
+
app/services/billing.rb:6
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
# Setup
|
|
50
|
+
|
|
51
|
+
The steps are:
|
|
52
|
+
|
|
53
|
+
1. Install the plugin.
|
|
54
|
+
2. Enable it in your `.habit-hooks/config.toml`.
|
|
55
|
+
3. Make sure you have rubocop installed.
|
|
56
|
+
4. Make sure your `.rubocop.yml` enables the cops you want to see.
|
|
57
|
+
|
|
58
|
+
## Install the plugin
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
uv tool install "habit-hooks[ruby]" # pip and pipx work too
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Or let setup do it: with habit-hooks already installed, `habit-hooks init` in your
|
|
65
|
+
project detects ruby, names this plugin in `.habit-hooks/config.toml`, and offers to
|
|
66
|
+
run the install for you — see the [Habit-Hooks README #install](https://github.com/habit-hooks/habit-hooks#install).
|
|
67
|
+
|
|
68
|
+
## Enable the plugin
|
|
69
|
+
|
|
70
|
+
Installing a plugin does not switch it on — it has to be named in
|
|
71
|
+
`plugins` before habit-hooks runs it.
|
|
72
|
+
|
|
73
|
+
```toml
|
|
74
|
+
# .habit-hooks/config.toml
|
|
75
|
+
plugins = ["ruby", "generic"]
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Keep `generic` in the list. Most of the smells this plugin reports are coached
|
|
79
|
+
by guides `generic` ships; without it they fall back to a short generic prompt.
|
|
80
|
+
|
|
81
|
+
## Sensors: install rubocop
|
|
82
|
+
|
|
83
|
+
The ruby plugin supports the rubocop sensor. It can use any rubocop on your `PATH`, but it is best to point it at the one your project uses.
|
|
84
|
+
|
|
85
|
+
Point it at your project's own rubocop:
|
|
86
|
+
|
|
87
|
+
**If your `Gemfile` pins rubocop, or your `.rubocop.yml` names an extension gem
|
|
88
|
+
(`rubocop-rails`, `rubocop-rspec`, `rubocop-performance`), generate binstubs:**
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
bundle binstubs rubocop
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Otherwise, if you have no rubocop yet, install one:
|
|
95
|
+
|
|
96
|
+
- `rubocop` — `gem install rubocop`
|
|
97
|
+
|
|
98
|
+
### How it finds rubocop
|
|
99
|
+
|
|
100
|
+
habit-hooks' rubocop detector searches your project's `bin/` ahead of the
|
|
101
|
+
machine's `PATH`, so `bin/rubocop` is what it will run — under your bundle,
|
|
102
|
+
with your extension gems loaded.
|
|
103
|
+
|
|
104
|
+
Without this, habit-hooks runs whatever `rubocop` your `PATH` answers with, and
|
|
105
|
+
a config naming cops that rubocop cannot load is a hard error rather than a
|
|
106
|
+
lint result:
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
Error: `Rails/*` has been extracted to the `rubocop-rails` gem.
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
habit-hooks reports that as a failed run.
|
|
113
|
+
|
|
114
|
+
### Configure your `.rubocop.yml`
|
|
115
|
+
|
|
116
|
+
The sensor runs `rubocop` and reads what comes back. RuboCop discovers your `.rubocop.yml`
|
|
117
|
+
exactly as it does when you run rubocop by hand. The `--force-exclusion` is the one flag
|
|
118
|
+
habit-hooks adds, so your `AllCops: Exclude:` keeps applying even though habit-hooks names files
|
|
119
|
+
explicitly.
|
|
120
|
+
|
|
121
|
+
Cops are mapped to canonical smells and are reported under it:
|
|
122
|
+
|
|
123
|
+
| Cop | Smell |
|
|
124
|
+
|-----|-------|
|
|
125
|
+
| `Metrics/ParameterLists` | `too-many-parameters` |
|
|
126
|
+
| `Metrics/MethodLength` | `oversized-function` |
|
|
127
|
+
| `Metrics/BlockLength` | `oversized-block` |
|
|
128
|
+
| `Metrics/CyclomaticComplexity` | `high-complexity` |
|
|
129
|
+
| `Metrics/PerceivedComplexity` | `high-complexity` |
|
|
130
|
+
| `Metrics/AbcSize` | `high-complexity` |
|
|
131
|
+
| `Metrics/BlockNesting` | `deep-nesting` |
|
|
132
|
+
| `Lint/UselessAssignment` | `unused-variable` |
|
|
133
|
+
| `Lint/SuppressedException` | `swallowed-exception` |
|
|
134
|
+
| `Lint/Syntax` | `parse-error` |
|
|
135
|
+
|
|
136
|
+
All other cops are **forwarded under their own name** by default.
|
|
137
|
+
Recommended: add coaches for cops as needed (see [Customization](#customization)).
|
|
138
|
+
Set the root `uncoached` key to `ignore` to drop these cops,
|
|
139
|
+
or `enforce` to fail the run on them
|
|
140
|
+
([config.md](https://github.com/habit-hooks/habit-hooks/blob/main/docs/config.md)).
|
|
141
|
+
|
|
142
|
+
#### A starting `.rubocop.yml`
|
|
143
|
+
|
|
144
|
+
If you have no config yet, this turns on the structural cops this plugin maps
|
|
145
|
+
and nothing else. It is a suggestion, not a default. Habit-hooks never writes
|
|
146
|
+
it for you and never passes it to rubocop.
|
|
147
|
+
|
|
148
|
+
```yaml
|
|
149
|
+
AllCops:
|
|
150
|
+
NewCops: enable
|
|
151
|
+
Exclude:
|
|
152
|
+
- 'db/schema.rb'
|
|
153
|
+
- 'vendor/**/*'
|
|
154
|
+
- 'tmp/**/*'
|
|
155
|
+
|
|
156
|
+
Metrics/ParameterLists:
|
|
157
|
+
Max: 4
|
|
158
|
+
Metrics/MethodLength:
|
|
159
|
+
Max: 20
|
|
160
|
+
Metrics/CyclomaticComplexity:
|
|
161
|
+
Max: 10
|
|
162
|
+
Metrics/BlockNesting:
|
|
163
|
+
Max: 3
|
|
164
|
+
Lint/UselessAssignment:
|
|
165
|
+
Enabled: true
|
|
166
|
+
Lint/SuppressedException:
|
|
167
|
+
Enabled: true
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
# Customization
|
|
171
|
+
|
|
172
|
+
Ruby Habit Hooks is customizable without modifying the installed package. Project
|
|
173
|
+
files under `.habit-hooks/ruby/` override the corresponding files shipped by the
|
|
174
|
+
Ruby plugin. Commit these overrides if they are intended to apply to the whole
|
|
175
|
+
project.
|
|
176
|
+
|
|
177
|
+
## Add project-specific coaching
|
|
178
|
+
|
|
179
|
+
Replace any Ruby guide by creating a file with the same name under
|
|
180
|
+
`.habit-hooks/ruby/guides/`. For example,
|
|
181
|
+
`.habit-hooks/ruby/guides/high-complexity.md` replaces the default coaching for
|
|
182
|
+
the `high-complexity` smell while leaving all other guides unchanged.
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
An unmapped cop forwarded under its own name can use a custom filename through the project
|
|
186
|
+
config:
|
|
187
|
+
|
|
188
|
+
```toml
|
|
189
|
+
[smells."Style/StringLiterals"]
|
|
190
|
+
guide = "style-string-literals.md"
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Then add a guide at `.habit-hooks/ruby/guides/style-string-literals.md`. For example:
|
|
194
|
+
|
|
195
|
+
```markdown
|
|
196
|
+
Use the quote style established by this project. preserve interpolation and readability.
|
|
197
|
+
|
|
198
|
+
{% for issue in issues -%}
|
|
199
|
+
{{ issue.details.file }}:{{ issue.details.line }}
|
|
200
|
+
{% endfor %}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Guides are Markdown Jinja templates. They can use `smell` and `language`, read
|
|
204
|
+
smell-level `details`, and loop over `issues` to show each offense. The loop in
|
|
205
|
+
the example above writes the file and line of each issue (required, otherwise the
|
|
206
|
+
agent will not know where the offense occurred).
|
|
207
|
+
|
|
208
|
+
## Add a cop-to-smell mapping
|
|
209
|
+
|
|
210
|
+
Prefer mapping a cop to an existing general smell when the guidance fits. This
|
|
211
|
+
lets the project benefit from shared coaching and keeps the smell vocabulary
|
|
212
|
+
small. If the cop represents a smell that should be useful beyond your project,
|
|
213
|
+
consider contributing the mapping, guide, and any needed vocabulary changes in
|
|
214
|
+
a pull request to the [Habit Hooks project](https://github.com/habit-hooks/habit-hooks).
|
|
215
|
+
|
|
216
|
+
The built-in cop mappings live in `sensors/rubocop_report.py`, in the
|
|
217
|
+
`COP_SMELLS` table.
|
|
218
|
+
|
|
219
|
+
To add a mapping for a project, copy that file to
|
|
220
|
+
`.habit-hooks/ruby/sensors/rubocop_report.py` and add an entry, for example:
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
COP_SMELLS = {
|
|
224
|
+
# existing mappings ...
|
|
225
|
+
"Style/StringLiterals": "project-style",
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Because the sensor helper imports this file from its own directory, also copy
|
|
230
|
+
`rubocop_sensor.py` and `rubocop.toml` to the same override directory (.habit-hooks/ruby/sensors).
|
|
231
|
+
Keep the recipe in the TOML file the same; its `${dir}` then points at the override and
|
|
232
|
+
loads your customized report module.
|
|
233
|
+
|
|
234
|
+
If the new mapping uses an existing smell, its existing guide and severity are
|
|
235
|
+
used. For a new smell, add a guide and configure its severity as needed:
|
|
236
|
+
|
|
237
|
+
```toml
|
|
238
|
+
[smells.project-style]
|
|
239
|
+
severity = "suggested"
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Then add `.habit-hooks/ruby/guides/project-style.md`.
|
|
243
|
+
|
|
244
|
+
## Add or replace a sensor
|
|
245
|
+
|
|
246
|
+
A sensor is a TOML recipe under `sensors/`. To replace the RuboCop sensor,
|
|
247
|
+
override `.habit-hooks/ruby/sensors/rubocop.toml`; to add a separate sensor,
|
|
248
|
+
create a new recipe such as `.habit-hooks/ruby/sensors/custom-check.toml` and
|
|
249
|
+
add its name to the Ruby plugin's `sensors` list in
|
|
250
|
+
`.habit-hooks/ruby/config.toml`:
|
|
251
|
+
|
|
252
|
+
```toml
|
|
253
|
+
sensors = ["rubocop", "custom-check"]
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
The plugin config override is a complete replacement, so copy the shipped
|
|
257
|
+
Ruby `config.toml` and preserve its `language`, `files`, `transformers`, and
|
|
258
|
+
`detectors` entries when adding a sensor. Declare every external command the
|
|
259
|
+
sensor uses in `detectors`, and use `${detector:<name>}` in its recipe when the
|
|
260
|
+
sensor invokes that command.
|
|
261
|
+
|
|
262
|
+
The sensor must print a JSON array of Habit Hooks findings. See the
|
|
263
|
+
[sensor interface](https://github.com/habit-hooks/habit-hooks/blob/main/docs/sensor-interface.spec.md)
|
|
264
|
+
for the finding shape and available recipe placeholders.
|
|
265
|
+
|
|
266
|
+
For example, a simple custom sensor returning a canned finding to demonstrate the customization.
|
|
267
|
+
|
|
268
|
+
- Create
|
|
269
|
+
`.habit-hooks/ruby/sensors/custom-check.toml`:
|
|
270
|
+
|
|
271
|
+
```toml
|
|
272
|
+
command = "${dir}/custom-check.sh"
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
- Then create the executable `.habit-hooks/ruby/sensors/custom-check.sh`:
|
|
276
|
+
|
|
277
|
+
```sh
|
|
278
|
+
#!/bin/sh
|
|
279
|
+
printf '%s\n' '[{"smell":"custom-check","details":{},"issues":[{"key":"app/models/example.rb","details":{"file":"app/models/example.rb","line":1,"message":"Canned custom-check result","source":"custom-check"}}]}]'
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
The sensor's issue paths should be relative to the project, and its
|
|
283
|
+
output must always be a JSON array of findings.
|
|
284
|
+
|
|
285
|
+
- Enable it by copying the entire `plugins/ruby/src/habit_hooks_ruby/config.toml` to `.habit-hooks/ruby/config.toml` and adding `"custom-check"` to the `sensors` list in the plugin config
|
|
286
|
+
override. for example:
|
|
287
|
+
```
|
|
288
|
+
sensors = ["rubocop", "custom-check"]
|
|
289
|
+
```
|