tng 0.5.2 → 0.5.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 29607684b1a5af61f1d0cf0647eaee7c99689992476309d183a3cd95634fe83e
4
- data.tar.gz: eaf5335df396998b57c1eaf1c5a98ae6e96fe4a7962e7aaedadcaec86fefe681
3
+ metadata.gz: 8adc64b4a410ddec1b69ada11a09e9cee648f88692873ae6551990753ba0f564
4
+ data.tar.gz: aa5c284afe9a769733b3ab87d37c1c51f23bf07ee4b3ffdfac65f60fe39f3548
5
5
  SHA512:
6
- metadata.gz: aa6f57d765bce27706eceac6c44a8318c5950e6374be0fa0f2c698865b0fa09c84dfe5c0372bdc0e791104a22c99f22ac3dfe6895c753e5aa7cd29d4bcd70776
7
- data.tar.gz: f24dd93a4fc7da347c5046f6d66c87844fb31fac451957a3f61284ceac1895fa72255b22bf5b2ac04cc22aa04edffe97c3692ac96dacc0fe8b0c1ca855dbfa8c
6
+ metadata.gz: 01701577e5856bbe861677eb03d8b8e629046e48e2301a4b4202cd78049252d7478ce7032c6f80cb547b9e9e324893bb5ad24859a16ae0491857287e9c040838
7
+ data.tar.gz: 6c8e17a8f0d9e6d6a2fa0c564b01d54eaa28c4e1d495f0fe359818477eed439632b42ea96d2bc7dfd32543fe9ada7759989c71950b538fdb76e2debc6f17860d
data/README.md CHANGED
@@ -144,12 +144,12 @@ bundle exec tng -f order.rb -m calculate_total -t
144
144
  - Method call chains
145
145
  - Mermaid diagram generation
146
146
 
147
- #### Impact Audit
147
+ #### Regression Check
148
148
 
149
149
  Compare a method against its committed (`HEAD`) version and surface blast radius:
150
150
 
151
151
  ```bash
152
- # Run impact audit for a method
152
+ # Run regression check for a method
153
153
  bundle exec tng --file app/services/payment_service.rb --method process_payment --impact
154
154
 
155
155
  # JSON output for automation/CI
@@ -318,7 +318,7 @@ Path 3: !discount_code.present? → calculate_tax → total
318
318
  | `--method` | `-m` | Method name to analyze |
319
319
  | `--audit` | `-a` | Run audit mode |
320
320
  | `--trace` | `-t` | Run symbolic trace mode |
321
- | `--impact` | | Run impact audit mode |
321
+ | `--impact` | | Run regression check mode |
322
322
  | `--clones` | `-c` | Run clone detection |
323
323
  | `--deadcode` | `-d` | Run dead code detection |
324
324
  | `--level` | `-l` | Clone detection level (1, 2, 3, or all) |
@@ -337,7 +337,7 @@ bundle exec tng -f payment_service.rb -m process -a
337
337
  # Symbolic trace
338
338
  bundle exec tng -f order.rb -m calculate_total -t
339
339
 
340
- # Impact audit
340
+ # Regression check
341
341
  bundle exec tng -f payment_service.rb -m process --impact
342
342
 
343
343
  # Clone detection (all levels)
data/bin/tng CHANGED
@@ -32,6 +32,17 @@ class CLI
32
32
  include TTY::Option
33
33
  include Tng::Utils
34
34
 
35
+ def method_label(class_name, method_name)
36
+ return method_name if class_name.nil? || class_name.empty?
37
+ "#{class_name}##{method_name}"
38
+ end
39
+
40
+ def parse_method_label(label)
41
+ return [nil, label] if label.nil? || !label.include?("#")
42
+ class_name, method_name = label.split("#", 2)
43
+ [class_name, method_name]
44
+ end
45
+
35
46
  usage do
36
47
  program "tng"
37
48
  desc "LLM-Powered Rails Test Generator"
@@ -40,6 +51,10 @@ class CLI
40
51
  example " • Select specific methods from filtered lists", ""
41
52
  example " • Generate tests for individual methods", ""
42
53
  example ""
54
+ example "Ignore paths via config (config/initializers/tng.rb):", ""
55
+ example " config.ignore_files = [\"app/models/user.rb\"]", ""
56
+ example " config.ignore_folders = [\"app/admin\", \"vendor\"]", ""
57
+ example ""
43
58
  example "Direct mode (automatic file type detection):", ""
44
59
  example " bundle exec tng app/controllers/users_controller.rb index", ""
45
60
  example " bundle exec tng f=users_controller.rb m=show", ""
@@ -57,8 +72,11 @@ class CLI
57
72
  example " bundle exec tng app/services/payment_processor.rb process_payment --xray", ""
58
73
  example " bundle exec tng --file=users_controller.rb --method=create --xray --json", ""
59
74
  example ""
60
- example "Impact Audit:", ""
75
+ example "Regression Check:", ""
61
76
  example " bundle exec tng --file=app/services/payment_service.rb --method=process --impact", ""
77
+ example ""
78
+ example "Call Sites:", ""
79
+ example " bundle exec tng --file=app/services/payment_service.rb --method=process --callsites", ""
62
80
  end
63
81
 
64
82
  option :file do
@@ -73,6 +91,11 @@ class CLI
73
91
  desc "Method name (for per-method tests)"
74
92
  end
75
93
 
94
+ option :class_name do
95
+ long "--class=CLASS"
96
+ desc "Class/module name to disambiguate methods when multiple classes define the same method in a file"
97
+ end
98
+
76
99
  flag :clones do
77
100
  short "-c"
78
101
  long "--clones"
@@ -107,9 +130,14 @@ class CLI
107
130
  desc "Generate and visualize a symbolic trace"
108
131
  end
109
132
 
133
+ flag :callsites do
134
+ long "--callsites"
135
+ desc "Find in-repo call sites for a method"
136
+ end
137
+
110
138
  flag :impact do
111
139
  long "--impact"
112
- desc "Run impact audit (compare against Git HEAD)"
140
+ desc "Run regression check (compare against Git HEAD)"
113
141
  end
114
142
 
115
143
  flag :xray do
@@ -172,6 +200,8 @@ class CLI
172
200
  handle_fix_command
173
201
  elsif params[:impact] && params[:file]
174
202
  run_direct_impact
203
+ elsif params[:callsites] && params[:file]
204
+ run_direct_call_sites
175
205
  elsif params[:trace] && params[:file]
176
206
  run_direct_trace
177
207
  elsif params[:deadcode]
@@ -220,6 +250,8 @@ class CLI
220
250
  normalized << "--impact"
221
251
  when /^(?:--)?(xray)$/
222
252
  normalized << "--xray"
253
+ when /^(?:--)?(callsites)$/
254
+ normalized << "--callsites"
223
255
  when /^(?:--)?(json|j)$/
224
256
  normalized << "--json"
225
257
  when /^(?:--)?(fix|x)$/
@@ -518,19 +550,20 @@ class CLI
518
550
  return
519
551
  end
520
552
 
521
- items = methods.map { |m| { name: m[:name], path: controller[:name] } }
553
+ items = methods.map { |m| { name: method_label(controller[:name], m[:name]), path: controller[:name] } }
522
554
  selected_name = @go_ui.show_list_view("Select Method to Audit", items)
523
555
  return if selected_name == "back"
524
556
 
525
- method_choice = methods.find { |m| m[:name] == selected_name }
557
+ selected_class, selected_method = parse_method_label(selected_name)
558
+ method_choice = methods.find { |m| m[:name] == selected_method }
526
559
  return unless method_choice
560
+ method_choice = method_choice.merge(class_name: selected_class || controller[:name])
527
561
 
528
562
  run_audit_for_controller_method(controller, method_choice)
529
563
  end
530
564
 
531
565
  def run_audit_for_controller_method(controller, method_info)
532
566
  result = nil
533
-
534
567
  @go_ui.show_progress("Auditing #{controller[:name]}##{method_info[:name]}") do |progress|
535
568
  progress.update("Analyzing method context...", 25)
536
569
  progress.update("Running logical analysis...", 50)
@@ -572,6 +605,7 @@ class CLI
572
605
  puts @pastel.red("Error: File '#{file_path}' not found.")
573
606
  return
574
607
  end
608
+ return unless ensure_not_ignored(file_path)
575
609
 
576
610
  absolute_path = File.expand_path(file_path)
577
611
 
@@ -691,7 +725,7 @@ class CLI
691
725
  begin
692
726
  files_json = Tng.list_deadcode_files(root)
693
727
  files = JSON.parse(files_json)
694
- return files.select { |f| File.file?(f) }
728
+ return files.select { |f| File.file?(f) && !Tng::Utils.ignored_path?(f) }
695
729
  rescue => e
696
730
  puts @pastel.red("Error listing repo files: #{e.message}")
697
731
  []
@@ -737,19 +771,20 @@ class CLI
737
771
  return
738
772
  end
739
773
 
740
- items = methods.map { |m| { name: m[:name], path: model[:name] } }
774
+ items = methods.map { |m| { name: method_label(model[:name], m[:name]), path: model[:name] } }
741
775
  selected_name = @go_ui.show_list_view("Select Method to Audit", items)
742
776
  return if selected_name == "back"
743
777
 
744
- method_choice = methods.find { |m| m[:name] == selected_name }
778
+ selected_class, selected_method = parse_method_label(selected_name)
779
+ method_choice = methods.find { |m| m[:name] == selected_method }
745
780
  return unless method_choice
781
+ method_choice = method_choice.merge(class_name: selected_class || model[:name])
746
782
 
747
783
  run_audit_for_model_method(model, method_choice)
748
784
  end
749
785
 
750
786
  def run_audit_for_model_method(model, method_info)
751
787
  result = nil
752
-
753
788
  @go_ui.show_progress("Auditing #{model[:name]}##{method_info[:name]}") do |progress|
754
789
  progress.update("Parsing method details...", 25)
755
790
  progress.update("Analyzing method context...", 25)
@@ -812,19 +847,20 @@ class CLI
812
847
  return
813
848
  end
814
849
 
815
- items = methods.map { |m| { name: m[:name], path: service[:name] } }
850
+ items = methods.map { |m| { name: method_label(service[:name], m[:name]), path: service[:name] } }
816
851
  selected_name = @go_ui.show_list_view("Select Method to Audit", items)
817
852
  return if selected_name == "back"
818
853
 
819
- method_choice = methods.find { |m| m[:name] == selected_name }
854
+ selected_class, selected_method = parse_method_label(selected_name)
855
+ method_choice = methods.find { |m| m[:name] == selected_method }
820
856
  return unless method_choice
857
+ method_choice = method_choice.merge(class_name: selected_class || service[:name])
821
858
 
822
859
  run_audit_for_service_method(service, method_choice)
823
860
  end
824
861
 
825
862
  def run_audit_for_service_method(service, method_info)
826
863
  result = nil
827
-
828
864
  @go_ui.show_progress("Auditing #{service[:name]}##{method_info[:name]}") do |progress|
829
865
  progress.update("Parsing method details...", 25)
830
866
  progress.update("Analyzing method context...", 25)
@@ -885,19 +921,20 @@ class CLI
885
921
  return
886
922
  end
887
923
 
888
- items = methods.map { |m| { name: m[:name], path: other_file[:name] } }
924
+ items = methods.map { |m| { name: method_label(other_file[:name], m[:name]), path: other_file[:name] } }
889
925
  selected_name = @go_ui.show_list_view("Audit Method in #{other_file[:name]}", items)
890
926
  return if selected_name == "back"
891
927
 
892
- method_choice = methods.find { |m| m[:name] == selected_name }
928
+ selected_class, selected_method = parse_method_label(selected_name)
929
+ method_choice = methods.find { |m| m[:name] == selected_method }
893
930
  return unless method_choice
931
+ method_choice = method_choice.merge(class_name: selected_class || other_file[:name])
894
932
 
895
933
  run_audit_for_other_method(other_file, method_choice)
896
934
  end
897
935
 
898
936
  def run_audit_for_other_method(other_file, method_info)
899
937
  result = nil
900
-
901
938
  @go_ui.show_progress("Auditing #{other_file[:name]}##{method_info[:name]}") do |progress|
902
939
  progress.update("Parsing method details...", 25)
903
940
  progress.update("Analyzing method context...", 25)
@@ -1051,12 +1088,14 @@ class CLI
1051
1088
  return
1052
1089
  end
1053
1090
 
1054
- items = methods.map { |m| { name: m[:name], path: controller[:name] } }
1091
+ items = methods.map { |m| { name: method_label(controller[:name], m[:name]), path: controller[:name] } }
1055
1092
  selected_name = @go_ui.show_list_view("Select Method in #{controller[:name]}", items)
1056
1093
  return if selected_name == "back"
1057
1094
 
1058
- method_choice = methods.find { |m| m[:name] == selected_name }
1095
+ selected_class, selected_method = parse_method_label(selected_name)
1096
+ method_choice = methods.find { |m| m[:name] == selected_method }
1059
1097
  return unless method_choice
1098
+ method_choice = method_choice.merge(class_name: selected_class || controller[:name])
1060
1099
 
1061
1100
  generate_test_for_controller_method(controller, method_choice)
1062
1101
  end
@@ -1109,12 +1148,14 @@ class CLI
1109
1148
  return
1110
1149
  end
1111
1150
 
1112
- items = methods.map { |m| { name: m[:name], path: model[:name] } }
1151
+ items = methods.map { |m| { name: method_label(model[:name], m[:name]), path: model[:name] } }
1113
1152
  selected_name = @go_ui.show_list_view("Select Method in #{model[:name]}", items)
1114
1153
  return if selected_name == "back"
1115
1154
 
1116
- method_choice = methods.find { |m| m[:name] == selected_name }
1155
+ selected_class, selected_method = parse_method_label(selected_name)
1156
+ method_choice = methods.find { |m| m[:name] == selected_method }
1117
1157
  return unless method_choice
1158
+ method_choice = method_choice.merge(class_name: selected_class || model[:name])
1118
1159
 
1119
1160
  generate_test_for_model_method(model, method_choice)
1120
1161
  end
@@ -1169,12 +1210,14 @@ class CLI
1169
1210
  return
1170
1211
  end
1171
1212
 
1172
- items = methods.map { |m| { name: m[:name], path: service[:name] } }
1213
+ items = methods.map { |m| { name: method_label(service[:name], m[:name]), path: service[:name] } }
1173
1214
  selected_name = @go_ui.show_list_view("Select Method in #{service[:name]}", items)
1174
1215
  return if selected_name == "back"
1175
1216
 
1176
- method_choice = methods.find { |m| m[:name] == selected_name }
1217
+ selected_class, selected_method = parse_method_label(selected_name)
1218
+ method_choice = methods.find { |m| m[:name] == selected_method }
1177
1219
  return unless method_choice
1220
+ method_choice = method_choice.merge(class_name: selected_class || service[:name])
1178
1221
 
1179
1222
  generate_test_for_service_method(service, method_choice)
1180
1223
  end
@@ -1193,12 +1236,14 @@ class CLI
1193
1236
  return
1194
1237
  end
1195
1238
 
1196
- items = methods.map { |m| { name: m[:name], path: other_file[:name] } }
1239
+ items = methods.map { |m| { name: method_label(other_file[:name], m[:name]), path: other_file[:name] } }
1197
1240
  selected_name = @go_ui.show_list_view("Select Method in #{other_file[:name]}", items)
1198
1241
  return if selected_name == "back"
1199
1242
 
1200
- method_choice = methods.find { |m| m[:name] == selected_name }
1243
+ selected_class, selected_method = parse_method_label(selected_name)
1244
+ method_choice = methods.find { |m| m[:name] == selected_method }
1201
1245
  return unless method_choice
1246
+ method_choice = method_choice.merge(class_name: selected_class || other_file[:name])
1202
1247
 
1203
1248
  generate_test_for_other_method(other_file, method_choice)
1204
1249
  end
@@ -1361,7 +1406,7 @@ class CLI
1361
1406
  end
1362
1407
 
1363
1408
  def show_post_generation_menu(result)
1364
- if @go_ui.is_a?(Tng::UI::JsonSession)
1409
+ if defined?(Tng::UI::JsonSession) && @go_ui.is_a?(Tng::UI::JsonSession)
1365
1410
  @go_ui.show_generation_result(result)
1366
1411
  return
1367
1412
  end
@@ -1409,6 +1454,16 @@ class CLI
1409
1454
  end
1410
1455
  end
1411
1456
 
1457
+ def ensure_not_ignored(file_path)
1458
+ return true unless Tng::Utils.ignored_path?(file_path)
1459
+
1460
+ puts @pastel.decorate(
1461
+ "Ignored by TNG config (ignore_files/ignore_folders). Remove it from config to proceed.",
1462
+ Tng::UI::Theme.color(:error)
1463
+ )
1464
+ false
1465
+ end
1466
+
1412
1467
  def initialize_config_and_clients
1413
1468
  @config_initialized = true
1414
1469
 
@@ -1507,25 +1562,25 @@ class CLI
1507
1562
  end
1508
1563
 
1509
1564
  def impact_controller_method
1510
- select_controller_and_method("Impact Audit") do |controller, method_info|
1565
+ select_controller_and_method("Regression Check") do |controller, method_info|
1511
1566
  run_impact_for_method(controller, method_info)
1512
1567
  end
1513
1568
  end
1514
1569
 
1515
1570
  def impact_model_method
1516
- select_model_and_method("Impact Audit") do |model, method_info|
1571
+ select_model_and_method("Regression Check") do |model, method_info|
1517
1572
  run_impact_for_method(model, method_info)
1518
1573
  end
1519
1574
  end
1520
1575
 
1521
1576
  def impact_service_method
1522
- select_service_and_method("Impact Audit") do |service, method_info|
1577
+ select_service_and_method("Regression Check") do |service, method_info|
1523
1578
  run_impact_for_method(service, method_info)
1524
1579
  end
1525
1580
  end
1526
1581
 
1527
1582
  def impact_other_method
1528
- select_other_and_method("Impact Audit") do |file, method_info|
1583
+ select_other_and_method("Regression Check") do |file, method_info|
1529
1584
  run_impact_for_method(file, method_info)
1530
1585
  end
1531
1586
  end
@@ -1562,11 +1617,12 @@ class CLI
1562
1617
  project_root = Dir.pwd
1563
1618
  path = File.expand_path(file_info[:path])
1564
1619
 
1620
+ class_name = method_info[:class_name] || file_info[:name]
1565
1621
  trace_json = Tng::Analyzer::Context.analyze_symbolic_trace(
1566
1622
  project_root,
1567
1623
  path,
1568
1624
  method_info[:name],
1569
- nil
1625
+ class_name
1570
1626
  )
1571
1627
 
1572
1628
  f = Tempfile.new(['trace', '.json'])
@@ -1589,12 +1645,12 @@ class CLI
1589
1645
  def run_impact_for_method(file_info, method_info)
1590
1646
  result_path = nil
1591
1647
 
1592
- @go_ui.show_spinner("Running impact audit for #{method_info[:name]}...") do
1648
+ @go_ui.show_spinner("Running regression check for #{method_info[:name]}...") do
1593
1649
  begin
1594
1650
  project_root = Dir.pwd
1595
1651
  path = File.expand_path(file_info[:path])
1596
1652
 
1597
- class_name = file_info[:name]
1653
+ class_name = method_info[:class_name] || file_info[:name]
1598
1654
  impact_json = Tng::Analyzer::Context.analyze_impact(
1599
1655
  project_root,
1600
1656
  path,
@@ -1607,7 +1663,7 @@ class CLI
1607
1663
  f.close
1608
1664
  result_path = f.path
1609
1665
 
1610
- { success: true, message: "Impact audit completed" }
1666
+ { success: true, message: "Regression check completed" }
1611
1667
  rescue => e
1612
1668
  { success: false, message: e.message }
1613
1669
  end
@@ -1633,18 +1689,68 @@ class CLI
1633
1689
  puts @pastel.decorate("File not found: #{full_path}", Tng::UI::Theme.color(:error))
1634
1690
  return
1635
1691
  end
1692
+ return unless ensure_not_ignored(full_path)
1636
1693
 
1637
1694
  relative_path = full_path.gsub("#{Dir.pwd}/", "")
1638
1695
  namespaced_name = relative_path.sub(/\.rb\z/, "").split("/").map(&:camelize).join("::")
1639
1696
  file_info = { path: full_path, name: namespaced_name }
1640
- method_info = { name: method_name }
1697
+ method_info = { name: method_name, class_name: params[:class_name] }
1641
1698
 
1642
1699
  run_trace_for_method(file_info, method_info)
1643
1700
  end
1644
1701
 
1702
+ def run_direct_call_sites
1703
+ file_path = params[:file]
1704
+ method_name = params[:method]
1705
+ class_name = params[:class_name]
1706
+
1707
+ unless method_name
1708
+ puts @pastel.decorate("Please specify a method name with --method or -m", Tng::UI::Theme.color(:error))
1709
+ return
1710
+ end
1711
+
1712
+ full_path = File.expand_path(file_path)
1713
+ unless File.exist?(full_path)
1714
+ puts @pastel.decorate("File not found: #{full_path}", Tng::UI::Theme.color(:error))
1715
+ return
1716
+ end
1717
+ return unless ensure_not_ignored(full_path)
1718
+
1719
+ relative_path = full_path.gsub("#{Dir.pwd}/", "")
1720
+ namespaced_name = relative_path.sub(/\.rb\z/, "").split("/").map(&:camelize).join("::")
1721
+ effective_class = class_name || namespaced_name
1722
+
1723
+ sites = Tng::Analyzer::Context.analyze_call_sites(
1724
+ Dir.pwd,
1725
+ full_path,
1726
+ method_name,
1727
+ effective_class
1728
+ )
1729
+
1730
+ if params[:json]
1731
+ puts JSON.generate(sites)
1732
+ return
1733
+ end
1734
+
1735
+ if sites.nil? || sites.empty?
1736
+ puts @pastel.decorate("No call sites found.", Tng::UI::Theme.color(:success))
1737
+ return
1738
+ end
1739
+
1740
+ puts @pastel.decorate("Found #{sites.length} call sites:", Tng::UI::Theme.color(:info))
1741
+ sites.take(200).each do |site|
1742
+ file = site["file"] || site[:file] || "unknown"
1743
+ line = site["line"] || site[:line] || 0
1744
+ content = site["content"] || site[:content] || ""
1745
+ puts "#{file}:#{line} #{content}"
1746
+ end
1747
+ puts @pastel.dim("... #{sites.length - 200} more") if sites.length > 200
1748
+ end
1749
+
1645
1750
  def run_direct_impact
1646
1751
  file_path = params[:file]
1647
1752
  method_name = params[:method]
1753
+ class_name = params[:class_name]
1648
1754
 
1649
1755
  unless method_name
1650
1756
  puts @pastel.decorate("Please specify a method name with --method or -m", Tng::UI::Theme.color(:error))
@@ -1656,11 +1762,12 @@ class CLI
1656
1762
  puts @pastel.decorate("File not found: #{full_path}", Tng::UI::Theme.color(:error))
1657
1763
  return
1658
1764
  end
1765
+ return unless ensure_not_ignored(full_path)
1659
1766
 
1660
1767
  relative_path = full_path.gsub("#{Dir.pwd}/", "")
1661
1768
  namespaced_name = relative_path.sub(/\.rb\z/, "").split("/").map(&:camelize).join("::")
1662
1769
  file_info = { path: full_path, name: namespaced_name }
1663
- method_info = { name: method_name }
1770
+ method_info = { name: method_name, class_name: class_name }
1664
1771
 
1665
1772
  run_impact_for_method(file_info, method_info)
1666
1773
  end
@@ -1683,6 +1790,7 @@ class CLI
1683
1790
  puts @pastel.decorate("File not found: #{full_path}", Tng::UI::Theme.color(:error))
1684
1791
  return
1685
1792
  end
1793
+ return unless ensure_not_ignored(full_path)
1686
1794
 
1687
1795
  relative_path = full_path.gsub("#{Dir.pwd}/", "")
1688
1796
  namespaced_name = relative_path.sub(/\.rb\z/, "").split("/").map(&:camelize).join("::")
@@ -1760,12 +1868,14 @@ class CLI
1760
1868
  next
1761
1869
  end
1762
1870
 
1763
- m_items = methods.map { |m| { name: m[:name], path: choice[:name] } }
1871
+ m_items = methods.map { |m| { name: method_label(choice[:name], m[:name]), path: choice[:name] } }
1764
1872
  m_selected = @go_ui.show_list_view("Select Method to #{action_name}", m_items)
1765
1873
  next if m_selected == "back"
1766
1874
 
1767
- m_choice = methods.find { |m| m[:name] == m_selected }
1875
+ selected_class, selected_method = parse_method_label(m_selected)
1876
+ m_choice = methods.find { |m| m[:name] == selected_method }
1768
1877
  next unless m_choice
1878
+ m_choice = m_choice.merge(class_name: selected_class || choice[:name])
1769
1879
 
1770
1880
  yield(choice, m_choice)
1771
1881
  end
@@ -1799,11 +1909,13 @@ class CLI
1799
1909
  next
1800
1910
  end
1801
1911
 
1802
- m_items = methods.map { |m| { name: m[:name], path: choice[:name] } }
1912
+ m_items = methods.map { |m| { name: method_label(choice[:name], m[:name]), path: choice[:name] } }
1803
1913
  m_selected = @go_ui.show_list_view("Select Method to #{action_name}", m_items)
1804
1914
  next if m_selected == "back"
1805
1915
 
1806
- m_choice = methods.find { |m| m[:name] == m_selected }
1916
+ selected_class, selected_method = parse_method_label(m_selected)
1917
+ m_choice = methods.find { |m| m[:name] == selected_method }
1918
+ m_choice = m_choice.merge(class_name: selected_class || choice[:name]) if m_choice
1807
1919
  yield(choice, m_choice)
1808
1920
  end
1809
1921
  end
@@ -1836,11 +1948,13 @@ class CLI
1836
1948
  next
1837
1949
  end
1838
1950
 
1839
- m_items = methods.map { |m| { name: m[:name], path: choice[:name] } }
1951
+ m_items = methods.map { |m| { name: method_label(choice[:name], m[:name]), path: choice[:name] } }
1840
1952
  m_selected = @go_ui.show_list_view("Select Method to #{action_name}", m_items)
1841
1953
  next if m_selected == "back"
1842
1954
 
1843
- m_choice = methods.find { |m| m[:name] == m_selected }
1955
+ selected_class, selected_method = parse_method_label(m_selected)
1956
+ m_choice = methods.find { |m| m[:name] == selected_method }
1957
+ m_choice = m_choice.merge(class_name: selected_class || choice[:name]) if m_choice
1844
1958
  yield(choice, m_choice)
1845
1959
  end
1846
1960
  end
@@ -1873,12 +1987,14 @@ class CLI
1873
1987
  next
1874
1988
  end
1875
1989
 
1876
- m_items = methods.map { |m| { name: m[:name], path: choice[:name] } }
1990
+ m_items = methods.map { |m| { name: method_label(choice[:name], m[:name]), path: choice[:name] } }
1877
1991
  m_selected = @go_ui.show_list_view("Select Method to #{action_name}", m_items)
1878
1992
  next if m_selected == "back"
1879
1993
 
1880
- m_choice = methods.find { |m| m[:name] == m_selected }
1994
+ selected_class, selected_method = parse_method_label(m_selected)
1995
+ m_choice = methods.find { |m| m[:name] == selected_method }
1881
1996
  next unless m_choice
1997
+ m_choice = m_choice.merge(class_name: selected_class || choice[:name])
1882
1998
 
1883
1999
  yield(choice, m_choice)
1884
2000
  end
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
data/binaries/tng.bundle CHANGED
Binary file
@@ -43,6 +43,10 @@ module Tng
43
43
  # Default auto-detection checks: spec/rails_helper.rb, spec/spec_helper.rb, test/test_helper.rb
44
44
  # config.test_helper_path = "spec/rails_helper.rb"
45
45
 
46
+ # Ignore paths (relative to project root)
47
+ # config.ignore_files = []
48
+ # config.ignore_folders = []
49
+
46
50
  # Authentication Configuration
47
51
  # Set to false if your application does not require authentication
48
52
  # config.authentication_enabled = true
@@ -5,7 +5,8 @@ module Tng
5
5
  class Controller
6
6
  def self.files_in_dir(dir)
7
7
  dir = File.join(Dir.pwd, "app/controllers") if dir.nil?
8
- Tng::Analyzer::Controller.files_in_dir(dir.to_s)
8
+ files = Tng::Analyzer::Controller.files_in_dir(dir.to_s)
9
+ Tng::Utils.filter_ignored_files(files)
9
10
  end
10
11
 
11
12
  def self.routes_for_controller(file_path)
@@ -42,7 +43,7 @@ module Tng
42
43
  end
43
44
  end
44
45
 
45
- def self.methods_for_controller(controller_name)
46
+ def self.methods_for_controller(controller_name, file_path = nil)
46
47
  raise "controller_name is required" if controller_name.nil?
47
48
 
48
49
  begin
@@ -58,6 +59,20 @@ module Tng
58
59
  method.owner == controller_class
59
60
  end
60
61
 
62
+ # Fallback to file parsing if reflection fails
63
+ if action_methods.empty? && file_path && File.exist?(file_path)
64
+ source_code = File.read(file_path)
65
+ result = Prism.parse(source_code)
66
+ defined_methods = []
67
+
68
+ result.value&.child_nodes&.each do |node|
69
+ next unless node.is_a?(Prism::DefNode)
70
+ defined_methods << node.name.to_s
71
+ end
72
+
73
+ action_methods = defined_methods
74
+ end
75
+
61
76
  # Return method info
62
77
  action_methods.map do |method_name|
63
78
  {
@@ -5,7 +5,8 @@ module Tng
5
5
  class Model
6
6
  def self.files_in_dir(dir = nil)
7
7
  dir = File.join(Dir.pwd, "app/models") if dir.nil?
8
- Tng::Analyzer::Model.files_in_dir(dir.to_s)
8
+ files = Tng::Analyzer::Model.files_in_dir(dir.to_s)
9
+ Tng::Utils.filter_ignored_files(files)
9
10
  end
10
11
 
11
12
  def self.value_for_model(file_path)
@@ -23,7 +24,7 @@ module Tng
23
24
  Tng::Analyzer::Model.model_connections(file_path.to_s)
24
25
  end
25
26
 
26
- def self.methods_for_model(model_name)
27
+ def self.methods_for_model(model_name, file_path = nil)
27
28
  raise "model_name is required" if model_name.nil?
28
29
 
29
30
  begin
@@ -35,7 +36,8 @@ module Tng
35
36
  model_class.private_instance_methods(false)
36
37
  class_methods = model_class.public_methods(false) - Class.public_methods
37
38
 
38
- model_file = Object.const_source_location(model_class.name)&.first
39
+ model_file = file_path
40
+ model_file ||= Object.const_source_location(model_class.name)&.first
39
41
 
40
42
  if model_file && File.exist?(model_file)
41
43
  source_code = File.read(model_file)
@@ -122,7 +122,7 @@ module Tng
122
122
  relative_path: relative_path,
123
123
  type: file_type
124
124
  }
125
- end
125
+ end.reject { |file| Tng::Utils.ignored_path?(file[:path]) }
126
126
  end
127
127
 
128
128
  def self.determine_file_type(file_path)