tryouts 3.5.0 → 3.5.2
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 +5 -8
- data/exe/try +6 -0
- data/lib/tryouts/cli/formatters/agent.rb +206 -34
- data/lib/tryouts/cli/opts.rb +15 -15
- data/lib/tryouts/expectation_evaluators/result_type.rb +15 -0
- data/lib/tryouts/file_processor.rb +2 -2
- data/lib/tryouts/parser_warning.rb +10 -0
- data/lib/tryouts/parsers/CLAUDE.md +178 -0
- data/lib/tryouts/parsers/base_parser.rb +101 -1
- data/lib/tryouts/parsers/enhanced_parser.rb +177 -25
- data/lib/tryouts/parsers/legacy_parser.rb +254 -0
- data/lib/tryouts/parsers/shared_methods.rb +5 -1
- data/lib/tryouts/test_batch.rb +1 -1
- data/lib/tryouts/test_executor.rb +12 -4
- data/lib/tryouts/test_result_aggregator.rb +15 -11
- data/lib/tryouts/test_runner.rb +18 -15
- data/lib/tryouts/version.rb +1 -1
- data/lib/tryouts.rb +1 -1
- metadata +3 -2
- data/lib/tryouts/parsers/prism_parser.rb +0 -122
@@ -0,0 +1,178 @@
|
|
1
|
+
In Ruby 3.4+, `case/when` and `case/in` represent fundamentally different approaches to conditional logic:
|
2
|
+
|
3
|
+
## `case/when` - Traditional Equality Matching
|
4
|
+
|
5
|
+
Uses the `===` operator for comparison. Simple and straightforward:
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
def classify_response(status)
|
9
|
+
case status
|
10
|
+
when 200..299
|
11
|
+
"success"
|
12
|
+
when 400..499
|
13
|
+
"client_error"
|
14
|
+
when 500..599
|
15
|
+
"server_error"
|
16
|
+
when String
|
17
|
+
"string_status"
|
18
|
+
else
|
19
|
+
"unknown"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
```
|
23
|
+
|
24
|
+
## `case/in` - Pattern Matching with Destructuring
|
25
|
+
|
26
|
+
Matches structure and binds variables. Much more powerful:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
def process_api_response(response)
|
30
|
+
case response
|
31
|
+
in { status: 200, data: { user: { name: String => name, age: Integer => age } } }
|
32
|
+
"User #{name} is #{age} years old"
|
33
|
+
|
34
|
+
in { status: 200, data: Array => items } if items.length > 10
|
35
|
+
"Got #{items.length} items"
|
36
|
+
|
37
|
+
in { status: 400..499, error: { message: msg } }
|
38
|
+
"Client error: #{msg}"
|
39
|
+
|
40
|
+
in { status: 500.. }
|
41
|
+
"Server error occurred"
|
42
|
+
|
43
|
+
in nil | {}
|
44
|
+
"Empty response"
|
45
|
+
else
|
46
|
+
"Unexpected response format"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
## Key Differences
|
52
|
+
|
53
|
+
### 1. **Variable Binding**
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
# case/when - no binding
|
57
|
+
case user
|
58
|
+
when Hash
|
59
|
+
puts user[:name] # Must access manually
|
60
|
+
end
|
61
|
+
|
62
|
+
# case/in - automatic binding
|
63
|
+
case user
|
64
|
+
in { name: String => username, age: } # 'age' variable created automatically
|
65
|
+
puts username # Bound variable available
|
66
|
+
end
|
67
|
+
```
|
68
|
+
|
69
|
+
### 2. **Structural Matching**
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
# case/when - only surface comparison
|
73
|
+
case data
|
74
|
+
when Array
|
75
|
+
# Know it's an array, but not its contents
|
76
|
+
end
|
77
|
+
|
78
|
+
# case/in - deep structure matching
|
79
|
+
case data
|
80
|
+
in [first, *middle, last] if middle.length > 2
|
81
|
+
# Automatically destructured with guard condition
|
82
|
+
end
|
83
|
+
```
|
84
|
+
|
85
|
+
### 3. **Guard Conditions**
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
# case/when - separate if needed
|
89
|
+
case number
|
90
|
+
when Integer
|
91
|
+
if number > 100
|
92
|
+
"big integer"
|
93
|
+
else
|
94
|
+
"small integer"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# case/in - integrated guards
|
99
|
+
case number
|
100
|
+
in Integer => n if n > 100
|
101
|
+
"big integer"
|
102
|
+
in Integer
|
103
|
+
"small integer"
|
104
|
+
end
|
105
|
+
```
|
106
|
+
|
107
|
+
## Practical Example for Tryouts
|
108
|
+
|
109
|
+
For parsing tryout lines, here's the difference:
|
110
|
+
|
111
|
+
### Traditional `case/when`
|
112
|
+
```ruby
|
113
|
+
def parse_line(line)
|
114
|
+
case line
|
115
|
+
when /^##\s*(.+)/
|
116
|
+
[:description, $1.strip]
|
117
|
+
when /^#=>\s*(.+)/
|
118
|
+
[:expectation, $1.strip]
|
119
|
+
when /^#=\?>\s*(.+)/
|
120
|
+
[:debug_info, $1.strip]
|
121
|
+
else
|
122
|
+
[:code, line]
|
123
|
+
end
|
124
|
+
end
|
125
|
+
```
|
126
|
+
|
127
|
+
### Pattern Matching `case/in`
|
128
|
+
```ruby
|
129
|
+
def parse_line(line)
|
130
|
+
case line
|
131
|
+
in /^##\s*(.+)/ => description
|
132
|
+
[:description, description.strip]
|
133
|
+
in /^#=>\s*(.+)/ => expectation
|
134
|
+
[:expectation, expectation.strip]
|
135
|
+
in /^#=\?>\s*(.+)/ => debug_expr
|
136
|
+
[:debug_info, debug_expr.strip]
|
137
|
+
in /^\s*$/
|
138
|
+
[:blank]
|
139
|
+
else
|
140
|
+
[:code, line]
|
141
|
+
end
|
142
|
+
end
|
143
|
+
```
|
144
|
+
|
145
|
+
## When to Use Which
|
146
|
+
|
147
|
+
### Use `case/when` for:
|
148
|
+
- Simple value comparisons
|
149
|
+
- Class/type checking
|
150
|
+
- Range matching
|
151
|
+
- Traditional switch-like logic
|
152
|
+
|
153
|
+
### Use `case/in` for:
|
154
|
+
- Complex data structure matching
|
155
|
+
- When you need variable binding
|
156
|
+
- Guard conditions
|
157
|
+
- Destructuring arrays/hashes
|
158
|
+
- Multiple conditions per branch
|
159
|
+
|
160
|
+
## Ruby 3.4+ Enhancements
|
161
|
+
|
162
|
+
Ruby 3.4 added several pattern matching improvements:
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
# Variable binding in array patterns
|
166
|
+
case data
|
167
|
+
in [String => first, *String => middle, String => last]
|
168
|
+
# All string array with bound variables
|
169
|
+
end
|
170
|
+
|
171
|
+
# Hash patterns with rest
|
172
|
+
case config
|
173
|
+
in { required: true, **rest } if rest.keys.all? { |k| k.is_a?(Symbol) }
|
174
|
+
# Required config with symbol keys only
|
175
|
+
end
|
176
|
+
```
|
177
|
+
|
178
|
+
For the Tryouts modernization, `case/in` provides cleaner syntax for parsing complex comment patterns while binding the captured content directly to variables, eliminating the need for global match variables like `$1`.
|
@@ -6,11 +6,89 @@ require_relative 'shared_methods'
|
|
6
6
|
require_relative '../parser_warning'
|
7
7
|
|
8
8
|
class Tryouts
|
9
|
-
# Fixed PrismParser with pattern matching for robust token filtering
|
10
9
|
module Parsers
|
10
|
+
# Base class for all tryout parsers providing common functionality
|
11
|
+
#
|
12
|
+
# BaseParser establishes the foundation for parsing tryout files by handling
|
13
|
+
# file loading, Prism integration, and providing shared parsing infrastructure.
|
14
|
+
# All concrete parser implementations (EnhancedParser, LegacyParser) inherit
|
15
|
+
# from this class.
|
16
|
+
#
|
17
|
+
# @abstract Subclass and implement {#parse} to create a concrete parser
|
18
|
+
# @example Implementing a custom parser
|
19
|
+
# class MyCustomParser < Tryouts::Parsers::BaseParser
|
20
|
+
# def parse
|
21
|
+
# # Your parsing logic here
|
22
|
+
# # Must return a Tryouts::Testrun object
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# private
|
26
|
+
#
|
27
|
+
# def parser_type
|
28
|
+
# :custom
|
29
|
+
# end
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# @!attribute [r] source_path
|
33
|
+
# @return [String] Path to the source file being parsed
|
34
|
+
# @!attribute [r] source
|
35
|
+
# @return [String] Raw source code content
|
36
|
+
# @!attribute [r] lines
|
37
|
+
# @return [Array<String>] Source lines with line endings removed
|
38
|
+
# @!attribute [r] prism_result
|
39
|
+
# @return [Prism::ParseResult] Result of parsing source with Prism
|
40
|
+
# @!attribute [r] parsed_at
|
41
|
+
# @return [Time] Timestamp when parsing was initiated
|
42
|
+
# @!attribute [r] options
|
43
|
+
# @return [Hash] Parser configuration options
|
44
|
+
# @!attribute [r] warnings
|
45
|
+
# @return [Array<Tryouts::ParserWarning>] Collection of parsing warnings
|
46
|
+
#
|
47
|
+
# ## Shared Functionality
|
48
|
+
#
|
49
|
+
# ### 1. File and Source Management
|
50
|
+
# - Automatic file reading and line splitting
|
51
|
+
# - UTF-8 encoding handling
|
52
|
+
# - Path normalization and validation
|
53
|
+
#
|
54
|
+
# ### 2. Prism Integration
|
55
|
+
# - Automatic Prism parsing of source code
|
56
|
+
# - Syntax error detection and handling
|
57
|
+
# - AST access for advanced parsing needs
|
58
|
+
#
|
59
|
+
# ### 3. Warning System
|
60
|
+
# - Centralized warning collection and management
|
61
|
+
# - Type-safe warning objects with context
|
62
|
+
# - Integration with output formatters
|
63
|
+
#
|
64
|
+
# ### 4. Shared Methods
|
65
|
+
# - Token grouping and classification logic
|
66
|
+
# - Test case boundary detection
|
67
|
+
# - Common utility methods for all parsers
|
68
|
+
#
|
69
|
+
# ## Parser Requirements
|
70
|
+
#
|
71
|
+
# Concrete parser implementations must:
|
72
|
+
# 1. Implement the abstract `parse` method
|
73
|
+
# 2. Return a `Tryouts::Testrun` object
|
74
|
+
# 3. Handle syntax errors appropriately
|
75
|
+
# 4. Provide a unique `parser_type` identifier
|
76
|
+
#
|
77
|
+
# @see EnhancedParser For Prism-based comment extraction
|
78
|
+
# @see LegacyParser For line-by-line parsing approach
|
79
|
+
# @see SharedMethods For common parsing utilities
|
80
|
+
# @since 3.0.0
|
11
81
|
class BaseParser
|
12
82
|
include Tryouts::Parsers::SharedMethods
|
13
83
|
|
84
|
+
# Initialize a new parser instance
|
85
|
+
#
|
86
|
+
# @param source_path [String] Absolute path to the tryout source file
|
87
|
+
# @param options [Hash] Configuration options for parsing behavior
|
88
|
+
# @option options [Boolean] :strict Enable strict mode validation
|
89
|
+
# @option options [Boolean] :warnings Enable warning collection (default: true)
|
90
|
+
# @raise [Errno::ENOENT] If source file doesn't exist
|
91
|
+
# @raise [Errno::EACCES] If source file isn't readable
|
14
92
|
def initialize(source_path, options = {})
|
15
93
|
@source_path = source_path
|
16
94
|
@source = File.read(source_path)
|
@@ -21,6 +99,28 @@ class Tryouts
|
|
21
99
|
@warnings = []
|
22
100
|
end
|
23
101
|
|
102
|
+
# Parse the source file into structured test data
|
103
|
+
#
|
104
|
+
# @abstract Subclasses must implement this method
|
105
|
+
# @return [Tryouts::Testrun] Parsed test structure with setup, tests, teardown, and warnings
|
106
|
+
# @raise [NotImplementedError] If called directly on BaseParser
|
107
|
+
def parse
|
108
|
+
raise NotImplementedError, "Subclasses must implement #parse"
|
109
|
+
end
|
110
|
+
|
111
|
+
protected
|
112
|
+
|
113
|
+
# Get the parser type identifier
|
114
|
+
#
|
115
|
+
# @abstract Subclasses should override to provide unique identifier
|
116
|
+
# @return [Symbol] Parser type identifier
|
117
|
+
def parser_type
|
118
|
+
:base
|
119
|
+
end
|
120
|
+
|
121
|
+
# Access to instance variables for subclasses
|
122
|
+
attr_reader :source_path, :source, :lines, :prism_result, :parsed_at, :options
|
123
|
+
|
24
124
|
end
|
25
125
|
end
|
26
126
|
end
|
@@ -1,16 +1,74 @@
|
|
1
1
|
# lib/tryouts/parsers/enhanced_parser.rb
|
2
2
|
|
3
|
-
# Enhanced parser using Prism's inhouse comment extraction capabilities
|
4
|
-
# Drop-in replacement for PrismParser that eliminates HEREDOC parsing issues
|
5
|
-
|
6
3
|
require_relative '../test_case'
|
7
4
|
require_relative 'base_parser'
|
8
5
|
|
9
6
|
class Tryouts
|
10
|
-
# Enhanced parser
|
11
|
-
#
|
7
|
+
# Enhanced parser using Prism's native comment extraction for robust parsing
|
8
|
+
#
|
9
|
+
# The EnhancedParser is the default parser that provides syntax-aware comment
|
10
|
+
# extraction by leveraging Ruby's official Prism parser. This approach eliminates
|
11
|
+
# common parsing issues found in regex-based parsers, particularly with complex
|
12
|
+
# Ruby syntax.
|
13
|
+
#
|
14
|
+
# @example Basic usage
|
15
|
+
# parser = Tryouts::EnhancedParser.new(source_code, file_path)
|
16
|
+
# testrun = parser.parse
|
17
|
+
# puts testrun.test_cases.length
|
18
|
+
#
|
19
|
+
# @example Problematic code that EnhancedParser handles correctly
|
20
|
+
# source = <<~RUBY
|
21
|
+
# ## Test HEREDOC handling
|
22
|
+
# sql = <<~SQL
|
23
|
+
# SELECT * FROM users
|
24
|
+
# -- This is NOT a tryout comment
|
25
|
+
# #=> This is NOT a tryout expectation
|
26
|
+
# SQL
|
27
|
+
# puts sql.length
|
28
|
+
# #=> Integer # This IS a real expectation
|
29
|
+
# RUBY
|
30
|
+
#
|
31
|
+
# @!attribute [r] parser_type
|
32
|
+
# @return [Symbol] Returns :enhanced to identify parser type
|
33
|
+
#
|
34
|
+
# ## Key Benefits over LegacyParser
|
35
|
+
#
|
36
|
+
# ### 1. HEREDOC Safety
|
37
|
+
# - Uses Prism's `parse_comments()` to extract only actual Ruby comments
|
38
|
+
# - Automatically excludes content inside string literals, HEREDOCs, and interpolation
|
39
|
+
# - Prevents false positive expectation detection
|
40
|
+
#
|
41
|
+
# ### 2. Inline Comment Handling
|
42
|
+
# - Correctly handles lines with both code and comments
|
43
|
+
# - Supports multiple comments per line with proper positioning
|
44
|
+
# - Emits separate tokens for code and comment content
|
45
|
+
#
|
46
|
+
# ### 3. Syntax Awareness
|
47
|
+
# - Leverages Ruby's official parser for accurate code understanding
|
48
|
+
# - Handles complex Ruby syntax edge cases reliably
|
49
|
+
# - More robust than regex-based parsing approaches
|
50
|
+
#
|
51
|
+
# ### 4. Performance
|
52
|
+
# - Uses optimized C-based Prism parsing for comment extraction
|
53
|
+
# - Efficient handling of large files with complex syntax
|
54
|
+
#
|
55
|
+
# ## Pattern Matching
|
56
|
+
# Uses Ruby 3.4+ pattern matching (`case/in`) for token classification,
|
57
|
+
# providing clean, modern syntax for expectation type detection.
|
58
|
+
#
|
59
|
+
# @see LegacyParser For simpler regex-based parsing (legacy compatibility)
|
60
|
+
# @see BaseParser For shared parsing functionality
|
61
|
+
# @since 3.2.0
|
12
62
|
class EnhancedParser < Tryouts::Parsers::BaseParser
|
13
63
|
|
64
|
+
# Parse source code into a Testrun using Prism-based comment extraction
|
65
|
+
#
|
66
|
+
# This method provides the main parsing logic that converts raw Ruby source
|
67
|
+
# code containing tryout syntax into structured test cases. Uses Prism's
|
68
|
+
# native comment extraction to avoid HEREDOC parsing issues.
|
69
|
+
#
|
70
|
+
# @return [Tryouts::Testrun] Structured test data with setup, test cases, teardown, and warnings
|
71
|
+
# @raise [Tryouts::TryoutSyntaxError] If source contains syntax errors or strict mode violations
|
14
72
|
def parse
|
15
73
|
return handle_syntax_errors if @prism_result.failure?
|
16
74
|
|
@@ -25,7 +83,20 @@ class Tryouts
|
|
25
83
|
|
26
84
|
private
|
27
85
|
|
28
|
-
#
|
86
|
+
# Extract and tokenize comments using Prism's native comment extraction
|
87
|
+
#
|
88
|
+
# This method replaces manual line-by-line regex parsing with Prism's
|
89
|
+
# built-in comment extraction capabilities. The key benefit is that
|
90
|
+
# `Prism.parse_comments()` only returns actual Ruby comments, automatically
|
91
|
+
# excluding content inside string literals, HEREDOCs, and interpolations.
|
92
|
+
#
|
93
|
+
# @return [Array<Hash>] Array of token hashes with keys :type, :content, :line, etc.
|
94
|
+
# @example Token structure
|
95
|
+
# [
|
96
|
+
# { type: :description, content: "Test case description", line: 5 },
|
97
|
+
# { type: :code, content: "result = calculate(x)", line: 6 },
|
98
|
+
# { type: :expectation, content: "42", line: 7, ast: <Prism::Node> }
|
99
|
+
# ]
|
29
100
|
def tokenize_content_with_inhouse_extraction
|
30
101
|
tokens = []
|
31
102
|
|
@@ -47,8 +118,8 @@ class Tryouts
|
|
47
118
|
tokens << { type: :code, content: line, line: index, ast: parse_ruby_line(line) }
|
48
119
|
emitted_code = true
|
49
120
|
end
|
50
|
-
# Inline comment
|
51
|
-
tokens <<
|
121
|
+
# Inline comment (after code) - treat as regular comment, not expectation
|
122
|
+
tokens << { type: :comment, content: comment_content.sub(/^#\s*/, ''), line: line_number - 1 }
|
52
123
|
else
|
53
124
|
tokens << classify_comment_inhousely(comment_content, line_number)
|
54
125
|
end
|
@@ -69,44 +140,125 @@ class Tryouts
|
|
69
140
|
tokens
|
70
141
|
end
|
71
142
|
|
72
|
-
#
|
143
|
+
# Classify comment content into specific token types using pattern matching
|
144
|
+
#
|
145
|
+
# Takes a raw comment string and determines what type of tryout token it
|
146
|
+
# represents (description, expectation, etc.). Uses Ruby 3.4+ pattern matching
|
147
|
+
# for clean, maintainable classification logic.
|
148
|
+
#
|
149
|
+
# @param content [String] The comment content (including # prefix)
|
150
|
+
# @param line_number [Integer] 1-based line number for error reporting
|
151
|
+
# @return [Hash] Token hash with :type, :content, :line and other type-specific keys
|
152
|
+
#
|
153
|
+
# @example Valid expectation
|
154
|
+
# classify_comment_inhousely("#=> 42", 10)
|
155
|
+
# # => { type: :expectation, content: "42", line: 9, ast: <Prism::Node> }
|
156
|
+
#
|
157
|
+
# @example Malformed expectation (triggers warning)
|
158
|
+
# classify_comment_inhousely("#=INVALID> 42", 10)
|
159
|
+
# # => { type: :malformed_expectation, syntax: "INVALID", content: "42", line: 9 }
|
160
|
+
# # Also adds warning to parser's warning collection
|
161
|
+
#
|
162
|
+
# @example Test description
|
163
|
+
# classify_comment_inhousely("## Test basic math", 5)
|
164
|
+
# # => { type: :description, content: "Test basic math", line: 4 }
|
73
165
|
def classify_comment_inhousely(content, line_number)
|
74
166
|
case content
|
75
|
-
|
167
|
+
in /^##\s*(.*)$/ # Test description format: ## description
|
76
168
|
{ type: :description, content: $1.strip, line: line_number - 1 }
|
77
|
-
|
169
|
+
in /^#\s*TEST\s*\d*:\s*(.*)$/ # rubocop:disable Lint/DuplicateBranch
|
78
170
|
{ type: :description, content: $1.strip, line: line_number - 1 }
|
79
|
-
|
171
|
+
in /^#\s*=!>\s*(.*)$/ # Exception expectation
|
80
172
|
{ type: :exception_expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
|
81
|
-
|
173
|
+
in /^#\s*=<>\s*(.*)$/ # Intentional failure expectation
|
82
174
|
{ type: :intentional_failure_expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
|
83
|
-
|
175
|
+
in /^#\s*==>\s*(.*)$/ # Boolean true expectation
|
84
176
|
{ type: :true_expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
|
85
|
-
|
177
|
+
in %r{^#\s*=/=>\s*(.*)$} # Boolean false expectation
|
86
178
|
{ type: :false_expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
|
87
|
-
|
179
|
+
in /^#\s*=\|>\s*(.*)$/ # Boolean (true or false) expectation
|
88
180
|
{ type: :boolean_expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
|
89
|
-
|
181
|
+
in /^#\s*=\*>\s*(.*)$/ # Non-nil expectation
|
90
182
|
{ type: :non_nil_expectation, content: $1.strip, line: line_number - 1 }
|
91
|
-
|
183
|
+
in /^#\s*=:>\s*(.*)$/ # Result type expectation
|
92
184
|
{ type: :result_type_expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
|
93
|
-
|
185
|
+
in /^#\s*=~>\s*(.*)$/ # Regex match expectation
|
94
186
|
{ type: :regex_match_expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
|
95
|
-
|
187
|
+
in /^#\s*=%>\s*(.*)$/ # Performance time expectation
|
96
188
|
{ type: :performance_time_expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
|
97
|
-
|
189
|
+
in /^#\s*=(\d+)>\s*(.*)$/ # Output expectation (stdout/stderr with pipe number)
|
98
190
|
{ type: :output_expectation, content: $2.strip, pipe: $1.to_i, line: line_number - 1, ast: parse_expectation($2.strip) }
|
99
|
-
|
191
|
+
in /^#\s*=>\s*(.*)$/ # Regular expectation
|
100
192
|
{ type: :expectation, content: $1.strip, line: line_number - 1, ast: parse_expectation($1.strip) }
|
101
|
-
|
193
|
+
in /^#\s*=([^>=:!~%*|\/\s]+)>\s*(.*)$/ # Malformed expectation - invalid characters between = and >
|
194
|
+
syntax = $1
|
195
|
+
content_part = $2.strip
|
196
|
+
add_warning(ParserWarning.malformed_expectation(
|
197
|
+
line_number: line_number,
|
198
|
+
syntax: syntax,
|
199
|
+
context: content.strip
|
200
|
+
))
|
201
|
+
{ type: :malformed_expectation, syntax: syntax, content: content_part, line: line_number - 1 }
|
202
|
+
in /^##\s*=>\s*(.*)$/ # Commented out expectation (should be ignored)
|
102
203
|
{ type: :comment, content: '=>' + $1.strip, line: line_number - 1 }
|
103
|
-
|
204
|
+
in content if looks_like_malformed_expectation?(content) # Comprehensive malformed expectation detection
|
205
|
+
detected_syntax = extract_malformed_syntax(content)
|
206
|
+
add_warning(ParserWarning.malformed_expectation(
|
207
|
+
line_number: line_number,
|
208
|
+
syntax: detected_syntax,
|
209
|
+
context: content.strip
|
210
|
+
))
|
211
|
+
{ type: :malformed_expectation, syntax: detected_syntax, content: content.strip, line: line_number - 1 }
|
212
|
+
in /^#\s*(.*)$/ # Single hash comment - potential description
|
104
213
|
{ type: :potential_description, content: $1.strip, line: line_number - 1 }
|
105
|
-
else
|
214
|
+
else # Unknown comment format
|
106
215
|
{ type: :comment, content: content.sub(/^#\s*/, ''), line: line_number - 1 }
|
107
216
|
end
|
108
217
|
end
|
109
218
|
|
219
|
+
# Detect if a comment looks like a malformed expectation attempt
|
220
|
+
# This catches patterns that suggest someone tried to write an expectation
|
221
|
+
# but got the syntax wrong (missing parts, wrong spacing, extra characters, etc.)
|
222
|
+
#
|
223
|
+
# Only flags as malformed if it starts with patterns that look like expectation syntax,
|
224
|
+
# not just any comment that happens to contain equals signs in natural language.
|
225
|
+
def looks_like_malformed_expectation?(content)
|
226
|
+
# Skip if it's already handled by specific patterns above
|
227
|
+
return false if content.match?(/^##\s*/) # Description
|
228
|
+
return false if content.match?(/^#\s*TEST\s*\d*:\s*/) # TEST format
|
229
|
+
return false if content.match?(/^##\s*=>\s*/) # Commented out expectation
|
230
|
+
|
231
|
+
# Only flag as malformed if it looks like an expectation attempt at the start
|
232
|
+
# Patterns that suggest malformed expectation syntax:
|
233
|
+
# - Starts with #= but doesn't match valid patterns
|
234
|
+
# - Has multiple = or > characters in suspicious positions near the start
|
235
|
+
# - Looks like broken expectation syntax (not natural language)
|
236
|
+
|
237
|
+
return true if content.match?(/^#\s*=\s*>/) # "# = >" (spaces in wrong places)
|
238
|
+
return true if content.match?(/^#\s*==+>/) # "# ==> " (wrong number of =)
|
239
|
+
return true if content.match?(/^#\s*=[^=:!~%*|\/>\s][^>]*>/) # "# =X> " (invalid chars between = and >)
|
240
|
+
return true if content.match?(/^#\s*>[^=]/) # "# >something" (starts with >)
|
241
|
+
return true if content.match?(/^#\s*<[^=]/) # "# <something" (starts with <)
|
242
|
+
|
243
|
+
false # Regular comments with = signs in natural language are OK
|
244
|
+
end
|
245
|
+
|
246
|
+
# Extract the malformed syntax portion for warning display
|
247
|
+
def extract_malformed_syntax(content)
|
248
|
+
# Try to identify what the user was attempting to write
|
249
|
+
case content
|
250
|
+
when /^#\s*([=><][^=><]*[=><].*?)(\s|$)/ # Pattern with expectation chars
|
251
|
+
$1.strip
|
252
|
+
when /^#\s*([=><].*?)(\s|$)/ # Simple pattern starting with expectation char
|
253
|
+
$1.strip
|
254
|
+
when /^#\s*(.*?[=><].*?)(\s|$)/ # Pattern containing expectation chars
|
255
|
+
$1.strip
|
256
|
+
else
|
257
|
+
# Fallback: show the part after #
|
258
|
+
content.sub(/^#\s*/, '').split(/\s/).first || 'unknown'
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
110
262
|
# Parser type identification for metadata
|
111
263
|
def parser_type
|
112
264
|
:enhanced
|