herb 0.6.1-x86-linux-musl → 0.7.0-x86-linux-musl
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 +1 -0
- data/ext/herb/nodes.c +6 -4
- data/lib/herb/3.0/herb.so +0 -0
- data/lib/herb/3.1/herb.so +0 -0
- data/lib/herb/3.2/herb.so +0 -0
- data/lib/herb/3.3/herb.so +0 -0
- data/lib/herb/3.4/herb.so +0 -0
- data/lib/herb/ast/helpers.rb +26 -0
- data/lib/herb/ast/nodes.rb +7 -3
- data/lib/herb/cli.rb +158 -1
- data/lib/herb/engine/compiler.rb +399 -0
- data/lib/herb/engine/debug_visitor.rb +321 -0
- data/lib/herb/engine/error_formatter.rb +420 -0
- data/lib/herb/engine/parser_error_overlay.rb +767 -0
- data/lib/herb/engine/validation_error_overlay.rb +182 -0
- data/lib/herb/engine/validation_errors.rb +65 -0
- data/lib/herb/engine/validator.rb +75 -0
- data/lib/herb/engine/validators/accessibility_validator.rb +31 -0
- data/lib/herb/engine/validators/nesting_validator.rb +95 -0
- data/lib/herb/engine/validators/security_validator.rb +71 -0
- data/lib/herb/engine.rb +366 -0
- data/lib/herb/project.rb +3 -3
- data/lib/herb/version.rb +1 -1
- data/lib/herb/visitor.rb +2 -0
- data/lib/herb.rb +2 -0
- data/sig/herb/ast/helpers.rbs +16 -0
- data/sig/herb/ast/nodes.rbs +4 -2
- data/sig/herb/engine/compiler.rbs +109 -0
- data/sig/herb/engine/debug.rbs +38 -0
- data/sig/herb/engine/debug_visitor.rbs +70 -0
- data/sig/herb/engine/error_formatter.rbs +47 -0
- data/sig/herb/engine/parser_error_overlay.rbs +41 -0
- data/sig/herb/engine/validation_error_overlay.rbs +35 -0
- data/sig/herb/engine/validation_errors.rbs +45 -0
- data/sig/herb/engine/validator.rbs +37 -0
- data/sig/herb/engine/validators/accessibility_validator.rbs +19 -0
- data/sig/herb/engine/validators/nesting_validator.rbs +25 -0
- data/sig/herb/engine/validators/security_validator.rbs +23 -0
- data/sig/herb/engine.rbs +72 -0
- data/sig/herb/visitor.rbs +2 -0
- data/sig/herb_c_extension.rbs +7 -0
- data/sig/serialized_ast_nodes.rbs +1 -0
- data/src/ast_nodes.c +2 -1
- data/src/ast_pretty_print.c +2 -1
- data/src/element_source.c +11 -0
- data/src/include/ast_nodes.h +3 -1
- data/src/include/element_source.h +13 -0
- data/src/include/version.h +1 -1
- data/src/parser.c +3 -0
- data/src/parser_helpers.c +1 -0
- metadata +30 -2
@@ -0,0 +1,182 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Herb
|
4
|
+
class Engine
|
5
|
+
class ValidationErrorOverlay
|
6
|
+
CONTEXT_LINES = 2
|
7
|
+
|
8
|
+
VALIDATOR_BADGES = {
|
9
|
+
"SecurityValidator" => { label: "Security", color: "#dc2626" },
|
10
|
+
"NestingValidator" => { label: "Nesting", color: "#f59e0b" },
|
11
|
+
"AccessibilityValidator" => { label: "A11y", color: "#3b82f6" },
|
12
|
+
}.freeze
|
13
|
+
|
14
|
+
SEVERITY_COLORS = {
|
15
|
+
"error" => "#dc2626",
|
16
|
+
"warning" => "#f59e0b",
|
17
|
+
"info" => "#3b82f6",
|
18
|
+
}.freeze
|
19
|
+
|
20
|
+
def initialize(source, error, filename: nil)
|
21
|
+
@source = source
|
22
|
+
@error = error
|
23
|
+
@filename = filename || "unknown"
|
24
|
+
@lines = source.lines
|
25
|
+
end
|
26
|
+
|
27
|
+
def generate_fragment
|
28
|
+
location = @error[:location]
|
29
|
+
line_num = location&.start&.line || 1
|
30
|
+
col_num = location&.start&.column || 1
|
31
|
+
|
32
|
+
validator_info = VALIDATOR_BADGES[@error[:source]] || { label: @error[:source], color: "#6b7280" }
|
33
|
+
severity_color = SEVERITY_COLORS[@error[:severity].to_s] || "#6b7280"
|
34
|
+
|
35
|
+
code_snippet = generate_code_snippet(line_num, col_num)
|
36
|
+
|
37
|
+
<<~HTML
|
38
|
+
<div class="herb-validation-item" data-severity="#{escape_attr(@error[:severity].to_s)}">
|
39
|
+
<div class="herb-validation-header">
|
40
|
+
<span class="herb-validation-badge" style="background: #{validator_info[:color]}">
|
41
|
+
#{escape_html(validator_info[:label])}
|
42
|
+
</span>
|
43
|
+
<span class="herb-validation-location">
|
44
|
+
#{escape_html(@filename)}:#{line_num}:#{col_num}
|
45
|
+
</span>
|
46
|
+
</div>
|
47
|
+
<div class="herb-validation-message" style="color: #{severity_color}">
|
48
|
+
#{escape_html(@error[:message])}
|
49
|
+
</div>
|
50
|
+
#{code_snippet}
|
51
|
+
#{generate_suggestion_html if @error[:suggestion]}
|
52
|
+
</div>
|
53
|
+
HTML
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def generate_code_snippet(line_num, col_num)
|
59
|
+
start_line = [line_num - CONTEXT_LINES, 1].max
|
60
|
+
end_line = [line_num + CONTEXT_LINES, @lines.length].min
|
61
|
+
|
62
|
+
code_lines = [] #: Array[String]
|
63
|
+
(start_line..end_line).each do |line|
|
64
|
+
line_content = @lines[line - 1] || ""
|
65
|
+
is_error_line = line == line_num
|
66
|
+
|
67
|
+
highlighted_content = syntax_highlight(line_content.chomp)
|
68
|
+
|
69
|
+
if is_error_line
|
70
|
+
code_lines << <<~HTML
|
71
|
+
<div class="herb-code-line herb-error-line">
|
72
|
+
<div class="herb-line-number">#{line}</div>
|
73
|
+
<div class="herb-line-content">#{highlighted_content}</div>
|
74
|
+
</div>
|
75
|
+
HTML
|
76
|
+
|
77
|
+
if col_num.positive?
|
78
|
+
pointer = "#{" " * (col_num - 1)}^"
|
79
|
+
code_lines << <<~HTML
|
80
|
+
<div class="herb-error-pointer">#{escape_html(pointer)}</div>
|
81
|
+
HTML
|
82
|
+
end
|
83
|
+
else
|
84
|
+
code_lines << <<~HTML
|
85
|
+
<div class="herb-code-line">
|
86
|
+
<div class="herb-line-number">#{line}</div>
|
87
|
+
<div class="herb-line-content">#{highlighted_content}</div>
|
88
|
+
</div>
|
89
|
+
HTML
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
<<~HTML
|
94
|
+
<div class="herb-code-snippet">
|
95
|
+
#{code_lines.join}
|
96
|
+
</div>
|
97
|
+
HTML
|
98
|
+
end
|
99
|
+
|
100
|
+
def generate_suggestion_html
|
101
|
+
<<~HTML
|
102
|
+
<div class="herb-validation-suggestion">
|
103
|
+
<span class="herb-suggestion-icon">💡</span>
|
104
|
+
#{escape_html(@error[:suggestion])}
|
105
|
+
</div>
|
106
|
+
HTML
|
107
|
+
end
|
108
|
+
|
109
|
+
def syntax_highlight(code)
|
110
|
+
lex_result = ::Herb.lex(code)
|
111
|
+
return escape_html(code) if lex_result.errors.any?
|
112
|
+
|
113
|
+
tokens = lex_result.value
|
114
|
+
highlight_with_tokens(tokens, code)
|
115
|
+
rescue StandardError
|
116
|
+
escape_html(code)
|
117
|
+
end
|
118
|
+
|
119
|
+
def highlight_with_tokens(tokens, code)
|
120
|
+
return escape_html(code) if tokens.nil? || tokens.empty?
|
121
|
+
|
122
|
+
highlighted = ""
|
123
|
+
last_end = 0
|
124
|
+
|
125
|
+
tokens.each do |token|
|
126
|
+
char_offset = get_character_offset(code, token.location.start.line, token.location.start.column)
|
127
|
+
char_end = get_character_offset(code, token.location.end_point.line, token.location.end_point.column)
|
128
|
+
|
129
|
+
highlighted += escape_html(code[last_end...char_offset]) if char_offset > last_end
|
130
|
+
|
131
|
+
token_text = code[char_offset...char_end]
|
132
|
+
highlighted += apply_token_style(token, token_text)
|
133
|
+
last_end = char_end
|
134
|
+
end
|
135
|
+
|
136
|
+
highlighted += escape_html(code[last_end..]) if last_end < code.length
|
137
|
+
|
138
|
+
highlighted
|
139
|
+
end
|
140
|
+
|
141
|
+
def get_character_offset(_content, line, column)
|
142
|
+
return column - 1 if line == 1
|
143
|
+
|
144
|
+
column - 1
|
145
|
+
end
|
146
|
+
|
147
|
+
def apply_token_style(token, text)
|
148
|
+
escaped_text = escape_html(text)
|
149
|
+
|
150
|
+
case token.type
|
151
|
+
when "TOKEN_ERB_START", "TOKEN_ERB_END"
|
152
|
+
"<span class=\"herb-erb\">#{escaped_text}</span>"
|
153
|
+
when "TOKEN_ERB_CONTENT"
|
154
|
+
"<span class=\"herb-erb-content\">#{escaped_text}</span>"
|
155
|
+
when "TOKEN_HTML_TAG_START", "TOKEN_HTML_TAG_START_CLOSE", "TOKEN_HTML_TAG_END", "TOKEN_HTML_TAG_SELF_CLOSE", "TOKEN_IDENTIFIER"
|
156
|
+
"<span class=\"herb-tag\">#{escaped_text}</span>"
|
157
|
+
when "TOKEN_HTML_ATTRIBUTE_NAME"
|
158
|
+
"<span class=\"herb-attr\">#{escaped_text}</span>"
|
159
|
+
when "TOKEN_QUOTE", "TOKEN_HTML_ATTRIBUTE_VALUE"
|
160
|
+
"<span class=\"herb-value\">#{escaped_text}</span>"
|
161
|
+
when "TOKEN_HTML_COMMENT_START", "TOKEN_HTML_COMMENT_END", "TOKEN_HTML_COMMENT_CONTENT"
|
162
|
+
"<span class=\"herb-comment\">#{escaped_text}</span>"
|
163
|
+
else
|
164
|
+
escaped_text
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def escape_html(text)
|
169
|
+
text.to_s
|
170
|
+
.gsub("&", "&")
|
171
|
+
.gsub("<", "<")
|
172
|
+
.gsub(">", ">")
|
173
|
+
.gsub('"', """)
|
174
|
+
.gsub("'", "'")
|
175
|
+
end
|
176
|
+
|
177
|
+
def escape_attr(text)
|
178
|
+
escape_html(text).gsub("\n", " ").gsub("\r", " ")
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: false
|
3
|
+
|
4
|
+
# rbs_inline: disabled
|
5
|
+
|
6
|
+
module Herb
|
7
|
+
class Engine
|
8
|
+
class SecurityError < StandardError
|
9
|
+
attr_reader :line, :column, :filename, :suggestion
|
10
|
+
|
11
|
+
def initialize(message, line: nil, column: nil, filename: nil, suggestion: nil)
|
12
|
+
@line = line
|
13
|
+
@column = column
|
14
|
+
@filename = filename
|
15
|
+
@suggestion = suggestion
|
16
|
+
|
17
|
+
super(build_error_message(message))
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def build_error_message(message)
|
23
|
+
parts = [] #: Array[String]
|
24
|
+
|
25
|
+
if @filename || (@line && @column)
|
26
|
+
location_parts = [] #: Array[String]
|
27
|
+
|
28
|
+
location_parts << @filename if @filename
|
29
|
+
location_parts << "#{@line}:#{@column}" if @line && @column
|
30
|
+
|
31
|
+
parts << location_parts.join(":")
|
32
|
+
end
|
33
|
+
|
34
|
+
parts << message
|
35
|
+
|
36
|
+
parts << "Suggestion: #{@suggestion}" if @suggestion
|
37
|
+
|
38
|
+
parts.join(" - ")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
module ValidationErrors
|
43
|
+
class ValidationError
|
44
|
+
attr_reader :type, :location, :message
|
45
|
+
|
46
|
+
def initialize(type, location, message)
|
47
|
+
@type = type
|
48
|
+
@location = location
|
49
|
+
@message = message
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class SecurityValidationError
|
54
|
+
attr_reader :type, :location, :message, :suggestion
|
55
|
+
|
56
|
+
def initialize(location, message, suggestion)
|
57
|
+
@type = "SecurityError"
|
58
|
+
@location = location
|
59
|
+
@message = message
|
60
|
+
@suggestion = suggestion
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Herb
|
4
|
+
class Engine
|
5
|
+
class Validator < Herb::Visitor
|
6
|
+
attr_reader :diagnostics
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
super
|
10
|
+
|
11
|
+
@diagnostics = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def validate(node)
|
15
|
+
visit(node)
|
16
|
+
end
|
17
|
+
|
18
|
+
def error(message, location, code: nil, source: nil)
|
19
|
+
add_diagnostic(message, location, :error, code: code, source: source)
|
20
|
+
end
|
21
|
+
|
22
|
+
def warning(message, location, code: nil, source: nil)
|
23
|
+
add_diagnostic(message, location, :warning, code: code, source: source)
|
24
|
+
end
|
25
|
+
|
26
|
+
def info(message, location, code: nil, source: nil)
|
27
|
+
add_diagnostic(message, location, :info, code: code, source: source)
|
28
|
+
end
|
29
|
+
|
30
|
+
def hint(message, location, code: nil, source: nil)
|
31
|
+
add_diagnostic(message, location, :hint, code: code, source: source)
|
32
|
+
end
|
33
|
+
|
34
|
+
def errors?
|
35
|
+
@diagnostics.any? { |diagnostic| diagnostic[:severity] == :error }
|
36
|
+
end
|
37
|
+
|
38
|
+
def warnings?
|
39
|
+
@diagnostics.any? { |diagnostic| diagnostic[:severity] == :warning }
|
40
|
+
end
|
41
|
+
|
42
|
+
def errors
|
43
|
+
@diagnostics.select { |diagnostic| diagnostic[:severity] == :error }
|
44
|
+
end
|
45
|
+
|
46
|
+
def warnings
|
47
|
+
@diagnostics.select { |diagnostic| diagnostic[:severity] == :warning }
|
48
|
+
end
|
49
|
+
|
50
|
+
def clear_diagnostics
|
51
|
+
@diagnostics.clear
|
52
|
+
end
|
53
|
+
|
54
|
+
def diagnostic_count(severity = nil)
|
55
|
+
return @diagnostics.length unless severity
|
56
|
+
|
57
|
+
@diagnostics.count { |diagnostic| diagnostic[:severity] == severity }
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def add_diagnostic(message, location, severity, code: nil, source: nil)
|
63
|
+
diagnostic = {
|
64
|
+
message: message,
|
65
|
+
location: location,
|
66
|
+
severity: severity,
|
67
|
+
code: code,
|
68
|
+
source: source || self.class.name,
|
69
|
+
}
|
70
|
+
|
71
|
+
@diagnostics << diagnostic
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: true
|
3
|
+
|
4
|
+
require_relative "../validator"
|
5
|
+
|
6
|
+
module Herb
|
7
|
+
class Engine
|
8
|
+
module Validators
|
9
|
+
class AccessibilityValidator < Validator
|
10
|
+
def visit_html_attribute_node(node)
|
11
|
+
validate_attribute(node)
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def validate_attribute(node)
|
18
|
+
# TODO: Add accessibility attribute validation
|
19
|
+
end
|
20
|
+
|
21
|
+
def validate_id_format(node)
|
22
|
+
# TODO: Add ID format validation
|
23
|
+
end
|
24
|
+
|
25
|
+
def add_validation_error(type, location, message)
|
26
|
+
error(message, location, code: type, source: "AccessibilityValidator")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: false
|
3
|
+
|
4
|
+
# rbs_inline: disabled
|
5
|
+
|
6
|
+
require_relative "../validator"
|
7
|
+
|
8
|
+
module Herb
|
9
|
+
class Engine
|
10
|
+
module Validators
|
11
|
+
class NestingValidator < Validator
|
12
|
+
def visit_html_element_node(node)
|
13
|
+
validate_html_nesting(node)
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def validate_html_nesting(node)
|
20
|
+
tag_name = node.tag_name&.value&.downcase
|
21
|
+
return unless tag_name
|
22
|
+
|
23
|
+
case tag_name
|
24
|
+
when "p"
|
25
|
+
validate_no_block_elements_in_paragraph(node)
|
26
|
+
when "a"
|
27
|
+
validate_no_nested_anchors(node)
|
28
|
+
when "button"
|
29
|
+
validate_no_interactive_in_button(node)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def validate_no_block_elements_in_paragraph(node)
|
34
|
+
block_elements = %w[div section article header footer nav aside p h1 h2 h3 h4 h5 h6 ul ol dl table form]
|
35
|
+
|
36
|
+
node.body.each do |child|
|
37
|
+
next unless child.is_a?(Herb::AST::HTMLElementNode)
|
38
|
+
|
39
|
+
child_tag = child.tag_name&.value&.downcase
|
40
|
+
next unless child_tag && block_elements.include?(child_tag)
|
41
|
+
|
42
|
+
add_validation_error(
|
43
|
+
"InvalidNestingError",
|
44
|
+
child.location,
|
45
|
+
"Block element <#{child_tag}> cannot be nested inside <p> at line #{child.location.start.line}"
|
46
|
+
)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def validate_no_nested_anchors(node)
|
51
|
+
find_nested_elements(node, "a") do |nested|
|
52
|
+
add_validation_error(
|
53
|
+
"NestedAnchorError",
|
54
|
+
nested.location,
|
55
|
+
"Anchor <a> cannot be nested inside another anchor at line #{nested.location.start.line}"
|
56
|
+
)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def validate_no_interactive_in_button(node)
|
61
|
+
interactive_elements = %w[a button input select textarea]
|
62
|
+
|
63
|
+
node.body.each do |child|
|
64
|
+
next unless child.is_a?(Herb::AST::HTMLElementNode)
|
65
|
+
|
66
|
+
child_tag = child.tag_name&.value&.downcase
|
67
|
+
next unless child_tag && interactive_elements.include?(child_tag)
|
68
|
+
|
69
|
+
add_validation_error(
|
70
|
+
"InvalidNestingError",
|
71
|
+
child.location,
|
72
|
+
"Interactive element <#{child_tag}> cannot be nested inside <button> at line #{child.location.start.line}"
|
73
|
+
)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def find_nested_elements(node, tag_name, &block)
|
78
|
+
node.body.each do |child|
|
79
|
+
if child.is_a?(Herb::AST::HTMLElementNode)
|
80
|
+
if child.tag_name&.value&.downcase == tag_name
|
81
|
+
yield child
|
82
|
+
else
|
83
|
+
find_nested_elements(child, tag_name, &block)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def add_validation_error(type, location, message)
|
90
|
+
error(message, location, code: type, source: "NestingValidator")
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: true
|
3
|
+
|
4
|
+
require_relative "../validator"
|
5
|
+
|
6
|
+
module Herb
|
7
|
+
class Engine
|
8
|
+
module Validators
|
9
|
+
class SecurityValidator < Validator
|
10
|
+
def visit_html_open_tag_node(node)
|
11
|
+
validate_tag_security(node)
|
12
|
+
|
13
|
+
super
|
14
|
+
end
|
15
|
+
|
16
|
+
def visit_html_attribute_name_node(node)
|
17
|
+
validate_attribute_name_security(node)
|
18
|
+
|
19
|
+
super
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def validate_tag_security(node)
|
25
|
+
node.children.each do |child|
|
26
|
+
next if child.is_a?(Herb::AST::HTMLAttributeNode)
|
27
|
+
next if child.is_a?(Herb::AST::WhitespaceNode)
|
28
|
+
|
29
|
+
next unless child.is_a?(Herb::AST::ERBContentNode) && erb_outputs?(child)
|
30
|
+
|
31
|
+
add_security_error(
|
32
|
+
child.location,
|
33
|
+
"ERB output tags (<%= %>) are not allowed in attribute position.",
|
34
|
+
"Use control flow (<% %>) with static attributes instead."
|
35
|
+
)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def validate_attribute_name_security(node)
|
40
|
+
node.children.each do |child|
|
41
|
+
next unless child.is_a?(Herb::AST::ERBContentNode) && erb_outputs?(child)
|
42
|
+
|
43
|
+
add_security_error(
|
44
|
+
child.location,
|
45
|
+
"ERB output in attribute names is not allowed for security reasons.",
|
46
|
+
"Use static attribute names with dynamic values instead."
|
47
|
+
)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def add_security_error(location, message, suggestion)
|
52
|
+
add_diagnostic(message, location, :error, code: "SecurityViolation", source: "SecurityValidator",
|
53
|
+
suggestion: suggestion)
|
54
|
+
end
|
55
|
+
|
56
|
+
def add_diagnostic(message, location, severity, code: nil, source: nil, suggestion: nil)
|
57
|
+
diagnostic = {
|
58
|
+
message: message,
|
59
|
+
location: location,
|
60
|
+
severity: severity,
|
61
|
+
code: code,
|
62
|
+
source: source || self.class.name,
|
63
|
+
suggestion: suggestion,
|
64
|
+
}
|
65
|
+
|
66
|
+
@diagnostics << diagnostic
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|