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.
@@ -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
@@ -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*(\w+))?/)
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 && !VALID_TYPES.include?(return_type) && !@type_aliases.key?(return_type)
94
- @errors << "Line #{idx + 1}: Unknown return type '#{return_type}'"
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
- param_list = params_str.split(",").map(&:strip)
106
- param_list.each do |param|
107
- match = param.match(/^(\w+)(?::\s*(\w+))?$/)
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
- @errors << "Line #{line_idx + 1}: Unknown parameter type '#{param_type}'"
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TRuby
4
- VERSION = "0.0.7"
4
+ VERSION = "0.0.34"
5
5
  end