t-ruby 0.0.7 → 0.0.11
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 +6 -2
- data/lib/t_ruby/cli.rb +71 -2
- data/lib/t_ruby/compiler.rb +87 -4
- data/lib/t_ruby/config.rb +3 -3
- data/lib/t_ruby/docs_badge_generator.rb +192 -0
- data/lib/t_ruby/docs_example_extractor.rb +156 -0
- data/lib/t_ruby/docs_example_verifier.rb +222 -0
- data/lib/t_ruby/error_handler.rb +191 -13
- data/lib/t_ruby/parser.rb +33 -0
- data/lib/t_ruby/version.rb +1 -1
- data/lib/t_ruby/watcher.rb +27 -7
- data/lib/t_ruby.rb +5 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f72a8bc0185eae719b855a9edb95a6fa17f9d9f3b4dfadf67f5a32b0a6ab0e32
|
|
4
|
+
data.tar.gz: 376722902f19f751b420e6e6e9c7951aebf45bc76c081c498d8ba4d92e7383f8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d742076e86ec5f1fdcd4bac9fe98b2fff48ed3fc964caa420f11e1d88263db26e55768e5fa6f00f1a976712d3aaf4cb6d541b3c79df863550c6f84eb2fba481b
|
|
7
|
+
data.tar.gz: ee975f1c9b0c70254f96082178734c8ae1f799565d193425a80d416bb60f088a4ccf41516b878d35a1c257ec7557c47220c65045b2d5099bfbb855446fb513f5
|
data/README.md
CHANGED
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
<img src="https://img.shields.io/badge/coverage-90%25-brightgreen" alt="Coverage: 90%" />
|
|
17
17
|
</p>
|
|
18
18
|
|
|
19
|
+
<p align="center">
|
|
20
|
+
<a href="https://type-ruby.github.io"><strong>🌐 Official Website</strong></a>
|
|
21
|
+
</p>
|
|
22
|
+
|
|
19
23
|
<p align="center">
|
|
20
24
|
<a href="#install">Install</a>
|
|
21
25
|
•
|
|
@@ -106,7 +110,7 @@ end
|
|
|
106
110
|
- Like TypeScript, types live inside your code.
|
|
107
111
|
- Write `.trb`, and `trc` generates both `.rb` and `.rbs`.
|
|
108
112
|
|
|
109
|
-
```
|
|
113
|
+
```trb
|
|
110
114
|
# greet.trb
|
|
111
115
|
def greet(name: String): String
|
|
112
116
|
"Hello, #{name}!"
|
|
@@ -150,7 +154,7 @@ trc --version
|
|
|
150
154
|
|
|
151
155
|
### 1. Write `.trb`
|
|
152
156
|
|
|
153
|
-
```
|
|
157
|
+
```trb
|
|
154
158
|
# hello.trb
|
|
155
159
|
def greet(name: String): String
|
|
156
160
|
"Hello, #{name}!"
|
data/lib/t_ruby/cli.rb
CHANGED
|
@@ -7,6 +7,8 @@ module TRuby
|
|
|
7
7
|
|
|
8
8
|
Usage:
|
|
9
9
|
trc <file.trb> Compile a .trb file to .rb
|
|
10
|
+
trc <file.rb> Copy .rb file to build/ and generate .rbs
|
|
11
|
+
trc --init Initialize a new t-ruby project
|
|
10
12
|
trc --watch, -w Watch input files and recompile on change
|
|
11
13
|
trc --decl <file.trb> Generate .d.trb declaration file
|
|
12
14
|
trc --lsp Start LSP server (for IDE integration)
|
|
@@ -15,8 +17,10 @@ module TRuby
|
|
|
15
17
|
|
|
16
18
|
Examples:
|
|
17
19
|
trc hello.trb Compile hello.trb to build/hello.rb
|
|
18
|
-
trc
|
|
19
|
-
trc
|
|
20
|
+
trc utils.rb Copy utils.rb to build/ and generate utils.rbs
|
|
21
|
+
trc --init Create trbconfig.yml and src/, build/ directories
|
|
22
|
+
trc -w Watch all .trb and .rb files in current directory
|
|
23
|
+
trc -w src/ Watch all .trb and .rb files in src/ directory
|
|
20
24
|
trc --watch hello.trb Watch specific file for changes
|
|
21
25
|
trc --decl hello.trb Generate hello.d.trb declaration file
|
|
22
26
|
trc --lsp Start language server for VS Code
|
|
@@ -41,6 +45,11 @@ module TRuby
|
|
|
41
45
|
return
|
|
42
46
|
end
|
|
43
47
|
|
|
48
|
+
if @args.include?("--init")
|
|
49
|
+
init_project
|
|
50
|
+
return
|
|
51
|
+
end
|
|
52
|
+
|
|
44
53
|
if @args.include?("--lsp")
|
|
45
54
|
start_lsp_server
|
|
46
55
|
return
|
|
@@ -63,6 +72,66 @@ module TRuby
|
|
|
63
72
|
|
|
64
73
|
private
|
|
65
74
|
|
|
75
|
+
def init_project
|
|
76
|
+
config_file = "trbconfig.yml"
|
|
77
|
+
src_dir = "src"
|
|
78
|
+
build_dir = "build"
|
|
79
|
+
|
|
80
|
+
created = []
|
|
81
|
+
skipped = []
|
|
82
|
+
|
|
83
|
+
# Create trbconfig.yml
|
|
84
|
+
if File.exist?(config_file)
|
|
85
|
+
skipped << config_file
|
|
86
|
+
else
|
|
87
|
+
File.write(config_file, <<~YAML)
|
|
88
|
+
emit:
|
|
89
|
+
rb: true
|
|
90
|
+
rbs: true
|
|
91
|
+
dtrb: false
|
|
92
|
+
|
|
93
|
+
paths:
|
|
94
|
+
src: "./#{src_dir}"
|
|
95
|
+
out: "./#{build_dir}"
|
|
96
|
+
|
|
97
|
+
strict:
|
|
98
|
+
rbs_compat: true
|
|
99
|
+
null_safety: false
|
|
100
|
+
inference: basic
|
|
101
|
+
YAML
|
|
102
|
+
created << config_file
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Create src/ directory
|
|
106
|
+
if Dir.exist?(src_dir)
|
|
107
|
+
skipped << "#{src_dir}/"
|
|
108
|
+
else
|
|
109
|
+
Dir.mkdir(src_dir)
|
|
110
|
+
created << "#{src_dir}/"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Create build/ directory
|
|
114
|
+
if Dir.exist?(build_dir)
|
|
115
|
+
skipped << "#{build_dir}/"
|
|
116
|
+
else
|
|
117
|
+
Dir.mkdir(build_dir)
|
|
118
|
+
created << "#{build_dir}/"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Output results
|
|
122
|
+
if created.any?
|
|
123
|
+
puts "Created: #{created.join(', ')}"
|
|
124
|
+
end
|
|
125
|
+
if skipped.any?
|
|
126
|
+
puts "Skipped (already exists): #{skipped.join(', ')}"
|
|
127
|
+
end
|
|
128
|
+
if created.empty? && skipped.any?
|
|
129
|
+
puts "Project already initialized."
|
|
130
|
+
else
|
|
131
|
+
puts "t-ruby project initialized successfully!"
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
66
135
|
def start_lsp_server
|
|
67
136
|
server = LSPServer.new
|
|
68
137
|
server.run
|
data/lib/t_ruby/compiler.rb
CHANGED
|
@@ -6,13 +6,13 @@ module TRuby
|
|
|
6
6
|
class Compiler
|
|
7
7
|
attr_reader :declaration_loader, :use_ir, :optimizer
|
|
8
8
|
|
|
9
|
-
def initialize(config, use_ir: true, optimize: true)
|
|
10
|
-
@config = config
|
|
9
|
+
def initialize(config = nil, use_ir: true, optimize: true)
|
|
10
|
+
@config = config || Config.new
|
|
11
11
|
@use_ir = use_ir
|
|
12
12
|
@optimize = optimize
|
|
13
13
|
@declaration_loader = DeclarationLoader.new
|
|
14
14
|
@optimizer = IR::Optimizer.new if use_ir && optimize
|
|
15
|
-
setup_declaration_paths
|
|
15
|
+
setup_declaration_paths if @config
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def compile(input_path)
|
|
@@ -20,8 +20,13 @@ module TRuby
|
|
|
20
20
|
raise ArgumentError, "File not found: #{input_path}"
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
# Handle .rb files separately
|
|
24
|
+
if input_path.end_with?(".rb")
|
|
25
|
+
return copy_ruby_file(input_path)
|
|
26
|
+
end
|
|
27
|
+
|
|
23
28
|
unless input_path.end_with?(".trb")
|
|
24
|
-
raise ArgumentError, "Expected .trb file, got: #{input_path}"
|
|
29
|
+
raise ArgumentError, "Expected .trb or .rb file, got: #{input_path}"
|
|
25
30
|
end
|
|
26
31
|
|
|
27
32
|
source = File.read(input_path)
|
|
@@ -58,6 +63,54 @@ module TRuby
|
|
|
58
63
|
output_path
|
|
59
64
|
end
|
|
60
65
|
|
|
66
|
+
# Compile T-Ruby source code from a string (useful for WASM/playground)
|
|
67
|
+
# @param source [String] T-Ruby source code
|
|
68
|
+
# @param options [Hash] Options for compilation
|
|
69
|
+
# @option options [Boolean] :rbs Whether to generate RBS output (default: true)
|
|
70
|
+
# @return [Hash] Result with :ruby, :rbs, :errors keys
|
|
71
|
+
def compile_string(source, options = {})
|
|
72
|
+
generate_rbs = options.fetch(:rbs, true)
|
|
73
|
+
|
|
74
|
+
parser = Parser.new(source, use_combinator: @use_ir)
|
|
75
|
+
parse_result = parser.parse
|
|
76
|
+
|
|
77
|
+
# Transform source to Ruby code
|
|
78
|
+
ruby_output = @use_ir ? transform_with_ir(source, parser) : transform_legacy(source, parse_result)
|
|
79
|
+
|
|
80
|
+
# Generate RBS if requested
|
|
81
|
+
rbs_output = ""
|
|
82
|
+
if generate_rbs
|
|
83
|
+
if @use_ir && parser.ir_program
|
|
84
|
+
generator = IR::RBSGenerator.new
|
|
85
|
+
rbs_output = generator.generate(parser.ir_program)
|
|
86
|
+
else
|
|
87
|
+
generator = RBSGenerator.new
|
|
88
|
+
rbs_output = generator.generate(
|
|
89
|
+
parse_result[:functions] || [],
|
|
90
|
+
parse_result[:type_aliases] || []
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
{
|
|
96
|
+
ruby: ruby_output,
|
|
97
|
+
rbs: rbs_output,
|
|
98
|
+
errors: []
|
|
99
|
+
}
|
|
100
|
+
rescue ParseError => e
|
|
101
|
+
{
|
|
102
|
+
ruby: "",
|
|
103
|
+
rbs: "",
|
|
104
|
+
errors: [e.message]
|
|
105
|
+
}
|
|
106
|
+
rescue StandardError => e
|
|
107
|
+
{
|
|
108
|
+
ruby: "",
|
|
109
|
+
rbs: "",
|
|
110
|
+
errors: ["Compilation error: #{e.message}"]
|
|
111
|
+
}
|
|
112
|
+
end
|
|
113
|
+
|
|
61
114
|
# Compile to IR without generating output files
|
|
62
115
|
def compile_to_ir(input_path)
|
|
63
116
|
unless File.exist?(input_path)
|
|
@@ -166,6 +219,36 @@ module TRuby
|
|
|
166
219
|
generator = DeclarationGenerator.new
|
|
167
220
|
generator.generate_file(input_path, out_dir)
|
|
168
221
|
end
|
|
222
|
+
|
|
223
|
+
# Copy .rb file to output directory and generate .rbs signature
|
|
224
|
+
def copy_ruby_file(input_path)
|
|
225
|
+
unless File.exist?(input_path)
|
|
226
|
+
raise ArgumentError, "File not found: #{input_path}"
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
out_dir = @config.out_dir
|
|
230
|
+
FileUtils.mkdir_p(out_dir)
|
|
231
|
+
|
|
232
|
+
base_filename = File.basename(input_path, ".rb")
|
|
233
|
+
output_path = File.join(out_dir, base_filename + ".rb")
|
|
234
|
+
|
|
235
|
+
# Copy the .rb file to output directory
|
|
236
|
+
FileUtils.cp(input_path, output_path)
|
|
237
|
+
|
|
238
|
+
# Generate .rbs file if enabled in config
|
|
239
|
+
if @config.emit["rbs"]
|
|
240
|
+
generate_rbs_from_ruby(base_filename, out_dir, input_path)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
output_path
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Generate RBS from Ruby file using rbs prototype
|
|
247
|
+
def generate_rbs_from_ruby(base_filename, out_dir, input_path)
|
|
248
|
+
rbs_path = File.join(out_dir, base_filename + ".rbs")
|
|
249
|
+
result = `rbs prototype rb #{input_path} 2>/dev/null`
|
|
250
|
+
File.write(rbs_path, result) unless result.strip.empty?
|
|
251
|
+
end
|
|
169
252
|
end
|
|
170
253
|
|
|
171
254
|
# IR-aware code generator for source-preserving transformation
|
data/lib/t_ruby/config.rb
CHANGED
|
@@ -7,7 +7,7 @@ module TRuby
|
|
|
7
7
|
DEFAULT_CONFIG = {
|
|
8
8
|
"emit" => {
|
|
9
9
|
"rb" => true,
|
|
10
|
-
"rbs" =>
|
|
10
|
+
"rbs" => true,
|
|
11
11
|
"dtrb" => false
|
|
12
12
|
},
|
|
13
13
|
"paths" => {
|
|
@@ -43,8 +43,8 @@ module TRuby
|
|
|
43
43
|
def load_config(config_path)
|
|
44
44
|
if config_path && File.exist?(config_path)
|
|
45
45
|
YAML.safe_load_file(config_path, permitted_classes: [Symbol])
|
|
46
|
-
elsif File.exist?(".
|
|
47
|
-
YAML.safe_load_file(".
|
|
46
|
+
elsif File.exist?("trbconfig.yml")
|
|
47
|
+
YAML.safe_load_file("trbconfig.yml", permitted_classes: [Symbol])
|
|
48
48
|
else
|
|
49
49
|
DEFAULT_CONFIG.dup
|
|
50
50
|
end
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "docs_example_verifier"
|
|
4
|
+
|
|
5
|
+
module TRuby
|
|
6
|
+
# Generates badges and reports for documentation verification results.
|
|
7
|
+
#
|
|
8
|
+
# Supports:
|
|
9
|
+
# - Shields.io compatible JSON badges
|
|
10
|
+
# - SVG badge generation
|
|
11
|
+
# - Markdown report generation
|
|
12
|
+
# - JSON report generation
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# generator = DocsBadgeGenerator.new
|
|
16
|
+
# verifier = DocsExampleVerifier.new
|
|
17
|
+
# results = verifier.verify_glob("docs/**/*.md")
|
|
18
|
+
# generator.generate_badge(results, "coverage/docs_badge.json")
|
|
19
|
+
#
|
|
20
|
+
class DocsBadgeGenerator
|
|
21
|
+
# Badge colors based on pass rate
|
|
22
|
+
COLORS = {
|
|
23
|
+
excellent: "brightgreen", # 95-100%
|
|
24
|
+
good: "green", # 80-94%
|
|
25
|
+
fair: "yellow", # 60-79%
|
|
26
|
+
poor: "orange", # 40-59%
|
|
27
|
+
critical: "red", # 0-39%
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
def initialize
|
|
31
|
+
@verifier = DocsExampleVerifier.new
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Generate all outputs
|
|
35
|
+
#
|
|
36
|
+
# @param results [Array<DocsExampleVerifier::VerificationResult>] Results
|
|
37
|
+
# @param output_dir [String] Output directory
|
|
38
|
+
def generate_all(results, output_dir)
|
|
39
|
+
FileUtils.mkdir_p(output_dir)
|
|
40
|
+
|
|
41
|
+
generate_badge_json(results, File.join(output_dir, "docs_badge.json"))
|
|
42
|
+
generate_badge_svg(results, File.join(output_dir, "docs_badge.svg"))
|
|
43
|
+
generate_report_json(results, File.join(output_dir, "docs_report.json"))
|
|
44
|
+
generate_report_markdown(results, File.join(output_dir, "docs_report.md"))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Generate Shields.io compatible JSON badge
|
|
48
|
+
#
|
|
49
|
+
# @param results [Array<DocsExampleVerifier::VerificationResult>] Results
|
|
50
|
+
# @param output_path [String] Output file path
|
|
51
|
+
def generate_badge_json(results, output_path)
|
|
52
|
+
summary = @verifier.summary(results)
|
|
53
|
+
pass_rate = summary[:pass_rate]
|
|
54
|
+
|
|
55
|
+
badge = {
|
|
56
|
+
schemaVersion: 1,
|
|
57
|
+
label: "docs examples",
|
|
58
|
+
message: "#{pass_rate}%",
|
|
59
|
+
color: color_for_rate(pass_rate),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
File.write(output_path, JSON.pretty_generate(badge))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Generate SVG badge
|
|
66
|
+
#
|
|
67
|
+
# @param results [Array<DocsExampleVerifier::VerificationResult>] Results
|
|
68
|
+
# @param output_path [String] Output file path
|
|
69
|
+
def generate_badge_svg(results, output_path)
|
|
70
|
+
summary = @verifier.summary(results)
|
|
71
|
+
pass_rate = summary[:pass_rate]
|
|
72
|
+
color = svg_color_for_rate(pass_rate)
|
|
73
|
+
|
|
74
|
+
svg = <<~SVG
|
|
75
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="140" height="20">
|
|
76
|
+
<linearGradient id="b" x2="0" y2="100%">
|
|
77
|
+
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
|
78
|
+
<stop offset="1" stop-opacity=".1"/>
|
|
79
|
+
</linearGradient>
|
|
80
|
+
<mask id="a">
|
|
81
|
+
<rect width="140" height="20" rx="3" fill="#fff"/>
|
|
82
|
+
</mask>
|
|
83
|
+
<g mask="url(#a)">
|
|
84
|
+
<path fill="#555" d="M0 0h85v20H0z"/>
|
|
85
|
+
<path fill="#{color}" d="M85 0h55v20H85z"/>
|
|
86
|
+
<path fill="url(#b)" d="M0 0h140v20H0z"/>
|
|
87
|
+
</g>
|
|
88
|
+
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
|
|
89
|
+
<text x="42.5" y="15" fill="#010101" fill-opacity=".3">docs examples</text>
|
|
90
|
+
<text x="42.5" y="14">docs examples</text>
|
|
91
|
+
<text x="112" y="15" fill="#010101" fill-opacity=".3">#{pass_rate}%</text>
|
|
92
|
+
<text x="112" y="14">#{pass_rate}%</text>
|
|
93
|
+
</g>
|
|
94
|
+
</svg>
|
|
95
|
+
SVG
|
|
96
|
+
|
|
97
|
+
File.write(output_path, svg)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Generate JSON report
|
|
101
|
+
#
|
|
102
|
+
# @param results [Array<DocsExampleVerifier::VerificationResult>] Results
|
|
103
|
+
# @param output_path [String] Output file path
|
|
104
|
+
def generate_report_json(results, output_path)
|
|
105
|
+
summary = @verifier.summary(results)
|
|
106
|
+
|
|
107
|
+
report = {
|
|
108
|
+
generated_at: Time.now.iso8601,
|
|
109
|
+
summary: summary,
|
|
110
|
+
files: group_results_by_file(results),
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
File.write(output_path, JSON.pretty_generate(report))
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Generate Markdown report
|
|
117
|
+
#
|
|
118
|
+
# @param results [Array<DocsExampleVerifier::VerificationResult>] Results
|
|
119
|
+
# @param output_path [String] Output file path
|
|
120
|
+
def generate_report_markdown(results, output_path)
|
|
121
|
+
summary = @verifier.summary(results)
|
|
122
|
+
grouped = group_results_by_file(results)
|
|
123
|
+
|
|
124
|
+
markdown = <<~MD
|
|
125
|
+
# Documentation Examples Verification Report
|
|
126
|
+
|
|
127
|
+
Generated: #{Time.now.strftime("%Y-%m-%d %H:%M:%S")}
|
|
128
|
+
|
|
129
|
+
## Summary
|
|
130
|
+
|
|
131
|
+
| Metric | Value |
|
|
132
|
+
|--------|-------|
|
|
133
|
+
| Total Examples | #{summary[:total]} |
|
|
134
|
+
| Passed | #{summary[:passed]} |
|
|
135
|
+
| Failed | #{summary[:failed]} |
|
|
136
|
+
| Skipped | #{summary[:skipped]} |
|
|
137
|
+
| **Pass Rate** | **#{summary[:pass_rate]}%** |
|
|
138
|
+
|
|
139
|
+
## Results by File
|
|
140
|
+
|
|
141
|
+
MD
|
|
142
|
+
|
|
143
|
+
grouped.each do |file_path, file_results|
|
|
144
|
+
file_summary = @verifier.summary(file_results)
|
|
145
|
+
status_emoji = file_summary[:failed].zero? ? "✅" : "❌"
|
|
146
|
+
|
|
147
|
+
markdown += "### #{status_emoji} #{file_path}\n\n"
|
|
148
|
+
markdown += "Pass rate: #{file_summary[:pass_rate]}% (#{file_summary[:passed]}/#{file_summary[:total]})\n\n"
|
|
149
|
+
|
|
150
|
+
failed_results = file_results.select(&:fail?)
|
|
151
|
+
if failed_results.any?
|
|
152
|
+
markdown += "**Failed examples:**\n\n"
|
|
153
|
+
failed_results.each do |result|
|
|
154
|
+
markdown += "- Line #{result.line_number}:\n"
|
|
155
|
+
result.errors.each do |error|
|
|
156
|
+
markdown += " - #{error}\n"
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
markdown += "\n"
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
File.write(output_path, markdown)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
private
|
|
167
|
+
|
|
168
|
+
def color_for_rate(rate)
|
|
169
|
+
case rate
|
|
170
|
+
when 95..100 then COLORS[:excellent]
|
|
171
|
+
when 80...95 then COLORS[:good]
|
|
172
|
+
when 60...80 then COLORS[:fair]
|
|
173
|
+
when 40...60 then COLORS[:poor]
|
|
174
|
+
else COLORS[:critical]
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def svg_color_for_rate(rate)
|
|
179
|
+
case rate
|
|
180
|
+
when 95..100 then "#4c1" # bright green
|
|
181
|
+
when 80...95 then "#97ca00" # green
|
|
182
|
+
when 60...80 then "#dfb317" # yellow
|
|
183
|
+
when 40...60 then "#fe7d37" # orange
|
|
184
|
+
else "#e05d44" # red
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def group_results_by_file(results)
|
|
189
|
+
results.group_by(&:file_path)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TRuby
|
|
4
|
+
# Extracts code examples from Markdown documentation files.
|
|
5
|
+
#
|
|
6
|
+
# Supports extracting:
|
|
7
|
+
# - T-Ruby code blocks (```trb, ```t-ruby, ```ruby with type annotations)
|
|
8
|
+
# - Ruby code blocks for comparison
|
|
9
|
+
# - RBS type definitions
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# extractor = DocsExampleExtractor.new
|
|
13
|
+
# examples = extractor.extract_from_file("docs/getting-started.md")
|
|
14
|
+
# examples.each { |ex| puts ex.code }
|
|
15
|
+
#
|
|
16
|
+
class DocsExampleExtractor
|
|
17
|
+
# Represents an extracted code example
|
|
18
|
+
CodeExample = Struct.new(
|
|
19
|
+
:code, # The code content
|
|
20
|
+
:language, # Language identifier (trb, ruby, rbs)
|
|
21
|
+
:file_path, # Source file path
|
|
22
|
+
:line_number, # Starting line number
|
|
23
|
+
:metadata, # Optional metadata from code fence
|
|
24
|
+
keyword_init: true
|
|
25
|
+
) do
|
|
26
|
+
def trb?
|
|
27
|
+
%w[trb t-ruby].include?(language)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def ruby?
|
|
31
|
+
language == "ruby"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def rbs?
|
|
35
|
+
language == "rbs"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def should_verify?
|
|
39
|
+
!metadata&.include?("skip-verify")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def should_compile?
|
|
43
|
+
!metadata&.include?("no-compile")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def should_typecheck?
|
|
47
|
+
!metadata&.include?("no-typecheck")
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Code fence pattern: ```language title="file.ext" {metadata}
|
|
52
|
+
# Supports Docusaurus format: ```ruby title="example.trb"
|
|
53
|
+
CODE_FENCE_PATTERN = /^```(\w+)?(?:\s+title="([^"]*)")?(?:\s*\{([^}]*)\})?/
|
|
54
|
+
|
|
55
|
+
# Extract all code examples from a file
|
|
56
|
+
#
|
|
57
|
+
# @param file_path [String] Path to the markdown file
|
|
58
|
+
# @return [Array<CodeExample>] Extracted code examples
|
|
59
|
+
def extract_from_file(file_path)
|
|
60
|
+
content = File.read(file_path, encoding: "UTF-8")
|
|
61
|
+
extract_from_content(content, file_path)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Extract all code examples from content
|
|
65
|
+
#
|
|
66
|
+
# @param content [String] Markdown content
|
|
67
|
+
# @param file_path [String] Source file path (for reference)
|
|
68
|
+
# @return [Array<CodeExample>] Extracted code examples
|
|
69
|
+
def extract_from_content(content, file_path = "<string>")
|
|
70
|
+
examples = []
|
|
71
|
+
lines = content.lines
|
|
72
|
+
in_code_block = false
|
|
73
|
+
current_block = nil
|
|
74
|
+
block_start_line = 0
|
|
75
|
+
|
|
76
|
+
lines.each_with_index do |line, index|
|
|
77
|
+
line_number = index + 1
|
|
78
|
+
|
|
79
|
+
if !in_code_block && (match = line.match(CODE_FENCE_PATTERN))
|
|
80
|
+
in_code_block = true
|
|
81
|
+
block_start_line = line_number
|
|
82
|
+
lang = match[1] || "text"
|
|
83
|
+
title = match[2]
|
|
84
|
+
metadata = match[3]
|
|
85
|
+
|
|
86
|
+
# If title ends with .trb, treat as T-Ruby regardless of language tag
|
|
87
|
+
if title&.end_with?(".trb")
|
|
88
|
+
lang = "trb"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
current_block = {
|
|
92
|
+
language: lang,
|
|
93
|
+
metadata: metadata,
|
|
94
|
+
title: title,
|
|
95
|
+
lines: [],
|
|
96
|
+
}
|
|
97
|
+
elsif in_code_block && line.match(/^```\s*$/)
|
|
98
|
+
in_code_block = false
|
|
99
|
+
|
|
100
|
+
# Only include relevant languages
|
|
101
|
+
if relevant_language?(current_block[:language])
|
|
102
|
+
examples << CodeExample.new(
|
|
103
|
+
code: current_block[:lines].join,
|
|
104
|
+
language: normalize_language(current_block[:language]),
|
|
105
|
+
file_path: file_path,
|
|
106
|
+
line_number: block_start_line,
|
|
107
|
+
metadata: current_block[:metadata]
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
current_block = nil
|
|
112
|
+
elsif in_code_block
|
|
113
|
+
current_block[:lines] << line
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
examples
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Extract from multiple files using glob pattern
|
|
121
|
+
#
|
|
122
|
+
# @param pattern [String] Glob pattern (e.g., "docs/**/*.md")
|
|
123
|
+
# @return [Array<CodeExample>] All extracted examples
|
|
124
|
+
def extract_from_glob(pattern)
|
|
125
|
+
Dir.glob(pattern).flat_map { |file| extract_from_file(file) }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Get statistics about extracted examples
|
|
129
|
+
#
|
|
130
|
+
# @param examples [Array<CodeExample>] Code examples
|
|
131
|
+
# @return [Hash] Statistics
|
|
132
|
+
def statistics(examples)
|
|
133
|
+
{
|
|
134
|
+
total: examples.size,
|
|
135
|
+
trb: examples.count(&:trb?),
|
|
136
|
+
ruby: examples.count(&:ruby?),
|
|
137
|
+
rbs: examples.count(&:rbs?),
|
|
138
|
+
verifiable: examples.count(&:should_verify?),
|
|
139
|
+
files: examples.map(&:file_path).uniq.size,
|
|
140
|
+
}
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
def relevant_language?(lang)
|
|
146
|
+
%w[trb t-ruby ruby rbs].include?(lang&.downcase)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def normalize_language(lang)
|
|
150
|
+
case lang&.downcase
|
|
151
|
+
when "t-ruby" then "trb"
|
|
152
|
+
else lang&.downcase || "text"
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "docs_example_extractor"
|
|
4
|
+
|
|
5
|
+
module TRuby
|
|
6
|
+
# Verifies code examples extracted from documentation.
|
|
7
|
+
#
|
|
8
|
+
# Performs:
|
|
9
|
+
# - Syntax validation (parsing)
|
|
10
|
+
# - Type checking (for .trb examples)
|
|
11
|
+
# - Compilation (generates Ruby output)
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# verifier = DocsExampleVerifier.new
|
|
15
|
+
# results = verifier.verify_file("docs/getting-started.md")
|
|
16
|
+
# results.each { |r| puts "#{r.status}: #{r.file_path}:#{r.line_number}" }
|
|
17
|
+
#
|
|
18
|
+
class DocsExampleVerifier
|
|
19
|
+
# Result of verifying a single example
|
|
20
|
+
VerificationResult = Struct.new(
|
|
21
|
+
:example, # The original CodeExample
|
|
22
|
+
:status, # :pass, :fail, :skip
|
|
23
|
+
:errors, # Array of error messages
|
|
24
|
+
:output, # Compiled output (if applicable)
|
|
25
|
+
keyword_init: true
|
|
26
|
+
) do
|
|
27
|
+
def pass?
|
|
28
|
+
status == :pass
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def fail?
|
|
32
|
+
status == :fail
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def skip?
|
|
36
|
+
status == :skip
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def file_path
|
|
40
|
+
example.file_path
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def line_number
|
|
44
|
+
example.line_number
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def initialize
|
|
49
|
+
@extractor = DocsExampleExtractor.new
|
|
50
|
+
@compiler = TRuby::Compiler.new
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Verify all examples in a file
|
|
54
|
+
#
|
|
55
|
+
# @param file_path [String] Path to the markdown file
|
|
56
|
+
# @return [Array<VerificationResult>] Results for each example
|
|
57
|
+
def verify_file(file_path)
|
|
58
|
+
examples = @extractor.extract_from_file(file_path)
|
|
59
|
+
examples.map { |example| verify_example(example) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Verify all examples from multiple files
|
|
63
|
+
#
|
|
64
|
+
# @param pattern [String] Glob pattern
|
|
65
|
+
# @return [Array<VerificationResult>] All results
|
|
66
|
+
def verify_glob(pattern)
|
|
67
|
+
examples = @extractor.extract_from_glob(pattern)
|
|
68
|
+
examples.map { |example| verify_example(example) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Verify a single code example
|
|
72
|
+
#
|
|
73
|
+
# @param example [DocsExampleExtractor::CodeExample] The example to verify
|
|
74
|
+
# @return [VerificationResult] The verification result
|
|
75
|
+
def verify_example(example)
|
|
76
|
+
return skip_result(example, "Marked as skip-verify") unless example.should_verify?
|
|
77
|
+
|
|
78
|
+
case example.language
|
|
79
|
+
when "trb"
|
|
80
|
+
verify_trb_example(example)
|
|
81
|
+
when "ruby"
|
|
82
|
+
verify_ruby_example(example)
|
|
83
|
+
when "rbs"
|
|
84
|
+
verify_rbs_example(example)
|
|
85
|
+
else
|
|
86
|
+
skip_result(example, "Unknown language: #{example.language}")
|
|
87
|
+
end
|
|
88
|
+
rescue StandardError => e
|
|
89
|
+
fail_result(example, ["Exception: #{e.message}"])
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Generate a summary report
|
|
93
|
+
#
|
|
94
|
+
# @param results [Array<VerificationResult>] Verification results
|
|
95
|
+
# @return [Hash] Summary statistics
|
|
96
|
+
def summary(results)
|
|
97
|
+
{
|
|
98
|
+
total: results.size,
|
|
99
|
+
passed: results.count(&:pass?),
|
|
100
|
+
failed: results.count(&:fail?),
|
|
101
|
+
skipped: results.count(&:skip?),
|
|
102
|
+
pass_rate: results.empty? ? 0 : (results.count(&:pass?).to_f / results.size * 100).round(2),
|
|
103
|
+
}
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Print results to stdout
|
|
107
|
+
#
|
|
108
|
+
# @param results [Array<VerificationResult>] Verification results
|
|
109
|
+
# @param verbose [Boolean] Show passing tests too
|
|
110
|
+
def print_results(results, verbose: false)
|
|
111
|
+
results.each do |result|
|
|
112
|
+
next if result.pass? && !verbose
|
|
113
|
+
|
|
114
|
+
status_icon = case result.status
|
|
115
|
+
when :pass then "\e[32m✓\e[0m"
|
|
116
|
+
when :fail then "\e[31m✗\e[0m"
|
|
117
|
+
when :skip then "\e[33m○\e[0m"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
puts "#{status_icon} #{result.file_path}:#{result.line_number}"
|
|
121
|
+
|
|
122
|
+
result.errors&.each do |error|
|
|
123
|
+
puts " #{error}"
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
summary_data = summary(results)
|
|
128
|
+
puts
|
|
129
|
+
puts "Results: #{summary_data[:passed]} passed, #{summary_data[:failed]} failed, #{summary_data[:skipped]} skipped"
|
|
130
|
+
puts "Pass rate: #{summary_data[:pass_rate]}%"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
private
|
|
134
|
+
|
|
135
|
+
def verify_trb_example(example)
|
|
136
|
+
errors = []
|
|
137
|
+
|
|
138
|
+
# Step 1: Parse
|
|
139
|
+
ir_program = nil
|
|
140
|
+
begin
|
|
141
|
+
parser = TRuby::Parser.new(example.code)
|
|
142
|
+
parser.parse
|
|
143
|
+
ir_program = parser.ir_program
|
|
144
|
+
rescue TRuby::ParseError => e
|
|
145
|
+
return fail_result(example, ["Parse error: #{e.message}"])
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Step 2: Type check (if enabled)
|
|
149
|
+
if example.should_typecheck? && ir_program
|
|
150
|
+
begin
|
|
151
|
+
type_checker = TRuby::TypeChecker.new(use_smt: false)
|
|
152
|
+
result = type_checker.check_program(ir_program)
|
|
153
|
+
if result[:errors]&.any?
|
|
154
|
+
errors.concat(result[:errors].map { |e| "Type error: #{e}" })
|
|
155
|
+
end
|
|
156
|
+
rescue StandardError => e
|
|
157
|
+
errors << "Type check error: #{e.message}"
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Step 3: Compile (if enabled)
|
|
162
|
+
output = nil
|
|
163
|
+
if example.should_compile?
|
|
164
|
+
begin
|
|
165
|
+
output = @compiler.compile_string(example.code)
|
|
166
|
+
rescue StandardError => e
|
|
167
|
+
errors << "Compile error: #{e.message}"
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
errors.empty? ? pass_result(example, output) : fail_result(example, errors)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def verify_ruby_example(example)
|
|
175
|
+
# For Ruby examples, just validate syntax
|
|
176
|
+
begin
|
|
177
|
+
RubyVM::InstructionSequence.compile(example.code)
|
|
178
|
+
pass_result(example)
|
|
179
|
+
rescue SyntaxError => e
|
|
180
|
+
fail_result(example, ["Ruby syntax error: #{e.message}"])
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def verify_rbs_example(example)
|
|
185
|
+
# For RBS, we just do basic validation
|
|
186
|
+
# Full RBS validation would require rbs gem
|
|
187
|
+
if example.code.include?("def ") || example.code.include?("type ") ||
|
|
188
|
+
example.code.include?("interface ") || example.code.include?("class ")
|
|
189
|
+
pass_result(example)
|
|
190
|
+
else
|
|
191
|
+
skip_result(example, "Cannot validate RBS without rbs gem")
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def pass_result(example, output = nil)
|
|
196
|
+
VerificationResult.new(
|
|
197
|
+
example: example,
|
|
198
|
+
status: :pass,
|
|
199
|
+
errors: [],
|
|
200
|
+
output: output
|
|
201
|
+
)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def fail_result(example, errors)
|
|
205
|
+
VerificationResult.new(
|
|
206
|
+
example: example,
|
|
207
|
+
status: :fail,
|
|
208
|
+
errors: errors,
|
|
209
|
+
output: nil
|
|
210
|
+
)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def skip_result(example, reason)
|
|
214
|
+
VerificationResult.new(
|
|
215
|
+
example: example,
|
|
216
|
+
status: :skip,
|
|
217
|
+
errors: [reason],
|
|
218
|
+
output: nil
|
|
219
|
+
)
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
data/lib/t_ruby/error_handler.rb
CHANGED
|
@@ -9,6 +9,7 @@ module TRuby
|
|
|
9
9
|
@lines = source.split("\n")
|
|
10
10
|
@errors = []
|
|
11
11
|
@functions = {}
|
|
12
|
+
@type_parser = ParserCombinator::TypeParser.new
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
def check
|
|
@@ -20,6 +21,7 @@ module TRuby
|
|
|
20
21
|
check_type_alias_errors
|
|
21
22
|
check_interface_errors
|
|
22
23
|
check_syntax_errors
|
|
24
|
+
check_method_signature_errors
|
|
23
25
|
check_type_validation
|
|
24
26
|
check_duplicate_definitions
|
|
25
27
|
|
|
@@ -78,20 +80,192 @@ module TRuby
|
|
|
78
80
|
end
|
|
79
81
|
end
|
|
80
82
|
|
|
83
|
+
# New comprehensive method signature validation
|
|
84
|
+
def check_method_signature_errors
|
|
85
|
+
@lines.each_with_index do |line, idx|
|
|
86
|
+
next unless line.match?(/^\s*def\s+/)
|
|
87
|
+
check_single_method_signature(line, idx)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def check_single_method_signature(line, idx)
|
|
92
|
+
# Pattern 1: Check for colon without type (e.g., "def test():")
|
|
93
|
+
if line.match?(/def\s+\w+[^:]*\)\s*:\s*$/)
|
|
94
|
+
@errors << "Line #{idx + 1}: Expected type after colon, but found end of line"
|
|
95
|
+
return
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Pattern 2: Check for text after closing paren without colon (e.g., "def test() something")
|
|
99
|
+
if match = line.match(/def\s+\w+\s*\([^)]*\)\s*([^:\s].+?)\s*$/)
|
|
100
|
+
trailing = match[1].strip
|
|
101
|
+
# Allow if it's just end-of-line content or a valid Ruby block start
|
|
102
|
+
unless trailing.empty? || trailing.start_with?("#") || trailing == "end"
|
|
103
|
+
@errors << "Line #{idx + 1}: Unexpected token '#{trailing}' after method parameters - did you forget ':'?"
|
|
104
|
+
end
|
|
105
|
+
return
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Pattern 3: Check for parameter with colon but no type (e.g., "def test(x:)")
|
|
109
|
+
if line.match?(/def\s+\w+\s*\([^)]*\w+:\s*[,)]/)
|
|
110
|
+
@errors << "Line #{idx + 1}: Expected type after parameter colon"
|
|
111
|
+
return
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Pattern 4: Extract and validate return type
|
|
115
|
+
if match = line.match(/def\s+\w+\s*\([^)]*\)\s*:\s*(.+?)\s*$/)
|
|
116
|
+
return_type_str = match[1].strip
|
|
117
|
+
validate_type_expression(return_type_str, idx, "return type")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Pattern 5: Extract and validate parameter types
|
|
121
|
+
if match = line.match(/def\s+\w+\s*\(([^)]+)\)/)
|
|
122
|
+
params_str = match[1]
|
|
123
|
+
validate_parameter_types_expression(params_str, idx)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def validate_type_expression(type_str, line_idx, context = "type")
|
|
128
|
+
return if type_str.nil? || type_str.empty?
|
|
129
|
+
|
|
130
|
+
# Check for whitespace in simple type names (e.g., "Str ing")
|
|
131
|
+
if type_str.match?(/^[A-Z][a-z]*\s+[a-z]+/)
|
|
132
|
+
@errors << "Line #{line_idx + 1}: Invalid #{context} '#{type_str}' - unexpected whitespace in type name"
|
|
133
|
+
return
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Check for trailing operators (e.g., "String |" or "String &")
|
|
137
|
+
if type_str.match?(/[|&]\s*$/)
|
|
138
|
+
@errors << "Line #{line_idx + 1}: Invalid #{context} '#{type_str}' - trailing operator"
|
|
139
|
+
return
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Check for leading operators
|
|
143
|
+
if type_str.match?(/^\s*[|&]/)
|
|
144
|
+
@errors << "Line #{line_idx + 1}: Invalid #{context} '#{type_str}' - leading operator"
|
|
145
|
+
return
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Check for double operators (e.g., "String | | Integer")
|
|
149
|
+
if type_str.match?(/[|&]\s*[|&]/)
|
|
150
|
+
@errors << "Line #{line_idx + 1}: Invalid #{context} '#{type_str}' - consecutive operators"
|
|
151
|
+
return
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Check for unclosed brackets
|
|
155
|
+
if type_str.count("<") != type_str.count(">")
|
|
156
|
+
@errors << "Line #{line_idx + 1}: Invalid #{context} '#{type_str}' - unbalanced angle brackets"
|
|
157
|
+
return
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
if type_str.count("[") != type_str.count("]")
|
|
161
|
+
@errors << "Line #{line_idx + 1}: Invalid #{context} '#{type_str}' - unbalanced square brackets"
|
|
162
|
+
return
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
if type_str.count("(") != type_str.count(")")
|
|
166
|
+
@errors << "Line #{line_idx + 1}: Invalid #{context} '#{type_str}' - unbalanced parentheses"
|
|
167
|
+
return
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Check for empty generic arguments (e.g., "Array<>")
|
|
171
|
+
if type_str.match?(/<\s*>/)
|
|
172
|
+
@errors << "Line #{line_idx + 1}: Invalid #{context} '#{type_str}' - empty generic arguments"
|
|
173
|
+
return
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Check for generic without base type (e.g., "<String>")
|
|
177
|
+
if type_str.match?(/^\s*</)
|
|
178
|
+
@errors << "Line #{line_idx + 1}: Invalid #{context} '#{type_str}' - missing base type for generic"
|
|
179
|
+
return
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Check for missing arrow target in function type
|
|
183
|
+
if type_str.match?(/->\s*$/)
|
|
184
|
+
@errors << "Line #{line_idx + 1}: Invalid #{context} '#{type_str}' - missing return type after ->"
|
|
185
|
+
return
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Check for extra tokens after valid type (e.g., "String something_else")
|
|
189
|
+
# Use TypeParser to validate
|
|
190
|
+
result = @type_parser.parse(type_str)
|
|
191
|
+
if result[:success]
|
|
192
|
+
remaining = type_str[result[:position] || 0..]&.strip
|
|
193
|
+
if remaining && !remaining.empty? && result[:remaining] && !result[:remaining].strip.empty?
|
|
194
|
+
@errors << "Line #{line_idx + 1}: Unexpected token after #{context} '#{type_str}'"
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def validate_parameter_types_expression(params_str, line_idx)
|
|
200
|
+
return if params_str.nil? || params_str.empty?
|
|
201
|
+
|
|
202
|
+
# Split parameters handling nested generics
|
|
203
|
+
params = split_parameters(params_str)
|
|
204
|
+
|
|
205
|
+
params.each do |param|
|
|
206
|
+
param = param.strip
|
|
207
|
+
next if param.empty?
|
|
208
|
+
|
|
209
|
+
# Check for param: Type pattern
|
|
210
|
+
if match = param.match(/^(\w+)\s*:\s*(.+)$/)
|
|
211
|
+
param_name = match[1]
|
|
212
|
+
type_str = match[2].strip
|
|
213
|
+
|
|
214
|
+
if type_str.empty?
|
|
215
|
+
@errors << "Line #{line_idx + 1}: Expected type after colon for parameter '#{param_name}'"
|
|
216
|
+
next
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
validate_type_expression(type_str, line_idx, "parameter type for '#{param_name}'")
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def split_parameters(params_str)
|
|
225
|
+
result = []
|
|
226
|
+
current = ""
|
|
227
|
+
depth = 0
|
|
228
|
+
|
|
229
|
+
params_str.each_char do |char|
|
|
230
|
+
case char
|
|
231
|
+
when "<", "[", "("
|
|
232
|
+
depth += 1
|
|
233
|
+
current += char
|
|
234
|
+
when ">", "]", ")"
|
|
235
|
+
depth -= 1
|
|
236
|
+
current += char
|
|
237
|
+
when ","
|
|
238
|
+
if depth == 0
|
|
239
|
+
result << current.strip
|
|
240
|
+
current = ""
|
|
241
|
+
else
|
|
242
|
+
current += char
|
|
243
|
+
end
|
|
244
|
+
else
|
|
245
|
+
current += char
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
result << current.strip unless current.empty?
|
|
250
|
+
result
|
|
251
|
+
end
|
|
252
|
+
|
|
81
253
|
def check_type_validation
|
|
82
254
|
@lines.each_with_index do |line, idx|
|
|
83
255
|
next unless line.match?(/^\s*def\s+/)
|
|
84
256
|
|
|
85
|
-
# Extract types from function definition
|
|
86
|
-
match = line.match(/def\s+\w+\s*\((.*?)\)\s*(?::\s*(
|
|
257
|
+
# Extract types from function definition - now handle complex types
|
|
258
|
+
match = line.match(/def\s+\w+\s*\((.*?)\)\s*(?::\s*(.+?))?$/)
|
|
87
259
|
next unless match
|
|
88
260
|
|
|
89
261
|
params_str = match[1]
|
|
90
|
-
return_type = match[2]
|
|
262
|
+
return_type = match[2]&.strip
|
|
91
263
|
|
|
92
|
-
# Check return type
|
|
93
|
-
if return_type &&
|
|
94
|
-
|
|
264
|
+
# Check return type if it's a simple type name
|
|
265
|
+
if return_type && return_type.match?(/^\w+$/)
|
|
266
|
+
unless VALID_TYPES.include?(return_type) || @type_aliases.key?(return_type)
|
|
267
|
+
@errors << "Line #{idx + 1}: Unknown return type '#{return_type}'"
|
|
268
|
+
end
|
|
95
269
|
end
|
|
96
270
|
|
|
97
271
|
# Check parameter types
|
|
@@ -100,18 +274,22 @@ module TRuby
|
|
|
100
274
|
end
|
|
101
275
|
|
|
102
276
|
def check_parameter_types(params_str, line_idx)
|
|
103
|
-
return if params_str.empty?
|
|
277
|
+
return if params_str.nil? || params_str.empty?
|
|
104
278
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
279
|
+
params = split_parameters(params_str)
|
|
280
|
+
params.each do |param|
|
|
281
|
+
param = param.strip
|
|
282
|
+
match = param.match(/^(\w+)(?::\s*(.+))?$/)
|
|
108
283
|
next unless match
|
|
109
284
|
|
|
110
|
-
param_type = match[2]
|
|
285
|
+
param_type = match[2]&.strip
|
|
111
286
|
next unless param_type
|
|
112
|
-
next if VALID_TYPES.include?(param_type) || @type_aliases.key?(param_type)
|
|
113
287
|
|
|
114
|
-
|
|
288
|
+
# Only check simple type names against VALID_TYPES
|
|
289
|
+
if param_type.match?(/^\w+$/)
|
|
290
|
+
next if VALID_TYPES.include?(param_type) || @type_aliases.key?(param_type)
|
|
291
|
+
@errors << "Line #{line_idx + 1}: Unknown parameter type '#{param_type}'"
|
|
292
|
+
end
|
|
115
293
|
end
|
|
116
294
|
end
|
|
117
295
|
|
data/lib/t_ruby/parser.rb
CHANGED
|
@@ -116,6 +116,11 @@ module TRuby
|
|
|
116
116
|
params_str = match[2]
|
|
117
117
|
return_type_str = match[3]&.strip
|
|
118
118
|
|
|
119
|
+
# Validate return type if present
|
|
120
|
+
if return_type_str
|
|
121
|
+
return_type_str = validate_and_extract_type(return_type_str)
|
|
122
|
+
end
|
|
123
|
+
|
|
119
124
|
params = parse_parameters(params_str)
|
|
120
125
|
|
|
121
126
|
result = {
|
|
@@ -133,6 +138,34 @@ module TRuby
|
|
|
133
138
|
result
|
|
134
139
|
end
|
|
135
140
|
|
|
141
|
+
# Validate type string and return nil if invalid
|
|
142
|
+
def validate_and_extract_type(type_str)
|
|
143
|
+
return nil if type_str.nil? || type_str.empty?
|
|
144
|
+
|
|
145
|
+
# Check for whitespace in simple type names that would be invalid
|
|
146
|
+
# Pattern: Capital letter followed by lowercase, then space, then more lowercase
|
|
147
|
+
# e.g., "Str ing", "Int eger", "Bool ean"
|
|
148
|
+
if type_str.match?(/^[A-Z][a-z]*\s+[a-z]+/)
|
|
149
|
+
return nil
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Check for trailing operators
|
|
153
|
+
return nil if type_str.match?(/[|&]\s*$/)
|
|
154
|
+
|
|
155
|
+
# Check for leading operators
|
|
156
|
+
return nil if type_str.match?(/^\s*[|&]/)
|
|
157
|
+
|
|
158
|
+
# Check for unbalanced brackets
|
|
159
|
+
return nil if type_str.count("<") != type_str.count(">")
|
|
160
|
+
return nil if type_str.count("[") != type_str.count("]")
|
|
161
|
+
return nil if type_str.count("(") != type_str.count(")")
|
|
162
|
+
|
|
163
|
+
# Check for empty generic arguments
|
|
164
|
+
return nil if type_str.match?(/<\s*>/)
|
|
165
|
+
|
|
166
|
+
type_str
|
|
167
|
+
end
|
|
168
|
+
|
|
136
169
|
def parse_parameters(params_str)
|
|
137
170
|
return [] if params_str.empty?
|
|
138
171
|
|
data/lib/t_ruby/version.rb
CHANGED
data/lib/t_ruby/watcher.rb
CHANGED
|
@@ -59,8 +59,8 @@ module TRuby
|
|
|
59
59
|
compile_all
|
|
60
60
|
@stats[:total_time] += Time.now - start_time
|
|
61
61
|
|
|
62
|
-
# Start watching
|
|
63
|
-
listener = Listen.to(*watch_directories, only: /\.trb$/) do |modified, added, removed|
|
|
62
|
+
# Start watching (.trb and .rb files)
|
|
63
|
+
listener = Listen.to(*watch_directories, only: /\.(trb|rb)$/) do |modified, added, removed|
|
|
64
64
|
handle_changes(modified, added, removed)
|
|
65
65
|
end
|
|
66
66
|
|
|
@@ -91,7 +91,7 @@ module TRuby
|
|
|
91
91
|
end
|
|
92
92
|
|
|
93
93
|
def handle_changes(modified, added, removed)
|
|
94
|
-
changed_files = (modified + added).select { |f| f.end_with?(".trb") }
|
|
94
|
+
changed_files = (modified + added).select { |f| f.end_with?(".trb") || f.end_with?(".rb") }
|
|
95
95
|
return if changed_files.empty? && removed.empty?
|
|
96
96
|
|
|
97
97
|
puts
|
|
@@ -120,7 +120,9 @@ module TRuby
|
|
|
120
120
|
errors = []
|
|
121
121
|
|
|
122
122
|
trb_files = find_trb_files
|
|
123
|
-
|
|
123
|
+
rb_files = find_rb_files
|
|
124
|
+
all_files = trb_files + rb_files
|
|
125
|
+
@file_count = all_files.size
|
|
124
126
|
|
|
125
127
|
if @incremental && @cross_file_check
|
|
126
128
|
# Use enhanced incremental compiler with cross-file checking
|
|
@@ -128,9 +130,15 @@ module TRuby
|
|
|
128
130
|
errors = result[:errors].map { |e| format_error(e[:file], e[:error] || e[:message]) }
|
|
129
131
|
@error_count = errors.size
|
|
130
132
|
@stats[:total_compilations] += trb_files.size
|
|
131
|
-
|
|
133
|
+
|
|
134
|
+
# Also compile .rb files
|
|
135
|
+
rb_files.each do |file|
|
|
136
|
+
result = compile_file(file)
|
|
137
|
+
errors.concat(result[:errors]) if result[:errors].any?
|
|
138
|
+
end
|
|
139
|
+
elsif @parallel && all_files.size > 1
|
|
132
140
|
# Parallel compilation
|
|
133
|
-
results = @parallel_processor.process_files(
|
|
141
|
+
results = @parallel_processor.process_files(all_files) do |file|
|
|
134
142
|
compile_file(file)
|
|
135
143
|
end
|
|
136
144
|
results.each do |result|
|
|
@@ -138,7 +146,7 @@ module TRuby
|
|
|
138
146
|
end
|
|
139
147
|
else
|
|
140
148
|
# Sequential compilation
|
|
141
|
-
|
|
149
|
+
all_files.each do |file|
|
|
142
150
|
result = compile_file(file)
|
|
143
151
|
errors.concat(result[:errors]) if result[:errors].any?
|
|
144
152
|
end
|
|
@@ -234,6 +242,18 @@ module TRuby
|
|
|
234
242
|
files.uniq
|
|
235
243
|
end
|
|
236
244
|
|
|
245
|
+
def find_rb_files
|
|
246
|
+
files = []
|
|
247
|
+
@paths.each do |path|
|
|
248
|
+
if File.directory?(path)
|
|
249
|
+
files.concat(Dir.glob(File.join(path, "**", "*.rb")))
|
|
250
|
+
elsif File.file?(path) && path.end_with?(".rb")
|
|
251
|
+
files << path
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
files.uniq
|
|
255
|
+
end
|
|
256
|
+
|
|
237
257
|
def format_error(file, message)
|
|
238
258
|
# Parse error message for line/column info if available
|
|
239
259
|
# Format: file:line:col - error TRB0001: message
|
data/lib/t_ruby.rb
CHANGED
|
@@ -38,5 +38,10 @@ require_relative "t_ruby/bundler_integration"
|
|
|
38
38
|
require_relative "t_ruby/benchmark"
|
|
39
39
|
require_relative "t_ruby/doc_generator"
|
|
40
40
|
|
|
41
|
+
# Milestone -7: Documentation Verification
|
|
42
|
+
require_relative "t_ruby/docs_example_extractor"
|
|
43
|
+
require_relative "t_ruby/docs_example_verifier"
|
|
44
|
+
require_relative "t_ruby/docs_badge_generator"
|
|
45
|
+
|
|
41
46
|
module TRuby
|
|
42
47
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: t-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.11
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Y. Fred Kim
|
|
@@ -45,6 +45,9 @@ files:
|
|
|
45
45
|
- lib/t_ruby/constraint_checker.rb
|
|
46
46
|
- lib/t_ruby/declaration_generator.rb
|
|
47
47
|
- lib/t_ruby/doc_generator.rb
|
|
48
|
+
- lib/t_ruby/docs_badge_generator.rb
|
|
49
|
+
- lib/t_ruby/docs_example_extractor.rb
|
|
50
|
+
- lib/t_ruby/docs_example_verifier.rb
|
|
48
51
|
- lib/t_ruby/error_handler.rb
|
|
49
52
|
- lib/t_ruby/generic_type_parser.rb
|
|
50
53
|
- lib/t_ruby/intersection_type_parser.rb
|