rubocop-legion 0.1.7 → 0.1.8
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.
- checksums.yaml +4 -4
- data/.rubocop.yml +9 -0
- data/CHANGELOG.md +8 -0
- data/CLAUDE.md +17 -75
- data/config/default.yml +27 -0
- data/lib/rubocop/cop/legion/extension/every_actor_requires_time.rb +5 -1
- data/lib/rubocop/cop/legion/framework/no_direct_dispatch.rb +109 -0
- data/lib/rubocop/cop/legion/framework/no_inline_setting_defaults.rb +110 -0
- data/lib/rubocop/cop/legion/framework/no_shape_duck_typing.rb +100 -0
- data/lib/rubocop/cop/legion/framework/no_underscore_prefixed_kwargs.rb +89 -0
- data/lib/rubocop/legion/version.rb +1 -1
- data/lib/rubocop-legion.rb +4 -0
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '0668cc51aab49685f7ecb368113a7103253ec4d3aca22de2970c88ff4ae351f3'
|
|
4
|
+
data.tar.gz: abc9644c2e563387778df1459aa7abd81f49e79c14e7cc13e038546ceab40a56
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 43d9efec4fb9f8fc5af8eced138ed626c29211017382b4c71b84f3191b2b5316624e64821e657cf4e8a61426f51404c147c32522d623c3cce3c50d51f9738afe
|
|
7
|
+
data.tar.gz: 17807228e3e91cd2e4ac0be778a469256adbe691e7b0f1b6399344cb6d1ecffe6e699a5db7ffcf274a42d1a0df531842b32e218f89230d1ae1ced828ef733fbd
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.8] - 2026-06-16
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- New cop `Legion/Framework/NoUnderscorePrefixedKwargs`: ban `_foo:` keyword arguments and `**_rest` splats; autocorrects to plain names / `**opts`
|
|
7
|
+
- New cop `Legion/Framework/NoInlineSettingDefaults`: flag `Legion::Settings[...] || <literal>` and shadow-default patterns (tunables belong in `settings.rb`)
|
|
8
|
+
- New cop `Legion/Framework/NoDirectDispatch`: ban `*_direct` methods on `Legion::LLM` modules (every pipeline exit must route through the governed pipeline)
|
|
9
|
+
- New cop `Legion/Framework/NoShapeDuckTyping` (disabled by default): flag `respond_to?` on canonical shape methods and string-key fallback access on canonical bodies
|
|
10
|
+
|
|
3
11
|
## [0.1.7] - 2026-03-29
|
|
4
12
|
|
|
5
13
|
### Added
|
data/CLAUDE.md
CHANGED
|
@@ -1,98 +1,52 @@
|
|
|
1
1
|
# rubocop-legion
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## What is This?
|
|
6
|
-
|
|
7
|
-
Custom RuboCop plugin gem for the LegionIO ecosystem. Provides 47 AST-based cops across 6 departments. Uses the new RuboCop Plugin API (1.72+, lint_roller-based) with auto-discovery via gemspec metadata.
|
|
8
|
-
|
|
9
|
-
**GitHub**: https://github.com/LegionIO/rubocop-legion
|
|
10
|
-
**RubyGems**: https://rubygems.org/gems/rubocop-legion
|
|
11
|
-
**License**: MIT
|
|
3
|
+
Custom RuboCop plugin gem for the LegionIO ecosystem. Provides 47 AST-based cops across 6 departments. Uses the RuboCop Plugin API (1.72+, lint_roller-based) with auto-discovery via gemspec metadata.
|
|
12
4
|
|
|
13
5
|
## Shared Config Profiles
|
|
14
6
|
|
|
15
|
-
The gem ships shared `.rubocop.yml` profiles so repos don't duplicate config:
|
|
16
|
-
|
|
17
7
|
- `config/base.yml` — all shared settings (AllCops, Layout, Metrics, Style, Naming, Performance, ThreadSafety)
|
|
18
|
-
- `config/lex.yml` — inherits base, adds plugins
|
|
19
|
-
- `config/core.yml` — inherits base, adds plugins
|
|
20
|
-
|
|
21
|
-
### Bundled Plugins
|
|
8
|
+
- `config/lex.yml` — inherits base, adds plugins + `ParameterLists Max: 8`
|
|
9
|
+
- `config/core.yml` — inherits base, adds plugins + `ParameterLists Max: 10, CountKeywordArgs: false`
|
|
22
10
|
|
|
23
|
-
Both profiles load `rubocop-performance` (52 cops) and `rubocop-thread_safety` (6 cops) as runtime dependencies.
|
|
11
|
+
Both profiles load `rubocop-performance` (52 cops) and `rubocop-thread_safety` (6 cops) as runtime dependencies.
|
|
24
12
|
|
|
25
13
|
**LEX repos**: `inherit_gem: { rubocop-legion: config/lex.yml }`
|
|
26
14
|
**Core repos**: `inherit_gem: { rubocop-legion: config/core.yml }`
|
|
27
15
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
## Cop Scoping
|
|
31
|
-
|
|
32
|
-
Cops are scoped by gem type — no per-repo configuration needed:
|
|
33
|
-
|
|
34
|
-
- **Universal** (9 cops): Fire on all LegionIO gems
|
|
35
|
-
- **Library-specific** (6 cops): Fire on all gems but only trigger when using Sequel, Sinatra, Thor, Faraday, or cache
|
|
36
|
-
- **LEX-only** (31 cops): Scoped to `lib/legion/extensions/**/*.rb` via Include directive — never fire on core `legion-*` libraries
|
|
37
|
-
|
|
38
|
-
## Departments and Cops
|
|
39
|
-
|
|
40
|
-
### Universal — 10 cops
|
|
16
|
+
## Cop Departments
|
|
41
17
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
18
|
+
| Department | Count | Scope |
|
|
19
|
+
|------------|-------|-------|
|
|
20
|
+
| ConstantSafety | 4 | Universal |
|
|
21
|
+
| RescueLogging | 3 | Universal |
|
|
22
|
+
| Singleton | 1 | Universal |
|
|
23
|
+
| Framework | 8 | Universal + Library-specific |
|
|
24
|
+
| HelperMigration | 13 | LEX-only |
|
|
25
|
+
| Extension | 18 | LEX-only |
|
|
47
26
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
- `EagerSequelModel` — `Sequel::Model(:table)` at require time
|
|
51
|
-
- `SinatraHostAuth` — Sinatra 4.0+ `set :host_authorization`
|
|
52
|
-
- `ThorReservedRun` — Thor 1.5+ reserves `run`
|
|
53
|
-
- `FaradayXmlMiddleware` — Faraday 2.0+ removed `:xml`
|
|
54
|
-
- `CacheTimeCoercion` — Time→String after cache round-trip
|
|
55
|
-
- `ApiStringKeys` — `Legion::JSON.load` returns symbol keys (scoped to `lib/legion/extensions/**/*.rb`)
|
|
56
|
-
|
|
57
|
-
### LEX-Only — 29 cops
|
|
58
|
-
|
|
59
|
-
- **HelperMigration** (13): `DirectLogging`, `OldLoggingMethods`, `DirectJson`, `DirectCache`, `DirectLocalCache`, `DirectCrypt`, `DirectData`, `DirectLlm`, `DirectTransport`, `DirectKnowledge`, `RequireDefinedGuard` (all auto-fix) — use per-extension helpers, not global singletons. `LoggingGuard` (no auto-fix) — remove unnecessary `respond_to?(:log_warn)` / `defined?(Legion::Logging)` guards. `DefinedTransportGuard` (no auto-fix) — use `transport_connected?` instead of `defined?(Legion::Transport)`.
|
|
60
|
-
- **Extension** (18): `ActorSingularModule` (auto-fix), `RunnerPluralModule` (auto-fix), `CoreExtendGuard` (auto-fix), `RunnerMustBeModule`, `RunnerIncludeHelpers`, `ActorInheritance`, `EveryActorRequiresTime`, `SelfContainedActorRunnerClass`, `HookMissingRunnerClass`, `AbsorberMissingPattern`, `AbsorberMissingAbsorbMethod`, `DefinitionCallMismatched`, `RunnerReturnHash`, `SettingsKeyMethod` (auto-fix), `SettingsBracketMultiArg` (auto-fix), `LlmAskKwargs`, `ActorEnabledSideEffects`, `DataRequiredWithoutMigrations`.
|
|
27
|
+
Cops are scoped by gem type — no per-repo configuration needed. LEX-only cops fire on `lib/legion/extensions/**/*.rb` via Include directive.
|
|
61
28
|
|
|
62
29
|
## Architecture
|
|
63
30
|
|
|
64
31
|
```
|
|
65
32
|
rubocop-legion/
|
|
66
33
|
├── lib/
|
|
67
|
-
│ ├── rubocop-legion.rb # Entry point
|
|
34
|
+
│ ├── rubocop-legion.rb # Entry point
|
|
68
35
|
│ └── rubocop/
|
|
69
|
-
│ ├── legion.rb # Namespace declarations
|
|
70
36
|
│ ├── legion/
|
|
71
|
-
│ │ ├── version.rb
|
|
72
37
|
│ │ └── plugin.rb # LintRoller::Plugin (auto-discovery)
|
|
73
38
|
│ └── cop/legion/
|
|
74
39
|
│ ├── helper_migration/ # 13 cops (lex-only)
|
|
75
40
|
│ ├── constant_safety/ # 4 cops (universal)
|
|
76
41
|
│ ├── singleton/ # 1 cop (universal)
|
|
77
42
|
│ ├── rescue_logging/ # 3 cops (universal)
|
|
78
|
-
│ ├── framework/ #
|
|
79
|
-
│ └── extension/ #
|
|
43
|
+
│ ├── framework/ # 8 cops (universal + library-specific)
|
|
44
|
+
│ └── extension/ # 18 cops (lex-only)
|
|
80
45
|
├── config/
|
|
81
46
|
│ └── default.yml # All cop defaults, Include/Exclude scoping
|
|
82
47
|
└── spec/ # mirrors lib/ structure
|
|
83
48
|
```
|
|
84
49
|
|
|
85
|
-
## Key Implementation Details
|
|
86
|
-
|
|
87
|
-
- Plugin entry point: `RuboCop::Legion::Plugin` (LintRoller-based, registered via gemspec metadata `default_lint_roller_plugin`)
|
|
88
|
-
- All cops inherit from `RuboCop::Cop::Base`
|
|
89
|
-
- Auto-correctable cops use `extend AutoCorrector`
|
|
90
|
-
- AST matching via `def_node_matcher` and `def_node_search`
|
|
91
|
-
- Specs use `RuboCop::RSpec::Support` with `:config` shared context and `expect_offense`/`expect_correction`
|
|
92
|
-
- `BareRescue` and `NoCapture` skip rescue modifiers (inline `rescue`) to avoid syntax corruption
|
|
93
|
-
- `NoCapture` has no auto-correct to prevent correction loop with `Lint/UselessAssignment`
|
|
94
|
-
- `SilentCapture` skips `_`-prefixed variables (Ruby unused convention)
|
|
95
|
-
|
|
96
50
|
## Development
|
|
97
51
|
|
|
98
52
|
```bash
|
|
@@ -100,15 +54,3 @@ bundle install
|
|
|
100
54
|
bundle exec rspec # 350 specs
|
|
101
55
|
bundle exec rubocop # Self-linting
|
|
102
56
|
```
|
|
103
|
-
|
|
104
|
-
## Common Per-Repo Overrides
|
|
105
|
-
|
|
106
|
-
```yaml
|
|
107
|
-
# Repos using Faraday JSON middleware (string keys, not Legion::JSON symbol keys)
|
|
108
|
-
Legion/Framework/ApiStringKeys:
|
|
109
|
-
Enabled: false
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
---
|
|
113
|
-
|
|
114
|
-
**Maintained By**: Matthew Iverson (@Esity)
|
data/config/default.yml
CHANGED
|
@@ -336,3 +336,30 @@ Legion/Extension/DefinitionCallMismatched:
|
|
|
336
336
|
Enabled: true
|
|
337
337
|
Severity: error
|
|
338
338
|
VersionAdded: '0.1.4'
|
|
339
|
+
|
|
340
|
+
# Legion/Framework — N×N routing guard cops (Phase 6 enforcement)
|
|
341
|
+
|
|
342
|
+
Legion/Framework/NoUnderscorePrefixedKwargs:
|
|
343
|
+
Description: 'Ban underscore-prefixed kwargs and `**_rest` splats. Use plain kwarg, defaulted kwarg, or `**opts`.'
|
|
344
|
+
Enabled: true
|
|
345
|
+
Severity: warning
|
|
346
|
+
VersionAdded: '0.1.8'
|
|
347
|
+
|
|
348
|
+
Legion/Framework/NoInlineSettingDefaults:
|
|
349
|
+
Description: 'Flag `Legion::Settings[...] || <literal>` and shadow-default patterns. Defaults belong in `settings.rb`.'
|
|
350
|
+
Enabled: true
|
|
351
|
+
Severity: warning
|
|
352
|
+
VersionAdded: '0.1.8'
|
|
353
|
+
|
|
354
|
+
Legion/Framework/NoDirectDispatch:
|
|
355
|
+
Description: 'Ban `_direct` methods on `Legion::LLM` modules. All inference routes through the governed pipeline.'
|
|
356
|
+
Enabled: true
|
|
357
|
+
Severity: error
|
|
358
|
+
VersionAdded: '0.1.8'
|
|
359
|
+
|
|
360
|
+
Legion/Framework/NoShapeDuckTyping:
|
|
361
|
+
Description: 'Flag `respond_to?` on canonical shape methods and string-key fallback access. Disabled by default.'
|
|
362
|
+
Enabled: false
|
|
363
|
+
Severity: convention
|
|
364
|
+
VersionAdded: '0.1.8'
|
|
365
|
+
Safe: false
|
|
@@ -59,7 +59,11 @@ module RuboCop
|
|
|
59
59
|
|
|
60
60
|
node.body.each_node(:send).any? do |send_node|
|
|
61
61
|
send_node.method_name == :time && send_node.receiver.nil?
|
|
62
|
-
end
|
|
62
|
+
end || defines_time_method?(node)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def defines_time_method?(node)
|
|
66
|
+
node.body.each_node(:def).any? { |def_node| def_node.method_name == :time }
|
|
63
67
|
end
|
|
64
68
|
end
|
|
65
69
|
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Legion
|
|
6
|
+
module Framework
|
|
7
|
+
# Bans defining methods matching `/_direct\z/` on `Legion::LLM` modules.
|
|
8
|
+
# Per G16, all pipeline bypasses (`chat_direct`, `embed_direct`,
|
|
9
|
+
# `structured_direct`) are removed — internal callers route through the
|
|
10
|
+
# governed pipeline with an appropriate profile.
|
|
11
|
+
#
|
|
12
|
+
# No auto-correct is provided because the fix requires routing through
|
|
13
|
+
# the inference pipeline.
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# # bad
|
|
17
|
+
# module Legion::LLM
|
|
18
|
+
# def chat_direct(message:)
|
|
19
|
+
# # bypasses metering, audit, ledger
|
|
20
|
+
# end
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# # bad
|
|
24
|
+
# module Legion::LLM
|
|
25
|
+
# def self.embed_direct(text:)
|
|
26
|
+
# end
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# # good
|
|
30
|
+
# module Legion::LLM
|
|
31
|
+
# def chat(message:)
|
|
32
|
+
# # routes through pipeline
|
|
33
|
+
# end
|
|
34
|
+
# end
|
|
35
|
+
class NoDirectDispatch < Base
|
|
36
|
+
MSG = 'Method `%<name>s` matches `/_direct\\z/` on a `Legion::LLM` module. ' \
|
|
37
|
+
'Pipeline bypasses are not allowed — route through the governed pipeline.'
|
|
38
|
+
|
|
39
|
+
DIRECT_PATTERN = /_direct\z/
|
|
40
|
+
|
|
41
|
+
def on_def(node)
|
|
42
|
+
return unless node.method_name.to_s.match?(DIRECT_PATTERN)
|
|
43
|
+
return unless inside_legion_llm?(node)
|
|
44
|
+
|
|
45
|
+
add_offense(node.loc.name, message: format(MSG, name: node.method_name))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def on_defs(node)
|
|
49
|
+
return unless node.method_name.to_s.match?(DIRECT_PATTERN)
|
|
50
|
+
return unless inside_legion_llm?(node)
|
|
51
|
+
|
|
52
|
+
add_offense(node.loc.name, message: format(MSG, name: node.method_name))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def inside_legion_llm?(node)
|
|
58
|
+
node.each_ancestor(:module, :class).any? do |ancestor|
|
|
59
|
+
legion_llm_module?(ancestor)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def legion_llm_module?(mod_node)
|
|
64
|
+
return false unless mod_node.module_type?
|
|
65
|
+
|
|
66
|
+
# Check compact form: Legion::LLM or Legion::LLM::Something
|
|
67
|
+
ident = mod_node.identifier
|
|
68
|
+
return true if compact_legion_llm?(ident)
|
|
69
|
+
|
|
70
|
+
# Check nested form: module Legion; module LLM; ...
|
|
71
|
+
# The identifier is just `LLM` but parent is `module Legion`
|
|
72
|
+
return true if nested_legion_llm?(mod_node)
|
|
73
|
+
|
|
74
|
+
false
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def compact_legion_llm?(ident)
|
|
78
|
+
parts = const_parts(ident)
|
|
79
|
+
parts.length >= 2 && parts[0] == :Legion && parts[1] == :LLM
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def nested_legion_llm?(mod_node)
|
|
83
|
+
return false unless mod_node.identifier.const_type?
|
|
84
|
+
return false unless mod_node.identifier.children.last == :LLM
|
|
85
|
+
|
|
86
|
+
# Check if parent module is `Legion`
|
|
87
|
+
parent = mod_node.parent
|
|
88
|
+
return false unless parent&.module_type?
|
|
89
|
+
return false unless parent&.identifier&.const_type?
|
|
90
|
+
|
|
91
|
+
parent.identifier.children.last == :Legion
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def const_parts(node)
|
|
95
|
+
return [] unless node&.const_type?
|
|
96
|
+
|
|
97
|
+
child = node.children.first
|
|
98
|
+
name = node.children.last
|
|
99
|
+
if child&.const_type?
|
|
100
|
+
const_parts(child) + [name]
|
|
101
|
+
else
|
|
102
|
+
[name]
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Legion
|
|
6
|
+
module Framework
|
|
7
|
+
# Detects `Legion::Settings[...]` reads combined with inline literal
|
|
8
|
+
# fallbacks (`|| <literal>`) or shadow-default patterns
|
|
9
|
+
# (`x = N unless x.positive?`). Per G13, all defaults belong in
|
|
10
|
+
# `settings.rb` — inline fallbacks are dead code after
|
|
11
|
+
# `register_defaults!` merge.
|
|
12
|
+
#
|
|
13
|
+
# No auto-correct is provided because the fix requires moving the
|
|
14
|
+
# literal into the appropriate `*_defaults` group in settings.rb.
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# # bad
|
|
18
|
+
# max_retries = Legion::Settings[:llm][:max_retries] || 3
|
|
19
|
+
# timeout = Legion::Settings[:timeout] || 30
|
|
20
|
+
#
|
|
21
|
+
# # bad (shadow default)
|
|
22
|
+
# max_retries = Legion::Settings[:llm][:max_retries]
|
|
23
|
+
# max_retries = 200 unless max_retries.positive?
|
|
24
|
+
#
|
|
25
|
+
# # good
|
|
26
|
+
# max_retries = Legion::Settings[:llm][:max_retries]
|
|
27
|
+
class NoInlineSettingDefaults < Base
|
|
28
|
+
MSG_OR = 'Inline default `%<default>s` after `Legion::Settings` read. ' \
|
|
29
|
+
'Move the default into `settings.rb` instead.'
|
|
30
|
+
MSG_SHADOW = 'Shadow-default pattern after `Legion::Settings` read. ' \
|
|
31
|
+
'Move the default into `settings.rb` instead.'
|
|
32
|
+
|
|
33
|
+
def on_or(node)
|
|
34
|
+
left = node.children.first
|
|
35
|
+
right = node.children.last
|
|
36
|
+
return unless settings_read?(left)
|
|
37
|
+
return unless right && literal?(right)
|
|
38
|
+
|
|
39
|
+
add_offense(node, message: format(MSG_OR, default: right.source))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def on_if(node)
|
|
43
|
+
cond = node.condition
|
|
44
|
+
return unless cond&.send_type?
|
|
45
|
+
|
|
46
|
+
receiver = cond.receiver
|
|
47
|
+
return unless receiver&.lvar_type?
|
|
48
|
+
|
|
49
|
+
method_name = cond.method_name
|
|
50
|
+
return unless %i[positive? nil? zero? empty? blank?].include?(method_name)
|
|
51
|
+
|
|
52
|
+
var_name = receiver.children.first
|
|
53
|
+
return unless preceded_by_settings_read?(node, var_name)
|
|
54
|
+
|
|
55
|
+
add_offense(node, message: MSG_SHADOW)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def settings_read?(node)
|
|
61
|
+
return false unless node&.send_type?
|
|
62
|
+
return true if direct_settings_access?(node)
|
|
63
|
+
return true if nested_settings_access?(node)
|
|
64
|
+
|
|
65
|
+
false
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def direct_settings_access?(node)
|
|
69
|
+
node.method_name == :[] &&
|
|
70
|
+
node.receiver&.const_type? &&
|
|
71
|
+
node.receiver.children.last == :Settings &&
|
|
72
|
+
legion_const?(node.receiver.children.first)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def nested_settings_access?(node)
|
|
76
|
+
node.method_name == :[] &&
|
|
77
|
+
node.receiver&.send_type? &&
|
|
78
|
+
node.receiver.method_name == :[] &&
|
|
79
|
+
direct_settings_access?(node.receiver)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def legion_const?(node)
|
|
83
|
+
return true if node.nil?
|
|
84
|
+
return false unless node.const_type?
|
|
85
|
+
|
|
86
|
+
node.children.last == :Legion
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def literal?(node)
|
|
90
|
+
%i[str int float sym].include?(node.type)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def preceded_by_settings_read?(if_node, var_name)
|
|
94
|
+
parent = if_node.parent
|
|
95
|
+
return false unless parent&.begin_type?
|
|
96
|
+
|
|
97
|
+
idx = parent.children.index(if_node)
|
|
98
|
+
return false unless idx&.positive?
|
|
99
|
+
|
|
100
|
+
prev = parent.children[idx - 1]
|
|
101
|
+
return false unless prev&.lvasgn_type?
|
|
102
|
+
return false unless prev.children.first == var_name
|
|
103
|
+
|
|
104
|
+
settings_read?(prev.children.last)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Legion
|
|
6
|
+
module Framework
|
|
7
|
+
# Flags `respond_to?` checks for canonical response shape methods
|
|
8
|
+
# (`thinking`, `tool_calls`, `content`, `stop_reason`) and string-key
|
|
9
|
+
# fallback access (`x[:k] || x['k']`). Per R10, downstream consumers
|
|
10
|
+
# of canonical types should rely on the struct interface, not duck-type.
|
|
11
|
+
#
|
|
12
|
+
# Ships **disabled by default**. Enable per-repo or per-directory once
|
|
13
|
+
# the canonical migration is complete.
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# # bad
|
|
17
|
+
# response.respond_to?(:thinking)
|
|
18
|
+
# response.respond_to?(:tool_calls)
|
|
19
|
+
# tc[:name] || tc['name']
|
|
20
|
+
#
|
|
21
|
+
# # good
|
|
22
|
+
# response.thinking
|
|
23
|
+
# response.tool_calls
|
|
24
|
+
# tc[:name]
|
|
25
|
+
class NoShapeDuckTyping < Base
|
|
26
|
+
MSG_RESPOND = 'Use canonical struct access instead of `respond_to?(:%<method>s)`. ' \
|
|
27
|
+
'Downstream of translators, the shape is guaranteed.'
|
|
28
|
+
MSG_FALLBACK = 'String-key fallback `%<key>s` suggests duck-typed access. ' \
|
|
29
|
+
'Use canonical struct access or symbol keys consistently.'
|
|
30
|
+
|
|
31
|
+
CANONICAL_METHODS = %w[thinking tool_calls content stop_reason].freeze
|
|
32
|
+
|
|
33
|
+
def on_send(node)
|
|
34
|
+
check_respond_to(node)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def on_or(node)
|
|
38
|
+
check_string_key_fallback(node)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def check_respond_to(node)
|
|
44
|
+
return unless node.method_name == :respond_to?
|
|
45
|
+
return unless node.arguments.length == 1
|
|
46
|
+
|
|
47
|
+
arg = node.arguments.first
|
|
48
|
+
method_name = case arg.type
|
|
49
|
+
when :sym then arg.value.to_s
|
|
50
|
+
when :str then arg.value
|
|
51
|
+
else return
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
return unless CANONICAL_METHODS.include?(method_name)
|
|
55
|
+
|
|
56
|
+
add_offense(node, message: format(MSG_RESPOND, method: method_name))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def check_string_key_fallback(node)
|
|
60
|
+
left = node.children.first
|
|
61
|
+
right = node.children.last
|
|
62
|
+
return unless left&.send_type? && right&.send_type?
|
|
63
|
+
return unless left.method_name == :[] && right.method_name == :[]
|
|
64
|
+
|
|
65
|
+
# Both must access the same receiver
|
|
66
|
+
left_recv = left.receiver
|
|
67
|
+
right_recv = right.receiver
|
|
68
|
+
return unless left_recv && right_recv
|
|
69
|
+
return unless same_receiver?(left_recv, right_recv)
|
|
70
|
+
|
|
71
|
+
# One must be sym, the other str
|
|
72
|
+
left_key = left.arguments.first
|
|
73
|
+
right_key = right.arguments.first
|
|
74
|
+
return unless left_key && right_key
|
|
75
|
+
|
|
76
|
+
has_sym = left_key.sym_type? || right_key.sym_type?
|
|
77
|
+
has_str = left_key.str_type? || right_key.str_type?
|
|
78
|
+
return unless has_sym && has_str
|
|
79
|
+
|
|
80
|
+
# Extract the key name from the symbol side
|
|
81
|
+
key_node = left_key.sym_type? ? left_key : right_key
|
|
82
|
+
return unless key_node
|
|
83
|
+
|
|
84
|
+
add_offense(node, message: format(MSG_FALLBACK, key: key_node.value))
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def same_receiver?(left, right)
|
|
88
|
+
# Both lvars with same name
|
|
89
|
+
return true if left.lvar_type? && right.lvar_type? && left.children.first == right.children.first
|
|
90
|
+
|
|
91
|
+
# Both sends with same method name and no receiver (bare identifier)
|
|
92
|
+
left.send_type? && right.send_type? &&
|
|
93
|
+
left.receiver.nil? && right.receiver.nil? &&
|
|
94
|
+
left.method_name == right.method_name
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Legion
|
|
6
|
+
module Framework
|
|
7
|
+
# Bans underscore-prefixed keyword arguments and `**_rest`/`**_` splat
|
|
8
|
+
# parameters in method definitions. The N×N routing design requires
|
|
9
|
+
# explicit kwarg signatures: required → plain kwarg, optional → defaulted
|
|
10
|
+
# kwarg, passthrough → `**opts` at the end.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# # bad
|
|
14
|
+
# def foo(_bar:); end
|
|
15
|
+
# def foo(**_rest); end
|
|
16
|
+
# def foo(**_); end
|
|
17
|
+
#
|
|
18
|
+
# # good
|
|
19
|
+
# def foo(bar:); end
|
|
20
|
+
# def foo(bar: nil); end
|
|
21
|
+
# def foo(**opts); end
|
|
22
|
+
class NoUnderscorePrefixedKwargs < Base
|
|
23
|
+
extend AutoCorrector
|
|
24
|
+
|
|
25
|
+
MSG_KWARG = 'Underscore-prefixed kwarg `%<name>s` is not allowed. ' \
|
|
26
|
+
'Use plain kwarg (required), defaulted kwarg (optional), ' \
|
|
27
|
+
'or `**opts` for passthrough.'
|
|
28
|
+
MSG_SPLAT = 'Underscore-prefixed kwarg splat `%<name>s` is not allowed. ' \
|
|
29
|
+
'Use `**opts` for passthrough.'
|
|
30
|
+
|
|
31
|
+
def on_def(node)
|
|
32
|
+
check_params(node)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def on_defs(node)
|
|
36
|
+
check_params(node)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def check_params(method_node)
|
|
42
|
+
args = method_node.arguments
|
|
43
|
+
return unless args
|
|
44
|
+
|
|
45
|
+
args.each_child_node do |arg|
|
|
46
|
+
case arg.type
|
|
47
|
+
when :kwarg, :kwoptarg
|
|
48
|
+
check_kwarg(arg)
|
|
49
|
+
when :kwrestarg
|
|
50
|
+
check_kwrestarg(arg)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def check_kwarg(arg)
|
|
56
|
+
name = arg.children.first
|
|
57
|
+
return unless name.is_a?(Symbol)
|
|
58
|
+
return unless name.to_s.start_with?('_')
|
|
59
|
+
|
|
60
|
+
add_offense(arg, message: format(MSG_KWARG, name: name)) do |corrector|
|
|
61
|
+
corrected = name.to_s.sub(/\A_+/, '')
|
|
62
|
+
corrected = 'arg' if corrected.empty?
|
|
63
|
+
corrector.replace(arg.source_range, kwarg_replacement(corrected, arg))
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def check_kwrestarg(arg)
|
|
68
|
+
name = arg.children.first
|
|
69
|
+
return unless name.is_a?(Symbol)
|
|
70
|
+
return unless name.to_s.start_with?('_')
|
|
71
|
+
|
|
72
|
+
add_offense(arg, message: format(MSG_SPLAT, name: name)) do |corrector|
|
|
73
|
+
corrector.replace(arg.source_range, '**opts')
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def kwarg_replacement(name, arg)
|
|
78
|
+
if arg.kwoptarg_type?
|
|
79
|
+
default = arg.children.last
|
|
80
|
+
"#{name}: #{default.source}"
|
|
81
|
+
else
|
|
82
|
+
"#{name}:"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
data/lib/rubocop-legion.rb
CHANGED
|
@@ -44,6 +44,10 @@ require 'rubocop/cop/legion/framework/module_function_private'
|
|
|
44
44
|
require 'rubocop/cop/legion/framework/cache_time_coercion'
|
|
45
45
|
require 'rubocop/cop/legion/framework/api_string_keys'
|
|
46
46
|
require 'rubocop/cop/legion/framework/mutex_nested_sync'
|
|
47
|
+
require 'rubocop/cop/legion/framework/no_underscore_prefixed_kwargs'
|
|
48
|
+
require 'rubocop/cop/legion/framework/no_inline_setting_defaults'
|
|
49
|
+
require 'rubocop/cop/legion/framework/no_direct_dispatch'
|
|
50
|
+
require 'rubocop/cop/legion/framework/no_shape_duck_typing'
|
|
47
51
|
|
|
48
52
|
# Legion/Extension
|
|
49
53
|
require 'rubocop/cop/legion/extension/actor_singular_module'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rubocop-legion
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -144,6 +144,10 @@ files:
|
|
|
144
144
|
- lib/rubocop/cop/legion/framework/faraday_xml_middleware.rb
|
|
145
145
|
- lib/rubocop/cop/legion/framework/module_function_private.rb
|
|
146
146
|
- lib/rubocop/cop/legion/framework/mutex_nested_sync.rb
|
|
147
|
+
- lib/rubocop/cop/legion/framework/no_direct_dispatch.rb
|
|
148
|
+
- lib/rubocop/cop/legion/framework/no_inline_setting_defaults.rb
|
|
149
|
+
- lib/rubocop/cop/legion/framework/no_shape_duck_typing.rb
|
|
150
|
+
- lib/rubocop/cop/legion/framework/no_underscore_prefixed_kwargs.rb
|
|
147
151
|
- lib/rubocop/cop/legion/framework/sinatra_host_auth.rb
|
|
148
152
|
- lib/rubocop/cop/legion/framework/thor_reserved_run.rb
|
|
149
153
|
- lib/rubocop/cop/legion/helper_migration/defined_transport_guard.rb
|
|
@@ -191,7 +195,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
191
195
|
- !ruby/object:Gem::Version
|
|
192
196
|
version: '0'
|
|
193
197
|
requirements: []
|
|
194
|
-
rubygems_version:
|
|
198
|
+
rubygems_version: 3.6.9
|
|
195
199
|
specification_version: 4
|
|
196
200
|
summary: LegionIO code quality cops for RuboCop
|
|
197
201
|
test_files: []
|