t-ruby 0.0.7 → 0.0.34
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 +131 -6
- data/lib/t_ruby/compiler.rb +97 -9
- data/lib/t_ruby/config.rb +366 -24
- 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 +42 -12
- data/lib/t_ruby.rb +5 -0
- metadata +4 -1
|
@@ -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