t-ruby 0.0.6 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3ff23c88b45e7a1f1c189bd3a9774d80877bc34024f2faa6475fccf2c063019f
4
- data.tar.gz: 8c8ec1a3b6964f72ea75144624099d8dadf8e27c893e0608b2147377db5eeeab
3
+ metadata.gz: f72a8bc0185eae719b855a9edb95a6fa17f9d9f3b4dfadf67f5a32b0a6ab0e32
4
+ data.tar.gz: 376722902f19f751b420e6e6e9c7951aebf45bc76c081c498d8ba4d92e7383f8
5
5
  SHA512:
6
- metadata.gz: 252399c336495e2c261cc137e9612d8f3076ae7e77a01fe827f9252357c693f8870482629bc8e858d9451fd377940abd1ba927ccbfa222fa29b9f34d5eb37f9e
7
- data.tar.gz: 82d876865a19cece6684474f09bbab7185fb2947070f2aeda5c9d5cd0dd6b250443f0595c88674748a351d2902ac7a4cc6e243c85ebfe34b6692f82d48252b1d
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
  &nbsp;&nbsp;•&nbsp;&nbsp;
@@ -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
- ```ruby
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
- ```ruby
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 -w Watch all .trb files in current directory
19
- trc -w src/ Watch all .trb files in src/ directory
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
@@ -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" => false,
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?(".trb.yml")
47
- YAML.safe_load_file(".trb.yml", permitted_classes: [Symbol])
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
@@ -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
 
@@ -234,6 +234,8 @@ module TRuby
234
234
  handle_definition(params)
235
235
  when "textDocument/semanticTokens/full"
236
236
  handle_semantic_tokens_full(params)
237
+ when "textDocument/diagnostic"
238
+ handle_diagnostic(params)
237
239
  else
238
240
  { error: { code: ErrorCodes::METHOD_NOT_FOUND, message: "Method not found: #{method}" } }
239
241
  end
@@ -342,6 +344,21 @@ module TRuby
342
344
 
343
345
  # === Diagnostics ===
344
346
 
347
+ # Handle pull-based diagnostics (LSP 3.17+)
348
+ def handle_diagnostic(params)
349
+ uri = params.dig("textDocument", "uri")
350
+ return { "kind" => "full", "items" => [] } unless uri
351
+
352
+ doc = @documents[uri]
353
+ return { "kind" => "full", "items" => [] } unless doc
354
+
355
+ text = doc[:text]
356
+ return { "kind" => "full", "items" => [] } unless text
357
+
358
+ diagnostics = analyze_document(text)
359
+ { "kind" => "full", "items" => diagnostics }
360
+ end
361
+
345
362
  def publish_diagnostics(uri, text)
346
363
  diagnostics = analyze_document(text)
347
364
 
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.6"
4
+ VERSION = "0.0.11"
5
5
  end
@@ -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
- @file_count = trb_files.size
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
- elsif @parallel && trb_files.size > 1
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(trb_files) do |file|
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
- trb_files.each do |file|
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.6
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