yard-rspec-coverage 0.1.3 → 0.2.0
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/CHANGELOG.md +33 -0
- data/README.md +34 -16
- data/lib/tasks/yard_rspec.rake +5 -47
- data/lib/yard-rspec-coverage.rb +5 -0
- data/lib/yard_rspec/ast_helpers.rb +26 -0
- data/lib/yard_rspec/coverage.rb +14 -8
- data/lib/yard_rspec/describe_problems.rb +8 -0
- data/lib/yard_rspec/example_extractor.rb +43 -72
- data/lib/yard_rspec/handler.rb +55 -114
- data/lib/yard_rspec/method_description.rb +14 -12
- data/lib/yard_rspec/nested_method_describes.rb +48 -0
- data/lib/yard_rspec/parsed_describe.rb +70 -0
- data/lib/yard_rspec/spec_objects.rb +28 -0
- data/lib/yard_rspec/task_reports.rb +52 -0
- data/lib/yard_rspec/unquote.rb +12 -0
- data/templates/default/method_details/html/setup.rb +4 -4
- metadata +9 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 92e79bd5a908db3cedd4c50b47932d17429ee4ea97be3bf4fc9051925f07214e
|
|
4
|
+
data.tar.gz: 456788b68888517279dc5aa9362d4512cc147499fba916e0ba2a3dc45d2ee723
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c70ce69ec8af7fcd6490acbd4f135be002a87241b6ef66da05caebd5ce8d4c7aa44ee52c8a75949dc35abc405b1419a44e3a46f6cc3f5e688f86a84b2aa6e09d
|
|
7
|
+
data.tar.gz: 78ad8e219a770c7c8aeecf1b7ef8771e4222bc893764f0e8c67bc2c669af4936095bcb52886ddf1345df83ebb16686de75a56afeb527bc2ac5573563b26cef3e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
Changed (these can change the coverage result of a project):
|
|
6
|
+
|
|
7
|
+
- `yard:rspec_coverage` parses only `YardRspec::Config.files`. The options in `.yardopts`, such
|
|
8
|
+
as `--exclude`, still apply, but the files listed there are no longer added.
|
|
9
|
+
- Classes, modules and methods that spec files define outside describe blocks, such as helpers
|
|
10
|
+
in `spec/support`, are left out of the documentation and of `yard stats`.
|
|
11
|
+
- Files ending in `_helper.rb` outside a spec directory, such as Rails view helpers, count for
|
|
12
|
+
RSpec coverage again. Spec files are the files in a `spec/` directory and files ending in
|
|
13
|
+
`_spec.rb`.
|
|
14
|
+
- An example without a block (`it 'spins twice'`) is pending, as in RSpec: it is labelled
|
|
15
|
+
"pending" and does not count as coverage.
|
|
16
|
+
- A description that is not a string literal, such as `it format('runs %s', 1)`, is shown as
|
|
17
|
+
`example`. A description with interpolation is shown as written.
|
|
18
|
+
|
|
19
|
+
Added:
|
|
20
|
+
|
|
21
|
+
- Descriptions split over several lines (`it 'spins ' \ 'twice'`) are joined, as in RSpec.
|
|
22
|
+
- `describe '.new'` and `describe '#new'` document `#initialize`, unless the class defines a
|
|
23
|
+
class method `new` itself.
|
|
24
|
+
|
|
25
|
+
Fixed:
|
|
26
|
+
|
|
27
|
+
- An exact coverage percentage failed an equal minimum, for example 57 of 100 methods against a
|
|
28
|
+
minimum of 57.
|
|
29
|
+
- A describe of a class defined as a constant (`Widget = Class.new`), a spec file that reopens a
|
|
30
|
+
class of another gem, and describes or contexts without a block or without arguments no
|
|
31
|
+
longer stop the processing of a describe.
|
|
32
|
+
- The plugin no longer keeps the syntax tree of every spec file in memory until the end of the
|
|
33
|
+
parse. In a project with about 600 spec files, peak memory dropped from 247 MB to 72 MB.
|
|
34
|
+
- Looking up the described method is faster, especially in large projects.
|
|
35
|
+
|
|
3
36
|
## 0.1.3
|
|
4
37
|
|
|
5
38
|
First public release as `yard-rspec-coverage`.
|
data/README.md
CHANGED
|
@@ -38,6 +38,10 @@ yard doc --plugin rspec-coverage 'lib/**/*.rb' 'spec/**/*.rb'
|
|
|
38
38
|
List the spec files explicitly: without file arguments (or files in `.yardopts`) YARD only parses
|
|
39
39
|
`lib/` and `app/`, so it finds no examples.
|
|
40
40
|
|
|
41
|
+
The spec files are parsed for their examples only. Classes, modules and methods they define
|
|
42
|
+
outside describe blocks, such as helpers in `spec/support`, are left out of the documentation and
|
|
43
|
+
of `yard stats`. Spec files are the files in a `spec/` directory and files ending in `_spec.rb`.
|
|
44
|
+
|
|
41
45
|
### Rake tasks
|
|
42
46
|
|
|
43
47
|
Declare the gem with `require: false` in your Gemfile, so you choose which YARD runs use the
|
|
@@ -56,9 +60,10 @@ This defines two tasks:
|
|
|
56
60
|
- `rake yard:doc` generates the documentation with the `rspec-coverage` plugin. It writes to `doc/`, like
|
|
57
61
|
other YARD tasks such as `yard:coverage`, so run it last.
|
|
58
62
|
- `rake yard:rspec_coverage` lists the public methods without RSpec examples and fails when the
|
|
59
|
-
coverage is below `minimum_rspec_coverage`. Attribute readers/writers and
|
|
60
|
-
counted. It also lists describe blocks that name a class or method that cannot be found, such
|
|
61
|
-
as a typo, without failing.
|
|
63
|
+
coverage is below `minimum_rspec_coverage`. Attribute readers/writers, aliases and code in
|
|
64
|
+
spec files are not counted. It also lists describe blocks that name a class or method that cannot be found, such
|
|
65
|
+
as a typo, without failing. It parses only `YardRspec::Config.files`: the options in
|
|
66
|
+
`.yardopts`, such as `--exclude`, apply, but the files listed there do not.
|
|
62
67
|
|
|
63
68
|
The plugin never logs YARD warnings, so it is safe in YARD runs with `--fail-on-warning`.
|
|
64
69
|
|
|
@@ -113,11 +118,14 @@ The plugin supports the following RSpec syntax:
|
|
|
113
118
|
|
|
114
119
|
- `describe ClassName, '#method_name'` for instance methods
|
|
115
120
|
- `describe ClassName, '.method_name'` for class methods
|
|
116
|
-
- `
|
|
121
|
+
- `describe ClassName, '.new'` (or `'#new'`) for the constructor, documented as `#initialize`,
|
|
122
|
+
unless the class defines a class method `new` itself
|
|
123
|
+
- `it "description"` blocks within describe blocks, also with a description split over several
|
|
124
|
+
lines as adjacent strings (`'spins ' \ 'twice'`)
|
|
117
125
|
- `specify "description"` and `example "description"` blocks (aliases for `it`)
|
|
118
|
-
- `xit`, `xspecify` and `xexample` blocks (excluded examples)
|
|
119
|
-
|
|
120
|
-
specs.
|
|
126
|
+
- `xit`, `xspecify` and `xexample` blocks (excluded examples) and examples without a block
|
|
127
|
+
(`it 'spins twice'`, which RSpec reports as "Not yet implemented"), labelled "pending". They do
|
|
128
|
+
not count as coverage: a method with only pending examples is reported as without specs.
|
|
121
129
|
- nested `context "..."` and `describe "..."` blocks, shown as groups of examples
|
|
122
130
|
|
|
123
131
|
### Not supported
|
|
@@ -142,20 +150,21 @@ documentation and do not count towards RSpec coverage.
|
|
|
142
150
|
|
|
143
151
|
Limitations of the supported constructs:
|
|
144
152
|
|
|
145
|
-
-
|
|
146
|
-
is shown as `loop
|
|
147
|
-
|
|
153
|
+
- A description is shown as written in the spec. So interpolation stays visible
|
|
154
|
+
(`it "loop #{index}"` is shown as `loop #{index}`) and so do escape sequences (`it "a\tb"` is
|
|
155
|
+
shown as `a\tb`, where RSpec shows a tab). Examples generated in a loop are listed once.
|
|
156
|
+
- Examples whose description is not a string literal, or is empty, are shown as `example`. That
|
|
157
|
+
includes a heredoc (`it <<~DESC do`).
|
|
148
158
|
|
|
149
159
|
## Development
|
|
150
160
|
|
|
151
|
-
To
|
|
161
|
+
To build the plugin's own documentation, with its specs embedded:
|
|
152
162
|
|
|
153
163
|
```bash
|
|
154
|
-
bundle install
|
|
155
164
|
./build.sh
|
|
156
165
|
```
|
|
157
166
|
|
|
158
|
-
Then open `doc/index.html` in your browser
|
|
167
|
+
Then open `doc/index.html` in your browser. `build.sh` also runs all the checks below.
|
|
159
168
|
|
|
160
169
|
Run all checks (RuboCop, a dependency vulnerability audit, the specs with SimpleCov, YARD
|
|
161
170
|
documentation coverage and RSpec coverage) with:
|
|
@@ -166,11 +175,20 @@ bundle exec rake
|
|
|
166
175
|
|
|
167
176
|
### Releasing
|
|
168
177
|
|
|
169
|
-
Update the version in `yard-rspec-coverage.gemspec` and `CHANGELOG.md
|
|
178
|
+
Update the version in `yard-rspec-coverage.gemspec` and `CHANGELOG.md` (a `## <version>` heading
|
|
179
|
+
instead of `## Unreleased`), commit, and run the checks for a release:
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
bundle exec rake prerelease
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
It checks the commit, runs `rake` and `yard:doc`, builds and tests the gem, and runs mutation
|
|
186
|
+
testing. Set `COMPARE_PROJECT` (and `COMPARE_RSPEC`) to also compare the plugin with
|
|
187
|
+
`rspec --dry-run` in a real project, see `AGENTS.md`. Then push a matching tag:
|
|
170
188
|
|
|
171
189
|
```bash
|
|
172
|
-
git tag v0.
|
|
173
|
-
git push origin v0.
|
|
190
|
+
git tag v0.2.0
|
|
191
|
+
git push origin v0.2.0
|
|
174
192
|
```
|
|
175
193
|
|
|
176
194
|
The release workflow runs all checks and publishes the gem to rubygems.org with
|
data/lib/tasks/yard_rspec.rake
CHANGED
|
@@ -3,49 +3,7 @@
|
|
|
3
3
|
# The tasks load the plugin when they run; loading it here would affect every YARD run in Rake.
|
|
4
4
|
require 'yard'
|
|
5
5
|
require_relative '../yard_rspec/config'
|
|
6
|
-
|
|
7
|
-
# @param [<YARD::CodeObjects::MethodObject>] uncovered
|
|
8
|
-
def uncovered_methods_report(uncovered)
|
|
9
|
-
return if uncovered.empty?
|
|
10
|
-
|
|
11
|
-
puts "\nMethods without RSpec specs:"
|
|
12
|
-
uncovered.sort_by(&:path).each do |method|
|
|
13
|
-
puts " #{method.file}:#{method.line}: #{method.path}"
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
# @param [<YARD::CodeObjects::MethodObject>] public_lib_methods
|
|
18
|
-
# @return [Float] The coverage percentage.
|
|
19
|
-
def rspec_coverage_report(public_lib_methods)
|
|
20
|
-
total = public_lib_methods.size
|
|
21
|
-
uncovered = public_lib_methods.reject {|method| YardRspec::Coverage.covered?(method)}
|
|
22
|
-
covered = total - uncovered.size
|
|
23
|
-
coverage = total.positive? ? (covered.to_f / total * 100).round(2) : 100.0
|
|
24
|
-
|
|
25
|
-
puts format('RSpec coverage: %<covered>d/%<total>d (%<coverage>.2f%%)',
|
|
26
|
-
covered:, total:, coverage:)
|
|
27
|
-
uncovered_methods_report(uncovered)
|
|
28
|
-
|
|
29
|
-
coverage
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# Applies .yardopts like `yard doc` does, so --exclude works. --no-output skips the markup check.
|
|
33
|
-
# @param [<String>] files Glob patterns; they replace the files in .yardopts.
|
|
34
|
-
# @return [YARD::CLI::Yardoc]
|
|
35
|
-
def yardoc_with_yardopts(files)
|
|
36
|
-
yardoc = YARD::CLI::Yardoc.new
|
|
37
|
-
yardoc.parse_arguments('--no-output', *files)
|
|
38
|
-
yardoc
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
# These do not fail the coverage check.
|
|
42
|
-
# @param [<String>] describe_problems
|
|
43
|
-
def describe_problems_report(describe_problems)
|
|
44
|
-
return if describe_problems.empty?
|
|
45
|
-
|
|
46
|
-
puts "\nDescribe blocks without a matching class or method:"
|
|
47
|
-
describe_problems.each {|describe_problem| puts " #{describe_problem}"}
|
|
48
|
-
end
|
|
6
|
+
require_relative '../yard_rspec/task_reports'
|
|
49
7
|
|
|
50
8
|
namespace :yard do
|
|
51
9
|
desc 'Generate YARD documentation with the yard-rspec-coverage plugin'
|
|
@@ -59,16 +17,16 @@ namespace :yard do
|
|
|
59
17
|
task :rspec_coverage do
|
|
60
18
|
require_relative '../yard-rspec-coverage'
|
|
61
19
|
YARD::Registry.clear
|
|
62
|
-
yardoc = yardoc_with_yardopts(YardRspec::Config.files)
|
|
20
|
+
yardoc = YardRspec::TaskReports.yardoc_with_yardopts(YardRspec::Config.files)
|
|
63
21
|
YARD.parse(yardoc.files, yardoc.excluded, YARD::Logger::ERROR)
|
|
64
22
|
|
|
65
|
-
coverage =
|
|
66
|
-
|
|
23
|
+
coverage = YardRspec::TaskReports.rspec_coverage(YardRspec::Coverage.countable_methods)
|
|
24
|
+
YardRspec::TaskReports.describe_problems(YardRspec::DescribeProblems.all)
|
|
67
25
|
minimum = YardRspec::Config.minimum_rspec_coverage
|
|
68
26
|
|
|
69
27
|
if coverage < minimum
|
|
70
28
|
warn format("\nRSpec coverage too low: %<coverage>.2f%% (minimum is %<minimum>.2f%%)",
|
|
71
|
-
coverage
|
|
29
|
+
coverage: coverage.floor(2), minimum:)
|
|
72
30
|
exit 1
|
|
73
31
|
end
|
|
74
32
|
end
|
data/lib/yard-rspec-coverage.rb
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'yard'
|
|
4
|
+
require_relative 'yard_rspec/ast_helpers'
|
|
4
5
|
require_relative 'yard_rspec/config'
|
|
5
6
|
require_relative 'yard_rspec/coverage'
|
|
6
7
|
require_relative 'yard_rspec/describe_problems'
|
|
7
8
|
require_relative 'yard_rspec/example_extractor'
|
|
8
9
|
require_relative 'yard_rspec/method_description'
|
|
10
|
+
require_relative 'yard_rspec/nested_method_describes'
|
|
11
|
+
require_relative 'yard_rspec/parsed_describe'
|
|
9
12
|
require_relative 'yard_rspec/handler'
|
|
13
|
+
require_relative 'yard_rspec/spec_objects'
|
|
10
14
|
require_relative 'yard_rspec/stats'
|
|
15
|
+
require_relative 'yard_rspec/unquote'
|
|
11
16
|
|
|
12
17
|
YARD::Templates::Engine.register_template_path File.expand_path('../templates', __dir__)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YardRspec
|
|
4
|
+
# Checks on YARD's Ruby AST that the handler, the example extractor and the nested describe
|
|
5
|
+
# scanner share, so that they agree on which calls are describes.
|
|
6
|
+
module AstHelpers
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
# @return [Boolean]
|
|
10
|
+
def ast_node?(node)
|
|
11
|
+
node.is_a?(YARD::Parser::Ruby::AstNode)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @param [YARD::Parser::Ruby::MethodCallNode] call_node A command such as `describe Widget`.
|
|
15
|
+
# @return [Boolean]
|
|
16
|
+
def describe_or_context_call?(call_node)
|
|
17
|
+
%i[describe context].include?(call_node.method_name(true))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @param [YARD::Parser::Ruby::MethodCallNode] call_node A command such as `it 'spins' do end`.
|
|
21
|
+
# @return [YARD::Parser::Ruby::AstNode, nil] The statements in the block, nil without a block.
|
|
22
|
+
def block_body_of(call_node)
|
|
23
|
+
call_node.block&.last
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/yard_rspec/coverage.rb
CHANGED
|
@@ -3,27 +3,33 @@
|
|
|
3
3
|
module YardRspec
|
|
4
4
|
# Which methods count for RSpec coverage.
|
|
5
5
|
module Coverage
|
|
6
|
-
# @return [Regexp]
|
|
7
|
-
|
|
6
|
+
# @return [Regexp] Files in a spec directory or ending in _spec.rb. Not any _helper.rb, which
|
|
7
|
+
# would include Rails view helpers.
|
|
8
|
+
SPEC_FILE_PATTERN = %r{(?:^|/)spec/|_spec\.rb$}
|
|
8
9
|
|
|
9
|
-
# @
|
|
10
|
+
# @param [String] file
|
|
11
|
+
# @return [Boolean] Whether the file is spec code, which needs no specs and no documentation.
|
|
12
|
+
def self.spec_file?(file)
|
|
13
|
+
file.match?(SPEC_FILE_PATTERN)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @return [<YARD::CodeObjects::MethodObject>] The methods that need specs, see {countable?}.
|
|
10
17
|
def self.countable_methods
|
|
11
18
|
YARD::Registry.all(:method).select {|method| countable?(method)}
|
|
12
19
|
end
|
|
13
20
|
|
|
14
|
-
# Attributes and aliases have no behaviour of their own to spec.
|
|
15
21
|
# @param [YARD::CodeObjects::MethodObject] method
|
|
16
|
-
# @return [Boolean]
|
|
22
|
+
# @return [Boolean] True for a public method outside spec files. Attributes and aliases are
|
|
23
|
+
# left out: they have no behaviour of their own.
|
|
17
24
|
def self.countable?(method)
|
|
18
25
|
method.visibility == :public &&
|
|
19
|
-
!method.file.to_s
|
|
26
|
+
!spec_file?(method.file.to_s) &&
|
|
20
27
|
!method.is_attribute? &&
|
|
21
28
|
!method.is_alias?
|
|
22
29
|
end
|
|
23
30
|
|
|
24
|
-
# Pending examples (`xit`) do not run, so they do not count.
|
|
25
31
|
# @param [YARD::CodeObjects::MethodObject] method
|
|
26
|
-
# @return [Boolean]
|
|
32
|
+
# @return [Boolean] Whether it has an example that is not pending: `xit` does not run.
|
|
27
33
|
def self.covered?(method)
|
|
28
34
|
examples(method[:rspec_examples] || []).any? {|example| !example[:pending]}
|
|
29
35
|
end
|
|
@@ -20,5 +20,13 @@ module YardRspec
|
|
|
20
20
|
def self.all
|
|
21
21
|
(YARD::Registry.root[REGISTRY_KEY] || []).sort
|
|
22
22
|
end
|
|
23
|
+
|
|
24
|
+
# Records an error as a problem instead of letting YARD log it, so the parse goes on.
|
|
25
|
+
# @param [String] location 'file:line' of the describe.
|
|
26
|
+
def self.recording_failure(location)
|
|
27
|
+
yield
|
|
28
|
+
rescue StandardError => e
|
|
29
|
+
add("#{location}: could not process the describe: #{e.class}: #{e.message}")
|
|
30
|
+
end
|
|
23
31
|
end
|
|
24
32
|
end
|
|
@@ -4,11 +4,13 @@ module YardRspec
|
|
|
4
4
|
# Extracts the RSpec examples of a describe block as a tree of example and context nodes.
|
|
5
5
|
# Excluded examples (xit/xspecify/xexample) get `pending: true`.
|
|
6
6
|
class ExampleExtractor
|
|
7
|
-
|
|
8
|
-
EXAMPLE_KEYWORDS = %w[it specify example].freeze
|
|
7
|
+
include AstHelpers
|
|
9
8
|
|
|
10
|
-
# @return [<
|
|
11
|
-
PENDING_KEYWORDS = %
|
|
9
|
+
# @return [<Symbol>] The keywords of examples that RSpec excludes from the run.
|
|
10
|
+
PENDING_KEYWORDS = %i[xit xspecify xexample].freeze
|
|
11
|
+
|
|
12
|
+
# @return [<Symbol>] The keywords of all examples, including the pending ones.
|
|
13
|
+
KEYWORDS = (%i[it specify example] + PENDING_KEYWORDS).freeze
|
|
12
14
|
|
|
13
15
|
# @param [YARD::Parser::Ruby::AstNode, nil] block_node The body of a describe block.
|
|
14
16
|
def initialize(block_node)
|
|
@@ -27,8 +29,6 @@ module YardRspec
|
|
|
27
29
|
private
|
|
28
30
|
|
|
29
31
|
def traverse(node, nodes)
|
|
30
|
-
return if !ast_node?(node)
|
|
31
|
-
|
|
32
32
|
if example_call?(node)
|
|
33
33
|
nodes << build_example(node)
|
|
34
34
|
elsif context_call?(node)
|
|
@@ -38,106 +38,77 @@ module YardRspec
|
|
|
38
38
|
end
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
-
# A context without examples is left out.
|
|
41
|
+
# A context without a block or without examples is left out.
|
|
42
42
|
def add_context(node, nodes)
|
|
43
|
+
block_body = block_body_of(node) or return
|
|
43
44
|
children = []
|
|
44
|
-
traverse(
|
|
45
|
+
traverse(block_body, children)
|
|
45
46
|
return if children.empty?
|
|
46
47
|
|
|
47
|
-
nodes << { description:
|
|
48
|
+
nodes << { description: string_literal_text(node.parameters.first), children: children }
|
|
48
49
|
end
|
|
49
50
|
|
|
50
|
-
# Includes the `x` variants.
|
|
51
|
-
# @return [Boolean]
|
|
51
|
+
# @return [Boolean] Includes the `x` variants.
|
|
52
52
|
def example_call?(node)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
name_node = node.children[0]
|
|
56
|
-
name_node&.type == :ident && (EXAMPLE_KEYWORDS + PENDING_KEYWORDS).include?(name_node.source)
|
|
53
|
+
node.type == :command && KEYWORDS.include?(node.method_name(true))
|
|
57
54
|
end
|
|
58
55
|
|
|
59
56
|
# @return [Boolean]
|
|
60
57
|
def context_call?(node)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
name_node = node.children[0]
|
|
64
|
-
name_node&.type == :ident && %w[context describe].include?(name_node.source)
|
|
58
|
+
node.type == :command && describe_or_context_call?(node)
|
|
65
59
|
end
|
|
66
60
|
|
|
67
|
-
# @return [
|
|
68
|
-
|
|
69
|
-
find_string_literal(node.parameters.first)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
# @return [Hash] With `pending: true` only for the `x` variants.
|
|
61
|
+
# @return [Hash] With `pending: true` only for the `x` variants and examples without a block,
|
|
62
|
+
# which RSpec reports as pending.
|
|
73
63
|
def build_example(node)
|
|
74
64
|
example = {
|
|
75
|
-
description:
|
|
76
|
-
code: extract_code(node
|
|
65
|
+
description: string_literal_text(node.parameters.first) || 'example',
|
|
66
|
+
code: extract_code(block_body_of(node))
|
|
77
67
|
}
|
|
78
|
-
example[:pending] = true if
|
|
68
|
+
example[:pending] = true if pending_example?(node)
|
|
79
69
|
example
|
|
80
70
|
end
|
|
81
71
|
|
|
82
|
-
# @return [
|
|
83
|
-
def
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
find_string_literal(args_node)
|
|
72
|
+
# @return [Boolean]
|
|
73
|
+
def pending_example?(node)
|
|
74
|
+
PENDING_KEYWORDS.include?(node.method_name(true)) || block_body_of(node).nil?
|
|
87
75
|
end
|
|
88
76
|
|
|
89
|
-
# @
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
result = find_string_literal(child)
|
|
96
|
-
return result if result
|
|
97
|
-
end
|
|
98
|
-
nil
|
|
77
|
+
# @param [YARD::Parser::Ruby::AstNode] argument The first argument of a call.
|
|
78
|
+
# @return [String, nil] The text as written, so with interpolations such as `#{count}`; nil if
|
|
79
|
+
# empty or not a string literal, such as `format('runs %s', count)`.
|
|
80
|
+
def string_literal_text(argument)
|
|
81
|
+
text = string_text(argument)
|
|
82
|
+
text if text && !text.empty?
|
|
99
83
|
end
|
|
100
84
|
|
|
101
|
-
# @return [String, nil]
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
end
|
|
85
|
+
# @return [String, nil] Nil when the node is not a string literal. Adjacent literals
|
|
86
|
+
# (`'runs ' \ 'twice'`) are joined, as RSpec sees them.
|
|
87
|
+
def string_text(node)
|
|
88
|
+
case node.type
|
|
89
|
+
when :string_literal
|
|
90
|
+
node.children.find {|child| child.type == :string_content}.children.map(&:source).join
|
|
91
|
+
when :string_concat
|
|
92
|
+
node.children.map {|child| string_text(child)}.join
|
|
110
93
|
end
|
|
111
|
-
nil
|
|
112
94
|
end
|
|
113
95
|
|
|
114
|
-
# @
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return '' if
|
|
118
|
-
|
|
119
|
-
body = block_node.children.first
|
|
120
|
-
return '' if !ast_node?(body)
|
|
96
|
+
# @param [YARD::Parser::Ruby::AstNode, nil] block_body
|
|
97
|
+
# @return [String] Empty without a block.
|
|
98
|
+
def extract_code(block_body)
|
|
99
|
+
return '' if block_body.nil?
|
|
121
100
|
|
|
122
101
|
# The source starts at the first token; restore its indentation before dedenting.
|
|
123
|
-
dedent((' ' * column_of(
|
|
102
|
+
dedent((' ' * column_of(block_body)) + block_body.source)
|
|
124
103
|
end
|
|
125
104
|
|
|
126
|
-
# @return [Integer]
|
|
105
|
+
# @return [Integer]
|
|
127
106
|
def column_of(node)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
return 0 if file_source.nil? || start.nil? || start.zero?
|
|
131
|
-
|
|
132
|
-
newline_before = file_source.rindex("\n", start - 1)
|
|
107
|
+
start = node.source_range.first
|
|
108
|
+
newline_before = node.full_source.rindex("\n", start - 1)
|
|
133
109
|
newline_before.nil? ? start : start - newline_before - 1
|
|
134
110
|
end
|
|
135
111
|
|
|
136
|
-
# @return [Boolean]
|
|
137
|
-
def ast_node?(node)
|
|
138
|
-
node.is_a?(YARD::Parser::Ruby::AstNode)
|
|
139
|
-
end
|
|
140
|
-
|
|
141
112
|
# @return [String]
|
|
142
113
|
def dedent(source)
|
|
143
114
|
lines = source.lines.map(&:rstrip)
|
data/lib/yard_rspec/handler.rb
CHANGED
|
@@ -2,39 +2,54 @@
|
|
|
2
2
|
|
|
3
3
|
module YardRspec
|
|
4
4
|
# Attaches the examples of RSpec describe blocks to the described methods, as :rspec_examples.
|
|
5
|
-
# When parsing a file list,
|
|
6
|
-
# unknown namespaces, so a spec may be parsed before its class.
|
|
5
|
+
# When parsing a file list, the examples are attached after all files: YARD postpones files with
|
|
6
|
+
# unknown namespaces, so a spec may be parsed before its class. The syntax tree is read right
|
|
7
|
+
# away, so that a waiting describe (a ParsedDescribe) does not keep it alive.
|
|
7
8
|
class DescribeHandler < YARD::Handlers::Ruby::Base
|
|
9
|
+
include AstHelpers
|
|
10
|
+
|
|
8
11
|
handles method_call(:describe)
|
|
9
12
|
handles method_call(:context)
|
|
10
13
|
namespace_only
|
|
11
14
|
|
|
12
|
-
#
|
|
15
|
+
# @return [Regexp] A class name such as 'Widget' or 'Shop::Widget', not a topic such as
|
|
16
|
+
# 'Spinning semantics'.
|
|
17
|
+
CLASS_NAME = /\A[A-Z][A-Za-z0-9_:]*\z/
|
|
18
|
+
|
|
19
|
+
# Deferred while parsing a file list, see the callbacks at the end of this file. There a
|
|
20
|
+
# failure is recorded as a describe problem, so it does not abort the parse.
|
|
13
21
|
def process
|
|
14
|
-
|
|
15
|
-
if
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
22
|
+
deferred = globals.yard_rspec_deferred_describes
|
|
23
|
+
return parse_describe&.attach if deferred.nil?
|
|
24
|
+
|
|
25
|
+
DescribeProblems.recording_failure(location_of) do
|
|
26
|
+
parsed = parse_describe
|
|
27
|
+
deferred << parsed if parsed
|
|
19
28
|
end
|
|
20
29
|
end
|
|
21
30
|
|
|
22
31
|
private
|
|
23
32
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return if target_namespace.nil?
|
|
33
|
+
# Method-only describes (describe '#eql?') are handled by their outer describe.
|
|
34
|
+
# @return [ParsedDescribe, nil] Nil for a describe without a class name.
|
|
35
|
+
def parse_describe
|
|
36
|
+
class_name = described_name
|
|
37
|
+
return nil if class_name.nil? || !class_name.match?(CLASS_NAME)
|
|
30
38
|
|
|
31
39
|
description = extract_method_description
|
|
32
|
-
if description
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
entries = if description
|
|
41
|
+
[entry(description, block_body_of(statement), statement.line)]
|
|
42
|
+
else
|
|
43
|
+
nested_entries
|
|
44
|
+
end
|
|
45
|
+
ParsedDescribe.new(namespace: namespace, class_name: class_name, location: location_of,
|
|
46
|
+
entries: entries)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @return [String, nil] The first describe parameter; nil for `describe do`.
|
|
50
|
+
def described_name
|
|
51
|
+
first_parameter = statement.parameters.first
|
|
52
|
+
Unquote.call(first_parameter.source) if ast_node?(first_parameter)
|
|
38
53
|
end
|
|
39
54
|
|
|
40
55
|
# @return [MethodDescription, nil] From the second describe parameter.
|
|
@@ -42,108 +57,30 @@ module YardRspec
|
|
|
42
57
|
second_param = statement.parameters[1]
|
|
43
58
|
return nil if !ast_node?(second_param)
|
|
44
59
|
|
|
45
|
-
MethodDescription.parse(
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# Records a describe problem for an unknown class, unless it is loaded at runtime: a class
|
|
49
|
-
# from another gem (e.g. YARD::CLI::Stats) is not a typo.
|
|
50
|
-
# @return [YARD::CodeObjects::Base, nil]
|
|
51
|
-
def resolve_namespace(obj_name)
|
|
52
|
-
# Not a constant, e.g. describe 'Partial Order Reduction semantics'.
|
|
53
|
-
return nil if !obj_name.match?(/\A[A-Z][A-Za-z0-9_:]*\z/)
|
|
54
|
-
|
|
55
|
-
return @describe_namespace if obj_name == @describe_namespace.name.to_s
|
|
56
|
-
|
|
57
|
-
resolved = YARD::Registry.resolve(@describe_namespace, obj_name, true)
|
|
58
|
-
if resolved.nil? && !loaded_constant?(obj_name)
|
|
59
|
-
DescribeProblems.add("#{location_of}: could not resolve class '#{obj_name}'")
|
|
60
|
-
end
|
|
61
|
-
resolved
|
|
60
|
+
MethodDescription.parse(Unquote.call(second_param.source))
|
|
62
61
|
end
|
|
63
62
|
|
|
64
|
-
# @return [
|
|
65
|
-
def
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
false
|
|
69
|
-
end
|
|
63
|
+
# @return [<ParsedDescribe::Entry>] The nested '#method' describes.
|
|
64
|
+
def nested_entries
|
|
65
|
+
block_body = block_body_of(statement)
|
|
66
|
+
return [] if block_body.nil?
|
|
70
67
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
method_obj = description.find_in(target_namespace)
|
|
74
|
-
if method_obj
|
|
75
|
-
attach_examples_to(method_obj, block_body)
|
|
76
|
-
elsif !description.imprecise?
|
|
77
|
-
add_missing_method_problem(description, obj_name, location)
|
|
68
|
+
NestedMethodDescribes.new(block_body).map do |description, nested_block_body, line|
|
|
69
|
+
entry(description, nested_block_body, line)
|
|
78
70
|
end
|
|
79
71
|
end
|
|
80
72
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
examples = ExampleExtractor.new(block_body).extract
|
|
88
|
-
method_obj[:rspec_examples] ||= []
|
|
89
|
-
method_obj[:rspec_examples].concat(examples)
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def process_nested_describes(target_namespace, obj_name)
|
|
93
|
-
block_body = statement.last&.last
|
|
94
|
-
return if block_body.nil?
|
|
95
|
-
|
|
96
|
-
each_method_describe(block_body) do |description, nested_block_body, node_line|
|
|
97
|
-
document_method(target_namespace, obj_name, description, nested_block_body,
|
|
98
|
-
location_of(node_line))
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
# Yields the description, block body and line of each nested method describe.
|
|
103
|
-
def each_method_describe(node, &)
|
|
104
|
-
return if !ast_node?(node)
|
|
105
|
-
|
|
106
|
-
if method_describe_node?(node)
|
|
107
|
-
description = MethodDescription.parse(unquote(node.parameters.first.source))
|
|
108
|
-
yield(description, node.last&.last, node.line)
|
|
109
|
-
else
|
|
110
|
-
node.children.each {|child| each_method_describe(child, &)}
|
|
111
|
-
end
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
# @return [Boolean]
|
|
115
|
-
def method_describe_node?(node)
|
|
116
|
-
return false if node.type != :command
|
|
117
|
-
|
|
118
|
-
name_node = node.children[0]
|
|
119
|
-
return false if !describe_or_context_call?(name_node)
|
|
120
|
-
|
|
121
|
-
first_param = node.parameters.first
|
|
122
|
-
return false if !ast_node?(first_param)
|
|
123
|
-
|
|
124
|
-
unquote(first_param.source).start_with?('#', '.')
|
|
125
|
-
rescue StandardError
|
|
126
|
-
false
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
# @return [Boolean]
|
|
130
|
-
def describe_or_context_call?(name_node)
|
|
131
|
-
name_node&.type == :ident && %w[describe context].include?(name_node.source)
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
# @return [Boolean]
|
|
135
|
-
def ast_node?(node)
|
|
136
|
-
node.is_a?(YARD::Parser::Ruby::AstNode)
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
# @return [String]
|
|
140
|
-
def unquote(source)
|
|
141
|
-
source.gsub(/['"]/, '')
|
|
73
|
+
# @param [MethodDescription, nil] description
|
|
74
|
+
# @param [YARD::Parser::Ruby::AstNode, nil] block_body
|
|
75
|
+
# @return [ParsedDescribe::Entry]
|
|
76
|
+
def entry(description, block_body, line)
|
|
77
|
+
ParsedDescribe::Entry.new(description, ExampleExtractor.new(block_body).extract,
|
|
78
|
+
location_of(line))
|
|
142
79
|
end
|
|
143
80
|
|
|
144
81
|
# @return [String] 'file:line', by default the line of the statement.
|
|
145
|
-
def location_of(line = statement.line
|
|
146
|
-
"#{statement.file
|
|
82
|
+
def location_of(line = statement.line)
|
|
83
|
+
"#{statement.file}:#{line}"
|
|
147
84
|
end
|
|
148
85
|
end
|
|
149
86
|
end
|
|
@@ -153,6 +90,10 @@ YARD::Parser::SourceParser.before_parse_list do |_files, global_state|
|
|
|
153
90
|
nil # Returning false would cancel the parse.
|
|
154
91
|
end
|
|
155
92
|
|
|
93
|
+
# Spec objects are removed only after the examples have been attached.
|
|
156
94
|
YARD::Parser::SourceParser.after_parse_list do |_files, global_state|
|
|
157
|
-
global_state.yard_rspec_deferred_describes.each
|
|
95
|
+
global_state.yard_rspec_deferred_describes.each do |parsed_describe|
|
|
96
|
+
YardRspec::DescribeProblems.recording_failure(parsed_describe.location) {parsed_describe.attach}
|
|
97
|
+
end
|
|
98
|
+
YardRspec::SpecObjects.remove
|
|
158
99
|
end
|
|
@@ -4,7 +4,7 @@ module YardRspec
|
|
|
4
4
|
# The method named by a describe string such as '#run', '.build' or '#initialize via Factory'.
|
|
5
5
|
class MethodDescription
|
|
6
6
|
# @return [Regexp] '#' (instance) or '.' (class), followed by the method name.
|
|
7
|
-
PATTERN =
|
|
7
|
+
PATTERN = /\A([#.])(\S+)/
|
|
8
8
|
|
|
9
9
|
# @return [String]
|
|
10
10
|
attr_reader :name
|
|
@@ -31,9 +31,8 @@ module YardRspec
|
|
|
31
31
|
@imprecise = imprecise
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
# Whether text follows the method name, as in '#initialize via SomeFactory'.
|
|
35
|
-
# is then not reported.
|
|
36
|
-
# @return [Boolean]
|
|
34
|
+
# @return [Boolean] Whether text follows the method name, as in '#initialize via SomeFactory'.
|
|
35
|
+
# A missing method is then not reported.
|
|
37
36
|
def imprecise?
|
|
38
37
|
@imprecise
|
|
39
38
|
end
|
|
@@ -43,20 +42,23 @@ module YardRspec
|
|
|
43
42
|
"#{@scope == :instance ? '#' : '.'}#{name}"
|
|
44
43
|
end
|
|
45
44
|
|
|
46
|
-
# Falls back to the other scope, because specs sometimes write '#' for class methods.
|
|
47
45
|
# @param [YARD::CodeObjects::NamespaceObject] namespace
|
|
48
|
-
# @return [YARD::CodeObjects::MethodObject, nil]
|
|
46
|
+
# @return [YARD::CodeObjects::MethodObject, nil] Searched in superclasses and mixins too. Falls
|
|
47
|
+
# back to the other scope, since specs sometimes write '#' for class methods, and from 'new'
|
|
48
|
+
# to #initialize, which YARD documents for the constructor.
|
|
49
49
|
def find_in(namespace)
|
|
50
|
-
find_with_scope(namespace, @scope) || find_with_scope(namespace, other_scope)
|
|
50
|
+
find_with_scope(namespace, @scope, name) || find_with_scope(namespace, other_scope, name) ||
|
|
51
|
+
(find_with_scope(namespace, :instance, 'initialize') if name == 'new')
|
|
51
52
|
end
|
|
52
53
|
|
|
53
54
|
private
|
|
54
55
|
|
|
55
|
-
# Includes mixins, so examples end up on the module's method. Own methods
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
.
|
|
56
|
+
# Includes superclasses and mixins, so examples end up on the module's method. Own methods
|
|
57
|
+
# come first, found directly: the full list takes time quadratic in the number of methods.
|
|
58
|
+
def find_with_scope(namespace, scope, method_name)
|
|
59
|
+
namespace.child(name: method_name.to_sym, scope: scope, type: :method) ||
|
|
60
|
+
namespace.meths(scope: scope, visibility: %i[public protected private], included: true)
|
|
61
|
+
.find {|method| method.name.to_s == method_name}
|
|
60
62
|
end
|
|
61
63
|
|
|
62
64
|
def other_scope
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YardRspec
|
|
4
|
+
# The `describe '#method'` and `describe '.method'` blocks nested in a describe block.
|
|
5
|
+
class NestedMethodDescribes
|
|
6
|
+
include Enumerable
|
|
7
|
+
include AstHelpers
|
|
8
|
+
|
|
9
|
+
# @param [YARD::Parser::Ruby::AstNode] block_body The body of the outer describe block.
|
|
10
|
+
def initialize(block_body)
|
|
11
|
+
@block_body = block_body
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @yieldparam [MethodDescription, nil] description Nil if the string names no method, as in
|
|
15
|
+
# `describe '#'`.
|
|
16
|
+
# @yieldparam [YARD::Parser::Ruby::AstNode, nil] block_body The body of the nested block, nil
|
|
17
|
+
# without a block.
|
|
18
|
+
# @yieldparam [Integer] line
|
|
19
|
+
def each(&)
|
|
20
|
+
return enum_for(:each) if !block_given?
|
|
21
|
+
|
|
22
|
+
walk(@block_body, &)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# Does not look inside a method describe.
|
|
28
|
+
def walk(node, &)
|
|
29
|
+
if method_describe_node?(node)
|
|
30
|
+
description = MethodDescription.parse(source_of(node.parameters.first))
|
|
31
|
+
yield(description, block_body_of(node), node.line)
|
|
32
|
+
else
|
|
33
|
+
node.children.each {|child| walk(child, &)}
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @return [Boolean]
|
|
38
|
+
def method_describe_node?(node)
|
|
39
|
+
node.type == :command && describe_or_context_call?(node) &&
|
|
40
|
+
source_of(node.parameters.first).start_with?('#', '.')
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @return [String] Without quotes.
|
|
44
|
+
def source_of(node)
|
|
45
|
+
Unquote.call(node.source)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YardRspec
|
|
4
|
+
# What DescribeHandler takes from a describe block's syntax tree: the class name and, per
|
|
5
|
+
# described method, its examples. It keeps no syntax tree, so a describe that waits for the end
|
|
6
|
+
# of a file list parse does not keep its spec file's tree alive (about 0.25 MB per spec file).
|
|
7
|
+
class ParsedDescribe
|
|
8
|
+
# One described method, with its examples.
|
|
9
|
+
# @attr [MethodDescription, nil] description Nil if the describe string names no method, as in
|
|
10
|
+
# `describe '#'`.
|
|
11
|
+
# @attr [<Hash>] examples See ExampleExtractor#extract.
|
|
12
|
+
# @attr [String] location 'file:line' of the describe of the method.
|
|
13
|
+
Entry = Struct.new(:description, :examples, :location)
|
|
14
|
+
|
|
15
|
+
# @return [String] 'file:line' of the describe.
|
|
16
|
+
attr_reader :location
|
|
17
|
+
|
|
18
|
+
# @param [YARD::CodeObjects::NamespaceObject] namespace Where the describe is; the class name
|
|
19
|
+
# is resolved from there.
|
|
20
|
+
# @param [String] class_name As written, such as 'Widget' or 'Shop::Widget'.
|
|
21
|
+
# @param [String] location 'file:line' of the describe.
|
|
22
|
+
# @param [<Entry>] entries
|
|
23
|
+
def initialize(namespace:, class_name:, location:, entries:)
|
|
24
|
+
@namespace = namespace
|
|
25
|
+
@class_name = class_name
|
|
26
|
+
@location = location
|
|
27
|
+
@entries = entries
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Attaches the examples to the described methods, or records describe problems. Call it once
|
|
31
|
+
# the described classes are in the registry.
|
|
32
|
+
def attach
|
|
33
|
+
target = resolve_namespace or return
|
|
34
|
+
@entries.each {|entry| attach_entry(target, entry)}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
# @return [YARD::CodeObjects::NamespaceObject, nil] Nil for an unknown class, recorded as a
|
|
40
|
+
# problem unless it is loaded at runtime (a class of another gem is no typo), and for a
|
|
41
|
+
# constant such as `Widget = Class.new`, whose methods YARD does not know.
|
|
42
|
+
def resolve_namespace
|
|
43
|
+
resolved = YARD::Registry.resolve(@namespace, @class_name, true)
|
|
44
|
+
if resolved.nil? && !loaded_constant?
|
|
45
|
+
DescribeProblems.add("#{@location}: could not resolve class '#{@class_name}'")
|
|
46
|
+
end
|
|
47
|
+
resolved if resolved.is_a?(YARD::CodeObjects::NamespaceObject)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @return [Boolean]
|
|
51
|
+
def loaded_constant?
|
|
52
|
+
Object.const_defined?(@class_name)
|
|
53
|
+
rescue NameError
|
|
54
|
+
false
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# A missing method is a describe problem, unless the description is imprecise.
|
|
58
|
+
def attach_entry(target, entry)
|
|
59
|
+
description = entry.description
|
|
60
|
+
if description.nil?
|
|
61
|
+
DescribeProblems.add("#{entry.location}: could not parse the method name of the describe")
|
|
62
|
+
elsif (method_obj = description.find_in(target))
|
|
63
|
+
(method_obj[:rspec_examples] ||= []).concat(entry.examples)
|
|
64
|
+
elsif !description.imprecise?
|
|
65
|
+
DescribeProblems.add("#{entry.location}: could not find method '#{description}' "\
|
|
66
|
+
"on '#{@class_name}'")
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YardRspec
|
|
4
|
+
# The classes, modules and methods that spec files define outside describe blocks, such as
|
|
5
|
+
# helpers in spec/support. YARD parses the spec files for their examples, but their own code is
|
|
6
|
+
# no part of the documentation, nor of `yard stats`.
|
|
7
|
+
module SpecObjects
|
|
8
|
+
# Removes objects defined only in spec files, and the spec files from the files of the others,
|
|
9
|
+
# such as a class that a spec reopens.
|
|
10
|
+
def self.remove
|
|
11
|
+
YARD::Registry.all.each do |object|
|
|
12
|
+
own_files = object.files.reject {|file, _line| Coverage.spec_file?(file)}
|
|
13
|
+
next if own_files == object.files
|
|
14
|
+
|
|
15
|
+
own_files.empty? ? delete(object) : object.files.replace(own_files)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# A namespace from another gem (`class Rack::Static::Middleware`) is a proxy without children.
|
|
20
|
+
# @param [YARD::CodeObjects::Base] object
|
|
21
|
+
def self.delete(object)
|
|
22
|
+
namespace = object.namespace
|
|
23
|
+
namespace.children.delete(object) if !namespace.is_a?(YARD::CodeObjects::Proxy)
|
|
24
|
+
YARD::Registry.delete(object)
|
|
25
|
+
end
|
|
26
|
+
private_class_method :delete
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YardRspec
|
|
4
|
+
# The output and options of the `yard:rspec_coverage` task. Not loaded into the global namespace
|
|
5
|
+
# by the rake file, since that would add the methods to every Rakefile that uses the plugin.
|
|
6
|
+
module TaskReports
|
|
7
|
+
# @param [<YARD::CodeObjects::MethodObject>] uncovered
|
|
8
|
+
def self.uncovered_methods(uncovered)
|
|
9
|
+
return if uncovered.empty?
|
|
10
|
+
|
|
11
|
+
puts "\nMethods without RSpec specs:"
|
|
12
|
+
uncovered.sort_by(&:path).each do |method|
|
|
13
|
+
puts " #{method.file}:#{method.line}: #{method.path}"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
private_class_method :uncovered_methods
|
|
17
|
+
|
|
18
|
+
# @param [<YARD::CodeObjects::MethodObject>] public_lib_methods
|
|
19
|
+
# @return [Float] The unrounded coverage percentage: rounding could pass a minimum of 100. It
|
|
20
|
+
# is printed rounded down, so below 100 never shows as 100.00%.
|
|
21
|
+
def self.rspec_coverage(public_lib_methods)
|
|
22
|
+
total = public_lib_methods.size
|
|
23
|
+
uncovered = public_lib_methods.reject {|method| YardRspec::Coverage.covered?(method)}
|
|
24
|
+
covered = total - uncovered.size
|
|
25
|
+
coverage = total.positive? ? covered * 100.0 / total : 100.0
|
|
26
|
+
|
|
27
|
+
puts format('RSpec coverage: %<covered>d/%<total>d (%<coverage>.2f%%)',
|
|
28
|
+
covered:, total:, coverage: coverage.floor(2))
|
|
29
|
+
uncovered_methods(uncovered)
|
|
30
|
+
|
|
31
|
+
coverage
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param [<String>] files Glob patterns; they replace the files in .yardopts, which YARD would
|
|
35
|
+
# otherwise add them to.
|
|
36
|
+
# @return [YARD::CLI::Yardoc] With the options of .yardopts (so --exclude works) and no output.
|
|
37
|
+
def self.yardoc_with_yardopts(files)
|
|
38
|
+
yardoc = YARD::CLI::Yardoc.new
|
|
39
|
+
yardoc.parse_arguments('--no-output')
|
|
40
|
+
yardoc.files = files
|
|
41
|
+
yardoc
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @param [<String>] describe_problems Reported only; they do not fail the check.
|
|
45
|
+
def self.describe_problems(describe_problems)
|
|
46
|
+
return if describe_problems.empty?
|
|
47
|
+
|
|
48
|
+
puts "\nDescribe blocks without a matching class or method:"
|
|
49
|
+
describe_problems.each {|describe_problem| puts " #{describe_problem}"}
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YardRspec
|
|
4
|
+
# Removes the quotes around the source of a string literal.
|
|
5
|
+
module Unquote
|
|
6
|
+
# @param [String] source For example `"'#run'"`.
|
|
7
|
+
# @return [String] Without the surrounding quotes only: `"it's"` stays `it's`.
|
|
8
|
+
def self.call(source)
|
|
9
|
+
source.sub(/\A(['"])(.*)\1\z/m, '\\2')
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -21,15 +21,15 @@ end
|
|
|
21
21
|
# @return [String]
|
|
22
22
|
def rspec_example_html(example)
|
|
23
23
|
pending = example[:pending] ? ' <span class="rspec_label rspec_pending">pending</span>' : ''
|
|
24
|
-
"<li><details><summary>#{h(example[:description])}#{pending}</summary>"
|
|
25
|
-
"<pre class=\"example code\">#{html_syntax_highlight(example[:code], :ruby)}</pre>"
|
|
24
|
+
"<li><details><summary>#{h(example[:description])}#{pending}</summary>"\
|
|
25
|
+
"<pre class=\"example code\">#{html_syntax_highlight(example[:code], :ruby)}</pre>"\
|
|
26
26
|
'</details></li>'
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
# @param [{description: String, children: [<Hash>]}] group
|
|
30
30
|
# @return [String]
|
|
31
31
|
def rspec_group_html(group)
|
|
32
|
-
'<li class="rspec_context">'
|
|
33
|
-
"<span class=\"rspec_context_name\">#{h(group[:description])}</span>"
|
|
32
|
+
'<li class="rspec_context">'\
|
|
33
|
+
"<span class=\"rspec_context_name\">#{h(group[:description])}</span>"\
|
|
34
34
|
"<ul class=\"rspec\">#{rspec_nodes_html(group[:children])}</ul></li>"
|
|
35
35
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: yard-rspec-coverage
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- vdbijl
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-11-04 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: yard
|
|
@@ -36,14 +36,20 @@ files:
|
|
|
36
36
|
- README.md
|
|
37
37
|
- lib/tasks/yard_rspec.rake
|
|
38
38
|
- lib/yard-rspec-coverage.rb
|
|
39
|
+
- lib/yard_rspec/ast_helpers.rb
|
|
39
40
|
- lib/yard_rspec/config.rb
|
|
40
41
|
- lib/yard_rspec/coverage.rb
|
|
41
42
|
- lib/yard_rspec/describe_problems.rb
|
|
42
43
|
- lib/yard_rspec/example_extractor.rb
|
|
43
44
|
- lib/yard_rspec/handler.rb
|
|
44
45
|
- lib/yard_rspec/method_description.rb
|
|
46
|
+
- lib/yard_rspec/nested_method_describes.rb
|
|
47
|
+
- lib/yard_rspec/parsed_describe.rb
|
|
45
48
|
- lib/yard_rspec/rake_tasks.rb
|
|
49
|
+
- lib/yard_rspec/spec_objects.rb
|
|
46
50
|
- lib/yard_rspec/stats.rb
|
|
51
|
+
- lib/yard_rspec/task_reports.rb
|
|
52
|
+
- lib/yard_rspec/unquote.rb
|
|
47
53
|
- templates/default/fulldoc/html/css/rspec.css
|
|
48
54
|
- templates/default/fulldoc/html/js/rspec.js
|
|
49
55
|
- templates/default/layout/html/setup.rb
|
|
@@ -72,7 +78,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
72
78
|
- !ruby/object:Gem::Version
|
|
73
79
|
version: '0'
|
|
74
80
|
requirements: []
|
|
75
|
-
rubygems_version: 3.
|
|
81
|
+
rubygems_version: 3.5.22
|
|
76
82
|
signing_key:
|
|
77
83
|
specification_version: 4
|
|
78
84
|
summary: YARD plugin to display RSpec examples in documentation and report RSpec coverage
|