simplecov-ai 0.10.3 → 0.10.4

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: 3b180d86c54ffab95578a779a8765ec546495b6bc3bd1e353674cf81697b9afe
4
- data.tar.gz: f86ba3e7b0b6e5b3ded6c40799424fded61d2f2d980cb92e784685d340ac31f6
3
+ metadata.gz: 21fb51a293aef52967848c9b4fcc176b83aa274c347fb29abeaf76a34f040754
4
+ data.tar.gz: b43d99c3b4064c5e2914be0df4251774f78f0f6fa41cc51ae85c4e4547a586e9
5
5
  SHA512:
6
- metadata.gz: 4cd0db7e047ccb21fdfc6ccf1c4e2855542c209051b6a0f663ec8037deddf6a4b8481865c492343ccee81f9ed42e5b937d0286b5d5f18dec0e07897aaf6cd902
7
- data.tar.gz: 92f1698f205c2ff941287b45c53290e52306e8e9b1ae3916e84477917c8045d3ed17c5a2653c33a4ea842e837b9c96521a3bb85bc44799e8135962ae65346f1f
6
+ metadata.gz: 55f0042ff2503ffc573c39a1393a407e66988e2de5f3e79d5d35eb539f70645bb1bcd6374870d84682dba430a87b7b0f9601baa446e9beb99f1e9e7aa3c45cf1
7
+ data.tar.gz: 2a94566df43e8bdd3c8e68ffcbe95bbda0683be0cd661313ede1af5a421d0972ad03d3b0363cf72259d631e80a490529075adc2b13df58a02d7ffa4c777ef95c
checksums.yaml.gz.sig CHANGED
Binary file
@@ -43,6 +43,11 @@ module SimpleCov
43
43
  @end_line = end_line
44
44
  @bypasses = bypasses
45
45
  end
46
+
47
+ sig { params(bypass: String).void }
48
+ def add_bypass(bypass)
49
+ @bypasses << bypass
50
+ end
46
51
  end
47
52
 
48
53
  # Orchestrates the initial mapping algorithm on a target file to extract structural
@@ -54,115 +59,118 @@ module SimpleCov
54
59
  def self.resolve(file_path)
55
60
  return [] unless File.exist?(file_path)
56
61
 
57
- begin
58
- source = File.read(file_path)
59
- ast, comments = Parser::CurrentRuby.parse_with_comments(source)
60
- new.traverse(ast, comments)
61
- rescue Parser::SyntaxError
62
- []
63
- end
62
+ source = File.read(file_path)
63
+ ast, comments = Parser::CurrentRuby.parse_with_comments(source)
64
+
65
+ resolver = new
66
+ nodes = resolver.traverse(ast)
67
+ resolver.assign_bypasses(nodes, comments)
68
+ nodes
64
69
  end
65
70
 
66
71
  # Recursively navigates an abstract node hierarchy, building SemanticNodes mappings
67
72
  # around modules, classes, singleton, and instance methods while aggregating parent paths.
68
73
  #
69
74
  # @param node [Parser::AST::Node] The root AST node from which traversal executes.
70
- # @param comments [Array<Parser::Source::Comment>] Lexical comments corresponding to nodes.
71
75
  # @param context [String] An accumulated identifier linking namespaces to inner entities.
72
76
  # @return [Array<SemanticNode>] Accumulation of all sub-tree defined endpoints.
73
77
  sig do
74
- params(node: Parser::AST::Node, comments: T::Array[Parser::Source::Comment],
78
+ params(node: T.nilable(Parser::AST::Node),
75
79
  context: String).returns(T::Array[SemanticNode])
76
80
  end
77
- def traverse(node, comments, context = '')
81
+ def traverse(node, context = '')
78
82
  return [] unless node.is_a?(Parser::AST::Node)
79
83
 
80
84
  nodes = T.let([], T::Array[SemanticNode])
81
- current_context, semantic_node = extract_node_metadata(node, comments, context)
85
+ current_context, semantic_node = extract_node_metadata(node, context)
82
86
  nodes << semantic_node if semantic_node
83
87
 
84
88
  node.children.grep(Parser::AST::Node).each do |child|
85
- nodes.concat(traverse(child, comments, current_context))
89
+ nodes.concat(traverse(child, current_context))
86
90
  end
87
91
 
88
92
  nodes
89
93
  end
90
94
 
95
+ sig { params(nodes: T::Array[SemanticNode], comments: T::Array[Parser::Source::Comment]).void }
96
+ def assign_bypasses(nodes, comments)
97
+ comments.each do |c|
98
+ c_text = T.cast(c.text, String)
99
+ next unless c_text.include?(':nocov:')
100
+
101
+ assign_bypass(nodes, c, c_text.strip)
102
+ end
103
+ end
104
+
91
105
  private
92
106
 
107
+ sig { params(nodes: T::Array[SemanticNode], comment: Parser::Source::Comment, text: String).void }
108
+ def assign_bypass(nodes, comment, text)
109
+ c_loc = T.cast(comment.loc, Parser::Source::Map)
110
+ c_line = T.cast(c_loc.line, Integer)
111
+
112
+ innermost_node = nodes.reverse.find { |n| c_line.between?(n.start_line - 1, n.end_line + 1) }
113
+ innermost_node&.add_bypass(text)
114
+ end
115
+
93
116
  sig do
94
- params(node: Parser::AST::Node, comments: T::Array[Parser::Source::Comment], context: String)
117
+ params(node: Parser::AST::Node, context: String)
95
118
  .returns([String, T.nilable(SemanticNode)])
96
119
  end
97
- def extract_node_metadata(node, comments, context)
120
+ def extract_node_metadata(node, context)
98
121
  case node.type
99
122
  when :class, :module
100
- extract_class_metadata(node, comments, context)
123
+ extract_class_metadata(node, context)
101
124
  when :def
102
- extract_instance_method_metadata(node, comments, context)
125
+ extract_instance_method_metadata(node, context)
103
126
  when :defs
104
- extract_singleton_method_metadata(node, comments, context)
127
+ extract_singleton_method_metadata(node, context)
105
128
  else
106
129
  [context, nil]
107
130
  end
108
131
  end
109
132
 
110
133
  sig do
111
- params(node: Parser::AST::Node, comments: T::Array[Parser::Source::Comment], context: String)
134
+ params(node: Parser::AST::Node, context: String)
112
135
  .returns([String, T.nilable(SemanticNode)])
113
136
  end
114
- def extract_class_metadata(node, comments, context)
137
+ def extract_class_metadata(node, context)
115
138
  const_node = T.cast(node.children[0], Parser::AST::Node)
116
139
  const_node_name = T.cast(T.cast(const_node.loc, Parser::Source::Map::Constant).name, Parser::Source::Range)
117
140
  name = T.cast(const_node_name.source, String)
118
141
  ctx = context.empty? ? name : "#{context}::#{name}"
119
- [ctx, build_node(node, comments, ctx, node.type.to_s.capitalize)]
142
+ [ctx, build_node(node, ctx, node.type.to_s.capitalize)]
120
143
  end
121
144
 
122
145
  sig do
123
- params(node: Parser::AST::Node, comments: T::Array[Parser::Source::Comment], context: String)
146
+ params(node: Parser::AST::Node, context: String)
124
147
  .returns([String, T.nilable(SemanticNode)])
125
148
  end
126
- def extract_instance_method_metadata(node, comments, context)
149
+ def extract_instance_method_metadata(node, context)
127
150
  name = T.cast(node.children.first, Symbol).to_s
128
151
  ctx = context.empty? ? "##{name}" : "#{context}##{name}"
129
- [ctx, build_node(node, comments, ctx, 'Instance Method')]
152
+ [ctx, build_node(node, ctx, 'Instance Method')]
130
153
  end
131
154
 
132
155
  sig do
133
- params(node: Parser::AST::Node, comments: T::Array[Parser::Source::Comment], context: String)
156
+ params(node: Parser::AST::Node, context: String)
134
157
  .returns([String, T.nilable(SemanticNode)])
135
158
  end
136
- def extract_singleton_method_metadata(node, comments, context)
159
+ def extract_singleton_method_metadata(node, context)
137
160
  name = T.cast(node.children[1], Symbol).to_s
138
161
  ctx = context.empty? ? ".#{name}" : "#{context}.#{name}"
139
- [ctx, build_node(node, comments, ctx, 'Singleton Method')]
162
+ [ctx, build_node(node, ctx, 'Singleton Method')]
140
163
  end
141
164
 
142
165
  sig do
143
- params(node: Parser::AST::Node, comments: T::Array[Parser::Source::Comment], name: String,
166
+ params(node: Parser::AST::Node, name: String,
144
167
  type: String).returns(SemanticNode)
145
168
  end
146
- def build_node(node, comments, name, type)
169
+ def build_node(node, name, type)
147
170
  loc = T.cast(node.loc, Parser::Source::Map)
148
171
  start_ln = T.cast(loc.line, Integer)
149
172
  end_ln = T.cast(loc.last_line, Integer)
150
- bypasses = extract_bypasses(comments, start_ln, end_ln)
151
- SemanticNode.new(name: name, type: type, start_line: start_ln, end_line: end_ln, bypasses: bypasses)
152
- end
153
-
154
- sig do
155
- params(comments: T::Array[Parser::Source::Comment], start_line: Integer, end_line: Integer)
156
- .returns(T::Array[String])
157
- end
158
- def extract_bypasses(comments, start_line, end_line)
159
- matched = comments.select do |c|
160
- c_loc = T.cast(c.loc, Parser::Source::Map)
161
- c_line = T.cast(c_loc.line, Integer)
162
- c_text = T.cast(c.text, String)
163
- c_line.between?(start_line - 1, end_line + 1) && c_text.include?(':nocov:')
164
- end
165
- matched.map { |c| T.cast(c.text, String).strip }
173
+ SemanticNode.new(name: name, type: type, start_line: start_ln, end_line: end_ln, bypasses: [])
166
174
  end
167
175
  end
168
176
  end
@@ -52,8 +52,11 @@ module SimpleCov
52
52
  end
53
53
  def write_file_bypasses(buffer, file, bypasses)
54
54
  buffer.puts "### `#{file.project_filename}`"
55
- bypasses.each do |node|
56
- buffer.puts "- `#{node.name}`\n - **Bypass Present:** Contains `:nocov:` directive."
55
+ total = bypasses.size
56
+ bypasses.each_with_index do |node, idx|
57
+ buffer.puts "- `#{node.name}`\n " \
58
+ '- **Bypass Present:** Contains `:nocov:` directive artificially ' \
59
+ "ignoring coverage (Occurrence #{idx + 1} of #{total})."
57
60
  end
58
61
  buffer.puts ''
59
62
  end
@@ -107,7 +107,7 @@ module SimpleCov
107
107
  line_num = line.line_number
108
108
  text = truncate_snippet(fetch_snippet_text([line_num], source_lines), @config.max_snippet_lines)
109
109
  occurrence_str = calculate_occurrence(line_num, source_lines, node)
110
- buffer.puts " - **Line Deficit:** #{occurrence_str}`#{text}`"
110
+ buffer.puts " - **Line Deficit:** `#{text}` #{occurrence_str}".rstrip
111
111
  end
112
112
 
113
113
  sig do
@@ -26,20 +26,39 @@ module SimpleCov
26
26
  grouper = new(nodes)
27
27
  grouper.group_missed_lines(file)
28
28
  grouper.group_missed_branches(file)
29
- grouper.node_deficits
29
+ grouper.sort_deficits
30
+ end
31
+
32
+ sig { returns(T::Hash[String, DeficitGroup]) }
33
+ def sort_deficits
34
+ T.let(
35
+ @node_deficits.sort_by do |name, group|
36
+ if (node = group.semantic_node)
37
+ node.start_line
38
+ else
39
+ name.match(/\d+/)&.to_s&.to_i || Float::INFINITY
40
+ end
41
+ end.to_h,
42
+ T::Hash[String, DeficitGroup]
43
+ )
30
44
  end
31
45
 
32
46
  sig { params(file: SimpleCov::SourceFile).void }
33
47
  def group_missed_lines(file)
34
48
  file.missed_lines.each do |line|
35
- line_num = line.line_number
36
- node = @nodes.find { |n| line_num.between?(n.start_line, n.end_line) }
37
- node_name = node ? node.name : "Line #{line_num}"
38
- @node_deficits[node_name] ||= DeficitGroup.new(semantic_node: node)
39
- T.must(@node_deficits[node_name]).lines << line
49
+ add_missed_line(line)
40
50
  end
41
51
  end
42
52
 
53
+ sig { params(line: SimpleCov::SourceFile::Line).void }
54
+ def add_missed_line(line)
55
+ line_num = line.line_number
56
+ node = @nodes.reverse.find { |n| line_num.between?(n.start_line, n.end_line) }
57
+ node_name = node ? node.name : "Line #{line_num}"
58
+ @node_deficits[node_name] ||= DeficitGroup.new(semantic_node: node)
59
+ T.must(@node_deficits[node_name]).lines << line
60
+ end
61
+
43
62
  sig { params(file: SimpleCov::SourceFile).void }
44
63
  def group_missed_branches(file)
45
64
  return unless file.respond_to?(:branches) && file.branches.any?
@@ -53,7 +72,7 @@ module SimpleCov
53
72
  def add_missed_branch(branch)
54
73
  start_line = branch.start_line
55
74
  end_line = branch.end_line
56
- node = @nodes.find { |n| start_line >= n.start_line && end_line <= n.end_line }
75
+ node = @nodes.reverse.find { |n| start_line >= n.start_line && end_line <= n.end_line }
57
76
  node_name = node ? node.name : "Lines #{start_line}-#{end_line}"
58
77
  @node_deficits[node_name] ||= DeficitGroup.new(semantic_node: node)
59
78
  T.must(@node_deficits[node_name]).branches << branch
@@ -48,7 +48,7 @@ module SimpleCov
48
48
 
49
49
  occurrences, current = count_snippet_occurrences(first_line_of_snippet, line_num, source_lines, node)
50
50
 
51
- occurrences > 1 ? "(Occurrence #{current} of #{occurrences}) " : ''
51
+ occurrences > 1 ? "(Occurrence #{current} of #{occurrences})." : ''
52
52
  end
53
53
 
54
54
  sig do
@@ -2,6 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require_relative 'ast_resolver'
5
+ require 'time'
5
6
  require_relative 'markdown_builder/snippet_formatter'
6
7
  require_relative 'markdown_builder/bypass_compiler'
7
8
  require_relative 'markdown_builder/deficit_grouper'
@@ -79,7 +80,7 @@ module SimpleCov
79
80
  @buffer.puts "**Status:** #{status}"
80
81
  @buffer.puts "**Global Line Coverage:** #{@result.covered_percent.round(1)}%"
81
82
  @buffer.puts "**Global Branch Coverage:** #{calculate_branch_pct.round(1)}%"
82
- @buffer.puts "**Generated At:** #{Time.now}"
83
+ @buffer.puts "**Generated At:** #{Time.now.iso8601} (Local Timezone)"
83
84
  @buffer.puts ''
84
85
  end
85
86
 
@@ -87,18 +88,22 @@ module SimpleCov
87
88
  def calculate_branch_pct
88
89
  return 0.0 unless @result.respond_to?(:covered_branches) && @result.respond_to?(:total_branches)
89
90
 
90
- T.cast(@result.covered_branches, Float) / @result.total_branches * 100
91
- rescue StandardError
92
- 0.0
91
+ total = @result.total_branches
92
+ return 0.0 if total.to_i.zero?
93
+
94
+ covered = @result.covered_branches
95
+ covered.to_f / total * 100.0
93
96
  end
94
97
 
95
98
  # Appends a critical alert if the output hit the token-ceiling constraint and was forcibly terminated.
96
99
  sig { void }
97
100
  def write_truncation_warning
98
101
  @buffer.puts '> **[WARNING] TRUNCATION NOTIFICATION:**'
99
- msg = "> The total coverage deficit report exceeded the maximum token constraint (#{@config.max_file_size_kb} kB). " \
100
- 'The report was truncated. The deficits detailed above represent the lowest-coverage files. ' \
101
- 'Please resolve these deficits to reveal the remaining uncovered files.'
102
+ msg = '> The total coverage deficit report exceeded the maximum token ' \
103
+ "constraint (#{@config.max_file_size_kb} kB). " \
104
+ 'The report was truncated. The deficits detailed above represent ' \
105
+ 'the lowest-coverage (most critical) files. ' \
106
+ 'Please resolve these deficits to reveal the remaining uncovered files in subsequent test runs.'
102
107
  @buffer.puts msg
103
108
  end
104
109
  end
@@ -8,7 +8,7 @@ module SimpleCov
8
8
  class AIFormatter
9
9
  # The semantic version identifier for the gem, used for dependency resolution
10
10
  # and enforcing compatibility across upgrades.
11
- VERSION = T.let('0.10.3', String)
11
+ VERSION = T.let('0.10.4', String)
12
12
  end
13
13
  end
14
14
  end
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simplecov-ai
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.3
4
+ version: 0.10.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vitalii Lazebnyi
@@ -336,7 +336,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
336
336
  requirements:
337
337
  - - ">="
338
338
  - !ruby/object:Gem::Version
339
- version: 3.0.0
339
+ version: 2.7.0
340
340
  required_rubygems_version: !ruby/object:Gem::Requirement
341
341
  requirements:
342
342
  - - ">="
metadata.gz.sig CHANGED
Binary file