rubycli 0.1.5 → 0.1.6
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/CHANGELOG.md +5 -0
- data/README.ja.md +5 -3
- data/README.md +5 -3
- data/lib/rubycli/documentation/metadata_parser.rb +217 -6
- data/lib/rubycli/version.rb +1 -1
- data/lib/rubycli.rb +11 -6
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 521eac399843d8fe002c1017b24b0cf72644f179460d3a599a1b9bf9d0949d7e
|
|
4
|
+
data.tar.gz: d245d65f31c08f15c0f9bad5d4e13e3d7cb1785936772ab6a83887cd4fdf4564
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 286af4633230ddd40c39e40c2ec92f810bd47cc533c1d578dc42178179552518bfc057f992beef340cc4a34592c603d0ddc6d10eef1df928ce83be383cd9857c
|
|
7
|
+
data.tar.gz: 82e587aac72882cb1b3e317d20d2dc37c54ed8e927fc39ffa9d28dd871d77b05c6d2fc64ad13dc5e3fe7c903b6f5312dfa0b34e9e46391e66788435286c909e9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.6] - 2025-11-11
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- `rubycli --check` now reports unknown type tokens and enumerated allowed values (with DidYouMean suggestions) instead of silently treating them as strings, while `--strict` continues to enforce the surviving annotations at runtime.
|
|
7
|
+
|
|
3
8
|
## [0.1.5] - 2025-11-10
|
|
4
9
|
|
|
5
10
|
### Added
|
data/README.ja.md
CHANGED
|
@@ -156,13 +156,13 @@ Rubycli は「ファイル名を CamelCase にした定数」を公開対象だ
|
|
|
156
156
|
|
|
157
157
|
大規模なコードベースでも安全側を保ちながら、どうしても自動選択したいときだけ 1 フラグで切り替えられます。
|
|
158
158
|
|
|
159
|
-
> **インスタンスメソッド専用のクラスについて** – 公開メソッドがインスタンス側(`def greet` など)にしか無い場合は、`--new` を付けて事前にインスタンス化しないと CLI から呼び出せません。クラスメソッドを 1 つ用意するか、`--new`
|
|
159
|
+
> **インスタンスメソッド専用のクラスについて** – 公開メソッドがインスタンス側(`def greet` など)にしか無い場合は、`--new` を付けて事前にインスタンス化しないと CLI から呼び出せません。クラスメソッドを 1 つ用意するか、`--new` を明示して実行してください。`--new` を付ければ `rubycli --help` でもインスタンスメソッドが一覧に現れ、`rubycli --check --new` でコメントの lint も実行できます。
|
|
160
160
|
|
|
161
161
|
## 開発方針
|
|
162
162
|
|
|
163
163
|
- **便利さが最優先** – 既存の Ruby スクリプトを最小の手間で CLI 化できることを目的にしており、Python Fire の完全移植は目指していません。
|
|
164
164
|
- **インスパイアであってポートではない** – アイデアの出自は Fire ですが、同等機能を揃える予定は基本的にありません。Fire 由来の未実装機能は仕様です。
|
|
165
|
-
- **メソッド定義が土台、コメントが挙動を補強** – 公開メソッドのシグネチャが CLI に露出する範囲と必須/任意を決めますが、コメントに `TAG...` や `[Integer]` を書くと同じ引数でも配列化や型変換が行われます。さらに Rubycli は `--names='["Alice","Bob"]'` のような JSON/YAML らしい入力を自動的に安全なリテラルとして評価します。`rubycli --check パス/対象.rb`
|
|
165
|
+
- **メソッド定義が土台、コメントが挙動を補強** – 公開メソッドのシグネチャが CLI に露出する範囲と必須/任意を決めますが、コメントに `TAG...` や `[Integer]` を書くと同じ引数でも配列化や型変換が行われます。さらに Rubycli は `--names='["Alice","Bob"]'` のような JSON/YAML らしい入力を自動的に安全なリテラルとして評価します。`rubycli --check パス/対象.rb` でコメントと実装のズレ(未定義の型ラベルや列挙値の誤記を含む)を DidYouMean の候補付きで検査し、通常実行時に `--strict` を付ければドキュメント通りでない入力をその場でエラーにできます。
|
|
166
166
|
- **軽量メンテナンス** – 実装の多くは AI 支援で作られており、深い Ruby メタプログラミングを伴う大規模拡張は想定外です。Fire 互換を求める PR は事前相談をお願いします。
|
|
167
167
|
|
|
168
168
|
## 特徴
|
|
@@ -173,11 +173,13 @@ Rubycli は「ファイル名を CamelCase にした定数」を公開対象だ
|
|
|
173
173
|
- `--pre-script`(エイリアス: `--init`)で任意の Ruby コードを評価し、その結果オブジェクトを公開
|
|
174
174
|
- `--check` でコメント整合性を lint、`--strict` で入力値をドキュメント通りに強制する二段構えのガード
|
|
175
175
|
|
|
176
|
+
> 補足: `--strict` はコメントに書かれた型/許可値をそのまま信頼して検証するため、コメントが誤記だと実行時には検出できません。CI では必ず `rubycli --check` を走らせ、`--strict` は「 lint を通過したドキュメントを本番で厳密に守る」用途に使ってください。
|
|
177
|
+
|
|
176
178
|
## Python Fire との違い
|
|
177
179
|
|
|
178
180
|
- **コメント対応のヘルプ生成**: コメントがあればヘルプに反映しつつ、最終的な判断は常にライブなメソッド定義に基づきます。
|
|
179
181
|
- **型に基づく解析**: `NAME [String]` や YARD タグから型を推論し、真偽値・配列・数値などを自動変換します。
|
|
180
|
-
- **厳密な整合性チェック**: `rubycli --check`
|
|
182
|
+
- **厳密な整合性チェック**: `rubycli --check` でコメントと実装のズレ(未定義の型ラベルや列挙値の誤記など)をコード実行前に検査し、通常実行時に `--strict` を付ければドキュメントで宣言した型・許可値以外の入力を拒否できます。
|
|
181
183
|
- **Ruby 向け拡張**: キーワード引数やブロック (`@yield*`) といった Ruby 固有の構文に合わせたパーサや `RUBYCLI_*` 環境変数を用意しています。
|
|
182
184
|
|
|
183
185
|
| 機能 | Python Fire | Rubycli |
|
data/README.md
CHANGED
|
@@ -163,13 +163,13 @@ Rubycli assumes that the file name (CamelCased) matches the class or module you
|
|
|
163
163
|
|
|
164
164
|
This keeps large projects safe by default but still provides a one-flag escape hatch when you prefer the fully automatic behaviour.
|
|
165
165
|
|
|
166
|
-
> **Instance-only classes** – If a class only defines public *instance* methods (for example, it exposes functionality via `def greet` on the instance), you must run Rubycli with `--new` so the class is instantiated before commands are resolved. Otherwise Rubycli cannot see any CLI-callable methods. Add at least one public class method when you do not want to rely on `--new`.
|
|
166
|
+
> **Instance-only classes** – If a class only defines public *instance* methods (for example, it exposes functionality via `def greet` on the instance), you must run Rubycli with `--new` so the class is instantiated before commands are resolved. Otherwise Rubycli cannot see any CLI-callable methods. Add at least one public class method when you do not want to rely on `--new`. Passing `--new` also makes those instance methods appear in `rubycli --help` output and allows `rubycli --check --new` to lint their documentation.
|
|
167
167
|
|
|
168
168
|
## Project Philosophy
|
|
169
169
|
|
|
170
170
|
- **Convenience first** – The goal is to wrap existing Ruby scripts in a CLI with almost no manual plumbing. Fidelity with Python Fire is not a requirement.
|
|
171
171
|
- **Inspired, not a port** – We borrow ideas from Python Fire, but we do not aim for feature parity. Missing Fire features are generally “by design.”
|
|
172
|
-
- **Method definitions first, comments augment behavior** – Public method signatures determine what gets exposed (and which arguments are required), while doc comments like `TAG...` or `[Integer]` can turn the very same CLI value into arrays, integers, booleans, etc. Rubycli also auto-parses inputs that look like JSON/YAML literals (for example `--names='["Alice","Bob"]'`) before enforcing the documented type. Run `rubycli --check path/to/script.rb` to lint documentation mismatches, and pass `--strict` during normal runs when you want invalid input to abort instead of merely warning.
|
|
172
|
+
- **Method definitions first, comments augment behavior** – Public method signatures determine what gets exposed (and which arguments are required), while doc comments like `TAG...` or `[Integer]` can turn the very same CLI value into arrays, integers, booleans, etc. Rubycli also auto-parses inputs that look like JSON/YAML literals (for example `--names='["Alice","Bob"]'`) before enforcing the documented type. Run `rubycli --check path/to/script.rb` to lint documentation mismatches—including undefined type labels or enumerated values, with DidYouMean suggestions for `Booalean`-style typos—and pass `--strict` during normal runs when you want invalid input to abort instead of merely warning.
|
|
173
173
|
- **Lightweight maintenance** – Much of the implementation was generated with AI assistance; contributions that diverge into deep Ruby metaprogramming are out of scope. Please discuss expectations before opening parity PRs.
|
|
174
174
|
|
|
175
175
|
## Features
|
|
@@ -180,11 +180,13 @@ This keeps large projects safe by default but still provides a one-flag escape h
|
|
|
180
180
|
- Optional pre-script hook (`--pre-script` / `--init`) to evaluate Ruby and expose the resulting object
|
|
181
181
|
- Dedicated CLI flags for quality gates: `--check` lints documentation/comments without running commands, and `--strict` treats documented types/choices as hard requirements
|
|
182
182
|
|
|
183
|
+
> Tip: `--strict` trusts whatever types/choices your comments spell out—if the annotations are misspelled, runtime enforcement has nothing reliable to compare against. Keep `rubycli --check` in CI so documentation typos are caught before production runs that rely on `--strict`.
|
|
184
|
+
|
|
183
185
|
## How it differs from Python Fire
|
|
184
186
|
|
|
185
187
|
- **Comment-aware help** – Rubycli leans on doc comments when present but still reflects the live method signature, keeping code as the ultimate authority.
|
|
186
188
|
- **Type-aware parsing** – Placeholder syntax (`NAME [String]`) and YARD tags let Rubycli coerce arguments to booleans, arrays, numerics, etc. without additional code.
|
|
187
|
-
- **Strict validation** – `rubycli --check` lint runs catch documentation drift
|
|
189
|
+
- **Strict validation** – `rubycli --check` lint runs catch documentation drift (including undefined type labels or enumerated values) without executing commands, while runtime `--strict` runs turn those documented types/choices into enforceable contracts.
|
|
188
190
|
- **Ruby-centric tooling** – Supports Ruby-specific conventions such as optional keyword arguments, block documentation (`@yield*` tags), and `RUBYCLI_*` environment toggles.
|
|
189
191
|
|
|
190
192
|
| Capability | Python Fire | Rubycli |
|
|
@@ -55,7 +55,7 @@ module Rubycli
|
|
|
55
55
|
next
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
-
if (return_meta = parse_return_metadata(stripped))
|
|
58
|
+
if (return_meta = parse_return_metadata(stripped, method_obj))
|
|
59
59
|
metadata[:returns] << return_meta
|
|
60
60
|
summary_phase = false
|
|
61
61
|
next
|
|
@@ -68,7 +68,7 @@ module Rubycli
|
|
|
68
68
|
next
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
-
if (positional = parse_positional_line(stripped))
|
|
71
|
+
if (positional = parse_positional_line(stripped, method_obj))
|
|
72
72
|
metadata[:positionals] << positional
|
|
73
73
|
summary_phase = false
|
|
74
74
|
next
|
|
@@ -137,6 +137,7 @@ module Rubycli
|
|
|
137
137
|
description = nil if description&.empty?
|
|
138
138
|
|
|
139
139
|
raw_types = parse_type_annotation(type_str)
|
|
140
|
+
audit_type_annotation_tokens(raw_types, method_obj)
|
|
140
141
|
types, allowed_values = partition_type_tokens(raw_types)
|
|
141
142
|
|
|
142
143
|
long_option = nil
|
|
@@ -193,6 +194,7 @@ module Rubycli
|
|
|
193
194
|
|
|
194
195
|
if (types.nil? || types.empty?) && type_token
|
|
195
196
|
inline_raw_types = parse_type_annotation(type_token)
|
|
197
|
+
audit_type_annotation_tokens(inline_raw_types, method_obj)
|
|
196
198
|
inline_types, inline_allowed = partition_type_tokens(inline_raw_types)
|
|
197
199
|
types = inline_types
|
|
198
200
|
allowed_values = merge_allowed_values(allowed_values, inline_allowed)
|
|
@@ -309,6 +311,7 @@ module Rubycli
|
|
|
309
311
|
description = description_tokens.join(' ').strip
|
|
310
312
|
description = nil if description.empty?
|
|
311
313
|
raw_types = parse_type_annotation(type_token)
|
|
314
|
+
audit_type_annotation_tokens(raw_types, method_obj)
|
|
312
315
|
types, allowed_values = partition_type_tokens(raw_types)
|
|
313
316
|
|
|
314
317
|
keyword = long_option.delete_prefix('--').tr('-', '_').to_sym
|
|
@@ -327,7 +330,7 @@ module Rubycli
|
|
|
327
330
|
)
|
|
328
331
|
end
|
|
329
332
|
|
|
330
|
-
def parse_positional_line(line)
|
|
333
|
+
def parse_positional_line(line, method_obj)
|
|
331
334
|
return nil if line.start_with?('--') || line.start_with?('-')
|
|
332
335
|
|
|
333
336
|
tokens = combine_bracketed_tokens(line.split(/\s+/))
|
|
@@ -346,6 +349,7 @@ module Rubycli
|
|
|
346
349
|
description = nil if description.empty?
|
|
347
350
|
|
|
348
351
|
raw_types = parse_type_annotation(type_token)
|
|
352
|
+
audit_type_annotation_tokens(raw_types, method_obj)
|
|
349
353
|
types, allowed_values = partition_type_tokens(raw_types)
|
|
350
354
|
placeholder_info = analyze_placeholder(placeholder)
|
|
351
355
|
inferred_types = infer_types_from_placeholder(
|
|
@@ -371,10 +375,11 @@ module Rubycli
|
|
|
371
375
|
)
|
|
372
376
|
end
|
|
373
377
|
|
|
374
|
-
def parse_return_metadata(line)
|
|
378
|
+
def parse_return_metadata(line, method_obj)
|
|
375
379
|
yard_match = /\A@return\s+\[([^\]]+)\](?:\s+(.*))?\z/.match(line)
|
|
376
380
|
if yard_match
|
|
377
381
|
types = parse_type_annotation(yard_match[1])
|
|
382
|
+
audit_type_annotation_tokens(types, method_obj)
|
|
378
383
|
description = yard_match[2]&.strip
|
|
379
384
|
return ReturnDefinition.new(types: types, description: description)
|
|
380
385
|
end
|
|
@@ -382,6 +387,7 @@ module Rubycli
|
|
|
382
387
|
shorthand_match = /\A=>\s+(\[[^\]]+\]|[^\s]+)(?:\s+(.*))?\z/.match(line)
|
|
383
388
|
if shorthand_match
|
|
384
389
|
types = parse_type_annotation(shorthand_match[1])
|
|
390
|
+
audit_type_annotation_tokens(types, method_obj)
|
|
385
391
|
description = shorthand_match[2]&.strip
|
|
386
392
|
return ReturnDefinition.new(types: types, description: description)
|
|
387
393
|
end
|
|
@@ -390,11 +396,74 @@ module Rubycli
|
|
|
390
396
|
stripped = line.sub(/\Areturn\s+/, '')
|
|
391
397
|
type_token, description = stripped.split(/\s+/, 2)
|
|
392
398
|
types = parse_type_annotation(type_token)
|
|
399
|
+
audit_type_annotation_tokens(types, method_obj)
|
|
393
400
|
description = description&.strip
|
|
394
401
|
return ReturnDefinition.new(types: types, description: description)
|
|
395
402
|
end
|
|
396
403
|
end
|
|
397
404
|
|
|
405
|
+
def doc_issue_location(method_obj)
|
|
406
|
+
return [nil, nil] unless method_obj.respond_to?(:source_location)
|
|
407
|
+
|
|
408
|
+
source_file, source_line = method_obj.source_location
|
|
409
|
+
line_number = source_line ? [source_line - 1, 1].max : nil
|
|
410
|
+
[source_file, line_number]
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def audit_type_annotation_tokens(tokens, method_obj)
|
|
414
|
+
return if tokens.nil? || tokens.empty?
|
|
415
|
+
return unless @environment.doc_check_mode?
|
|
416
|
+
|
|
417
|
+
source_file, line_number = doc_issue_location(method_obj)
|
|
418
|
+
literal_tokens_present = Array(tokens).any? do |token|
|
|
419
|
+
literal_entry = literal_entry_from_token(token)
|
|
420
|
+
literal_entry || literal_hint_token?(token)
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
Array(tokens).each do |token|
|
|
424
|
+
audit_single_token(token, source_file, line_number, literal_tokens_present)
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def audit_single_token(token, source_file, line_number, literal_context)
|
|
429
|
+
normalized = token.to_s.strip
|
|
430
|
+
return if normalized.empty?
|
|
431
|
+
|
|
432
|
+
if literal_hint_token?(normalized)
|
|
433
|
+
expand_annotation_token(normalized).each do |entry|
|
|
434
|
+
audit_literal_entry(entry, source_file, line_number)
|
|
435
|
+
end
|
|
436
|
+
return
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
literal_entry = literal_entry_from_token(normalized)
|
|
440
|
+
if literal_entry
|
|
441
|
+
return
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
if literal_context && literal_token_candidate?(normalized, include_uppercase: true) && !known_type_token?(normalized)
|
|
445
|
+
warn_unknown_allowed_value(normalized, source_file, line_number)
|
|
446
|
+
return
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
if literal_token_candidate?(normalized, include_uppercase: false)
|
|
450
|
+
warn_unknown_allowed_value(normalized, source_file, line_number)
|
|
451
|
+
return
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
return if inline_type_hint?(normalized)
|
|
455
|
+
return if known_type_token?(normalized)
|
|
456
|
+
|
|
457
|
+
warn_unknown_type_token(normalized, source_file, line_number)
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def audit_literal_entry(token, source_file, line_number)
|
|
461
|
+
literal_entry = literal_entry_from_token(token)
|
|
462
|
+
return if literal_entry
|
|
463
|
+
|
|
464
|
+
warn_unknown_allowed_value(token, source_file, line_number)
|
|
465
|
+
end
|
|
466
|
+
|
|
398
467
|
def extract_parameter_defaults(method_obj)
|
|
399
468
|
location = method_obj.source_location
|
|
400
469
|
return {} unless location
|
|
@@ -735,6 +804,93 @@ module Rubycli
|
|
|
735
804
|
nil
|
|
736
805
|
end
|
|
737
806
|
|
|
807
|
+
def literal_token_candidate?(token, include_uppercase: false)
|
|
808
|
+
return false unless token
|
|
809
|
+
|
|
810
|
+
stripped = token.strip
|
|
811
|
+
return false if stripped.empty?
|
|
812
|
+
|
|
813
|
+
lowered = stripped.downcase
|
|
814
|
+
return true if %w[true false nil null ~].include?(lowered)
|
|
815
|
+
return true if stripped.start_with?(':', '"', "'")
|
|
816
|
+
return true if stripped.match?(/\A-?\d/)
|
|
817
|
+
|
|
818
|
+
pattern = include_uppercase ? /\A[a-z0-9._-]+\z/i : /\A[a-z0-9._-]+\z/
|
|
819
|
+
stripped.match?(pattern)
|
|
820
|
+
end
|
|
821
|
+
|
|
822
|
+
def warn_unknown_type_token(token, source_file, line_number)
|
|
823
|
+
return if token.nil? || token.empty?
|
|
824
|
+
|
|
825
|
+
suggestions = type_token_suggestions(token)
|
|
826
|
+
message = "Unknown type token '#{token}'"
|
|
827
|
+
if suggestions.any?
|
|
828
|
+
hint = suggestions.first(2).join(' or ')
|
|
829
|
+
message = "#{message} (did you mean #{hint}?)"
|
|
830
|
+
end
|
|
831
|
+
@environment.handle_documentation_issue(message, file: source_file, line: line_number)
|
|
832
|
+
end
|
|
833
|
+
|
|
834
|
+
def warn_unknown_allowed_value(token, source_file, line_number)
|
|
835
|
+
return if token.nil? || token.empty?
|
|
836
|
+
|
|
837
|
+
suggestions = allowed_value_suggestions(token)
|
|
838
|
+
message = "Unknown allowed value token '#{token}'"
|
|
839
|
+
if suggestions.any?
|
|
840
|
+
hint = suggestions.first(2).join(' or ')
|
|
841
|
+
message = "#{message} (did you mean #{hint}?)"
|
|
842
|
+
end
|
|
843
|
+
@environment.handle_documentation_issue(message, file: source_file, line: line_number)
|
|
844
|
+
end
|
|
845
|
+
|
|
846
|
+
def type_token_suggestions(token)
|
|
847
|
+
dictionary = type_dictionary
|
|
848
|
+
return [] if dictionary.empty?
|
|
849
|
+
|
|
850
|
+
require 'did_you_mean'
|
|
851
|
+
checker = DidYouMean::SpellChecker.new(dictionary: dictionary)
|
|
852
|
+
checker.correct(token.to_s).take(3)
|
|
853
|
+
rescue LoadError, NameError
|
|
854
|
+
[]
|
|
855
|
+
end
|
|
856
|
+
|
|
857
|
+
def allowed_value_suggestions(token)
|
|
858
|
+
stripped = token.to_s.strip
|
|
859
|
+
return [] if stripped.empty?
|
|
860
|
+
|
|
861
|
+
candidates = []
|
|
862
|
+
if stripped.match?(/\A[a-z0-9._-]+\z/i)
|
|
863
|
+
candidates << ":#{stripped.downcase}"
|
|
864
|
+
candidates << stripped.downcase.inspect
|
|
865
|
+
end
|
|
866
|
+
return [] if candidates.empty?
|
|
867
|
+
|
|
868
|
+
require 'did_you_mean'
|
|
869
|
+
checker = DidYouMean::SpellChecker.new(dictionary: candidates)
|
|
870
|
+
checker.correct(stripped).take(2)
|
|
871
|
+
rescue LoadError, NameError
|
|
872
|
+
candidates.first(1)
|
|
873
|
+
end
|
|
874
|
+
|
|
875
|
+
def type_dictionary
|
|
876
|
+
builtins = (INLINE_TYPE_HINTS + %w[NilClass Fixnum Decimal Struct]).uniq
|
|
877
|
+
constant_names = []
|
|
878
|
+
begin
|
|
879
|
+
ObjectSpace.each_object(Module) do |mod|
|
|
880
|
+
name = mod.name
|
|
881
|
+
next unless name && !name.empty?
|
|
882
|
+
|
|
883
|
+
constant_names << name
|
|
884
|
+
parts = name.split('::')
|
|
885
|
+
constant_names << parts.last if parts.size > 1
|
|
886
|
+
end
|
|
887
|
+
rescue StandardError
|
|
888
|
+
constant_names = Object.constants.map(&:to_s)
|
|
889
|
+
end
|
|
890
|
+
|
|
891
|
+
(builtins + constant_names).map(&:to_s).map(&:strip).reject(&:empty?).uniq
|
|
892
|
+
end
|
|
893
|
+
|
|
738
894
|
|
|
739
895
|
def placeholder_token?(token)
|
|
740
896
|
return false unless token
|
|
@@ -786,8 +942,16 @@ module Rubycli
|
|
|
786
942
|
def known_type_token?(token)
|
|
787
943
|
return false unless token
|
|
788
944
|
|
|
789
|
-
|
|
790
|
-
|
|
945
|
+
normalized = normalize_type_token(token)
|
|
946
|
+
return false if normalized.empty?
|
|
947
|
+
|
|
948
|
+
return true if primitive_type_token?(normalized)
|
|
949
|
+
|
|
950
|
+
if (inner = array_inner_type_token(normalized))
|
|
951
|
+
return known_type_token?(inner)
|
|
952
|
+
end
|
|
953
|
+
|
|
954
|
+
!safe_constant_lookup(normalized).nil?
|
|
791
955
|
end
|
|
792
956
|
|
|
793
957
|
def inline_type_hint?(token)
|
|
@@ -805,6 +969,53 @@ module Rubycli
|
|
|
805
969
|
INLINE_TYPE_HINTS.include?(base)
|
|
806
970
|
end
|
|
807
971
|
|
|
972
|
+
def primitive_type_token?(token)
|
|
973
|
+
return false if token.nil? || token.empty?
|
|
974
|
+
|
|
975
|
+
base = token.to_s
|
|
976
|
+
INLINE_TYPE_HINTS.include?(base) || %w[NilClass Fixnum Decimal Struct].include?(base)
|
|
977
|
+
end
|
|
978
|
+
|
|
979
|
+
def array_inner_type_token(token)
|
|
980
|
+
return nil unless token
|
|
981
|
+
|
|
982
|
+
stripped = token.to_s.strip
|
|
983
|
+
if stripped.end_with?('[]')
|
|
984
|
+
stripped[0..-3]
|
|
985
|
+
elsif stripped.start_with?('Array<') && stripped.end_with?('>')
|
|
986
|
+
stripped[6..-2].strip
|
|
987
|
+
else
|
|
988
|
+
nil
|
|
989
|
+
end
|
|
990
|
+
end
|
|
991
|
+
|
|
992
|
+
def safe_constant_lookup(name)
|
|
993
|
+
parts = name.to_s.split('::').reject(&:empty?)
|
|
994
|
+
return nil if parts.empty?
|
|
995
|
+
|
|
996
|
+
context = Object
|
|
997
|
+
parts.each do |const_name|
|
|
998
|
+
return nil unless context.const_defined?(const_name, false)
|
|
999
|
+
|
|
1000
|
+
context = context.const_get(const_name)
|
|
1001
|
+
end
|
|
1002
|
+
context
|
|
1003
|
+
rescue NameError
|
|
1004
|
+
nil
|
|
1005
|
+
end
|
|
1006
|
+
|
|
1007
|
+
def literal_hint_token?(token)
|
|
1008
|
+
return false unless token
|
|
1009
|
+
|
|
1010
|
+
stripped = token.to_s.strip
|
|
1011
|
+
return false if stripped.empty?
|
|
1012
|
+
|
|
1013
|
+
stripped.start_with?('%i[') ||
|
|
1014
|
+
stripped.start_with?('%I[') ||
|
|
1015
|
+
stripped.start_with?('%w[') ||
|
|
1016
|
+
stripped.start_with?('%W[')
|
|
1017
|
+
end
|
|
1018
|
+
|
|
808
1019
|
def parameter_role(method_obj, keyword)
|
|
809
1020
|
return nil unless method_obj.respond_to?(:parameters)
|
|
810
1021
|
|
data/lib/rubycli/version.rb
CHANGED
data/lib/rubycli.rb
CHANGED
|
@@ -531,19 +531,24 @@ module Rubycli
|
|
|
531
531
|
|
|
532
532
|
def build_missing_constant_message(name, defined_constants, full_path, details: nil)
|
|
533
533
|
lines = ["Could not find definition: #{name}"]
|
|
534
|
-
lines <<
|
|
534
|
+
lines << ''
|
|
535
|
+
lines << "Loaded file: #{File.expand_path(full_path)}" if full_path
|
|
535
536
|
|
|
536
537
|
if defined_constants && !defined_constants.empty?
|
|
537
538
|
sample = defined_constants.first(5)
|
|
538
|
-
suffix = defined_constants.size > sample.size ? "
|
|
539
|
-
lines << "
|
|
539
|
+
suffix = defined_constants.size > sample.size ? " … (#{defined_constants.size} total)" : ''
|
|
540
|
+
lines << "Constants found in this file: #{sample.join(', ')}#{suffix}"
|
|
540
541
|
else
|
|
541
|
-
lines <<
|
|
542
|
+
lines << 'Rubycli could not detect any publicly exposable constants in this file.'
|
|
542
543
|
end
|
|
543
544
|
|
|
544
|
-
|
|
545
|
+
if details
|
|
546
|
+
lines << ''
|
|
547
|
+
lines << details
|
|
548
|
+
end
|
|
545
549
|
|
|
546
|
-
lines <<
|
|
550
|
+
lines << ''
|
|
551
|
+
lines << 'Hint: Ensure the CLASS_OR_MODULE argument is correct when invoking the CLI.'
|
|
547
552
|
lines.join("\n")
|
|
548
553
|
end
|
|
549
554
|
end
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rubycli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- inakaegg
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2025-11-
|
|
10
|
+
date: 2025-11-11 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
12
|
description: Rubycli turns plain Ruby classes and modules into command-line interfaces
|
|
13
13
|
by reading their documentation comments, inspired by Python Fire but tailored for
|