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 +4 -4
- data/README.md +4 -4
- data/bin/tng +158 -42
- data/binaries/go-ui-darwin-amd64 +0 -0
- data/binaries/go-ui-darwin-arm64 +0 -0
- data/binaries/go-ui-linux-amd64 +0 -0
- data/binaries/go-ui-linux-arm64 +0 -0
- data/binaries/tng-darwin-arm64.bundle +0 -0
- data/binaries/tng-darwin-x86_64.bundle +0 -0
- data/binaries/tng-linux-arm64.so +0 -0
- data/binaries/tng-linux-x86_64.so +0 -0
- data/binaries/tng.bundle +0 -0
- data/lib/generators/tng/install_generator.rb +4 -0
- data/lib/tng/analyzers/controller.rb +17 -2
- data/lib/tng/analyzers/model.rb +5 -3
- data/lib/tng/analyzers/other.rb +1 -1
- data/lib/tng/analyzers/service.rb +12 -10
- data/lib/tng/api/http_client.rb +4 -4
- data/lib/tng/services/direct_generation.rb +19 -1
- data/lib/tng/services/extract_methods.rb +3 -3
- data/lib/tng/services/file_type_detector.rb +50 -14
- data/lib/tng/services/test_generator.rb +67 -78
- data/lib/tng/ui/go_ui_session.rb +4 -4
- data/lib/tng/ui/post_install_box.rb +12 -12
- data/lib/tng/ui/theme.rb +22 -0
- data/lib/tng/utils.rb +63 -0
- data/lib/tng/version.rb +1 -1
- data/lib/tng.rb +19 -1
- metadata +5 -26
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8adc64b4a410ddec1b69ada11a09e9cee648f88692873ae6551990753ba0f564
|
|
4
|
+
data.tar.gz: aa5c284afe9a769733b3ab87d37c1c51f23bf07ee4b3ffdfac65f60fe39f3548
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
####
|
|
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
|
|
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
|
|
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
|
-
#
|
|
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 "
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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
|
-
|
|
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
|
|
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: "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/binaries/go-ui-darwin-amd64
CHANGED
|
Binary file
|
data/binaries/go-ui-darwin-arm64
CHANGED
|
Binary file
|
data/binaries/go-ui-linux-amd64
CHANGED
|
Binary file
|
data/binaries/go-ui-linux-arm64
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
data/binaries/tng-linux-arm64.so
CHANGED
|
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
|
{
|
data/lib/tng/analyzers/model.rb
CHANGED
|
@@ -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 =
|
|
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)
|