├── .rubocop_todo.yml ├── .rspec ├── .yardopts ├── Gemfile ├── .gitignore ├── .codeclimate.yml ├── examples ├── input │ ├── unparseable.rb │ ├── circular_ref.rb │ └── lot_of_errors.rb ├── bare_log.rb └── formatted_log.rb ├── lib ├── yard-junk │ ├── version.rb │ ├── rake.rb │ ├── command_line.rb │ ├── janitor │ │ ├── text_reporter.rb │ │ ├── yard_options.rb │ │ ├── base_reporter.rb │ │ ├── html_reporter.rb │ │ └── resolver.rb │ ├── logger │ │ ├── spellcheck.rb │ │ └── message.rb │ ├── logger.rb │ └── janitor.rb └── yard-junk.rb ├── Rakefile ├── spec ├── spec_helper.rb ├── .rubocop.yml └── yard-junk │ ├── janitor │ ├── html_reporter_spec.rb │ ├── resolver_spec.rb │ └── text_reporter_spec.rb │ ├── logger │ └── message_spec.rb │ ├── logger_spec.rb │ ├── janitor_spec.rb │ └── integration │ └── inject_in_parsing_spec.rb ├── .github └── workflows │ └── ci.yml ├── .rubocop.yml ├── Changelog.md ├── yard-junk.gemspec ├── exe └── yard-junk ├── Gemfile.lock └── README.md /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | -r ./spec/spec_helper 2 | --color 3 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --markup markdown 2 | -e ./lib/yard-junk.rb 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | doc 2 | .yardoc 3 | examples/doc 4 | tmp 5 | pkg 6 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | rubocop: 3 | enabled: true 4 | channel: rubocop-0-49 5 | -------------------------------------------------------------------------------- /examples/input/unparseable.rb: -------------------------------------------------------------------------------- 1 | class 2 | def m1 3 | end 4 | end 5 | 6 | class B 7 | end 8 | -------------------------------------------------------------------------------- /examples/bare_log.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | exec('cd examples; yard doc -o doc/ --no-stats input/*.rb') 4 | -------------------------------------------------------------------------------- /examples/input/circular_ref.rb: -------------------------------------------------------------------------------- 1 | class Foo 2 | # @param (see #b) 3 | def a; end 4 | # @param (see #a) 5 | def b; end 6 | end 7 | -------------------------------------------------------------------------------- /examples/formatted_log.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | exec('cd examples; yard doc -o doc/ --no-stats -e ../lib/junk_yard.rb input/*.rb') 4 | 5 | 6 | -------------------------------------------------------------------------------- /lib/yard-junk/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module YardJunk 4 | # @private 5 | MAJOR = 0 6 | # @private 7 | MINOR = 0 8 | # @private 9 | PATCH = 10 10 | 11 | # @private 12 | VERSION = [MINOR, MAJOR, PATCH].join('.') 13 | end 14 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'rubygems/tasks' 3 | Gem::Tasks.new 4 | 5 | require_relative 'lib/yard-junk/rake' 6 | YardJunk::Rake.define_task 7 | 8 | require 'rspec/core/rake_task' 9 | RSpec::Core::RakeTask.new 10 | 11 | require 'rubocop/rake_task' 12 | RuboCop::RakeTask.new 13 | 14 | task default: %w[spec rubocop yard:junk] 15 | -------------------------------------------------------------------------------- /lib/yard-junk.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'yard' 4 | require_relative 'yard-junk/version' 5 | require_relative 'yard-junk/logger' 6 | require_relative 'yard-junk/command_line' 7 | require_relative 'yard-junk/janitor' 8 | 9 | YARD::Logger.prepend YardJunk::Logger::Mixin 10 | YARD::CLI::Command.prepend YardJunk::CommandLineOptions 11 | -------------------------------------------------------------------------------- /lib/yard-junk/rake.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module YardJunk 4 | module Rake 5 | extend ::Rake::DSL 6 | 7 | def self.define_task(*args) 8 | desc 'Check the junk in your YARD Documentation' 9 | task('yard:junk') do 10 | require 'yard' 11 | require_relative '../yard-junk' 12 | args = :text if args.empty? 13 | exit Janitor.new.run.report(*args) 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rspec/its' 4 | require 'fakefs/spec_helpers' 5 | require 'saharspec' 6 | 7 | # Imitating YARD's core_ext/file.rb 8 | module FakeFS 9 | class File 10 | def self.cleanpath(path) 11 | path 12 | end 13 | 14 | def self.read_binary(file) 15 | File.binread(file) 16 | end 17 | end 18 | end 19 | 20 | $LOAD_PATH.unshift 'lib' 21 | 22 | require 'yard' 23 | require 'yard-junk/version' 24 | require 'yard-junk/logger' 25 | require 'yard-junk/janitor' 26 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | main: 11 | name: >- 12 | ${{ matrix.ruby }} 13 | runs-on: ubuntu-latest 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | ruby: [ 2.7, "3.0", "3.1", "3.2", "3.3", head ] 18 | 19 | steps: 20 | - name: checkout 21 | uses: actions/checkout@v2 22 | - name: set up Ruby 23 | uses: ruby/setup-ruby@v1 24 | with: 25 | ruby-version: ${{ matrix.ruby }} 26 | 27 | - name: install dependencies 28 | run: bundle install --jobs 3 --retry 3 29 | - name: spec 30 | run: bundle exec rake spec 31 | - name: rubocop 32 | run: bundle exec rake rubocop 33 | -------------------------------------------------------------------------------- /examples/input/lot_of_errors.rb: -------------------------------------------------------------------------------- 1 | # This file tries to show as much errors in documentation as possible 2 | 3 | # Macros attached to class 4 | # @!macro [attach] attached4 5 | # $1 $2 $3 6 | class A 7 | # Wrong macro directive format: 8 | # 9 | # @!macro 10 | # 11 | # Unknown directive: 12 | # @!wtf 13 | 14 | 15 | # @wrong Free hanging unknown tag 16 | 17 | # Unknown macro name: 18 | # @!macro wtf 19 | # Points to unknown class: {B} 20 | # 21 | # @wrong This is unknown tag 22 | # 23 | # @param arg1 [C] Link to unknown class. 24 | # @param arg3 This is unknown argument. 25 | # 26 | def foo(arg1, arg2) 27 | end 28 | 29 | # @param para 30 | # @param para 31 | # @example 32 | # @see {invalid} 33 | def bar(para) 34 | end 35 | 36 | OPTIONS = %i[foo bar baz] 37 | # undocumentable attr_reader 38 | attr_reader *OPTIONS 39 | end 40 | 41 | # not recognize namespace 42 | Bar::BOOKS = 5 43 | -------------------------------------------------------------------------------- /lib/yard-junk/command_line.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module YardJunk 4 | module CommandLineOptions 5 | def common_options(opts) # rubocop:disable Metrics/MethodLength 6 | super 7 | 8 | opts.separator '' 9 | opts.separator 'YardJunk plugin options' 10 | 11 | opts.on('--junk-log-format [FMT]', 12 | 'YardJunk::Logger format string, by default ' \ 13 | "#{Logger::Message::DEFAULT_FORMAT.inspect}") do |format| 14 | Logger.instance.format = format 15 | end 16 | 17 | opts.on('--junk-log-ignore [TYPE1,TYPE2,...]', 18 | 'YardJunk::Logger message types to ignore, by default ' \ 19 | "#{Logger::DEFAULT_IGNORE.map(&:inspect).join(', ')}") do |ignore| 20 | Logger.instance.ignore = ignore.to_s.split(',') 21 | end 22 | 23 | opts.separator '' 24 | opts.separator 'Generic options' 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: "../.rubocop.yml" 2 | 3 | Style/BlockDelimiters: 4 | Enabled: false 5 | 6 | Metrics/BlockLength: 7 | Enabled: false 8 | 9 | Layout/MultilineBlockLayout: 10 | Enabled: false 11 | 12 | Layout/ParameterAlignment: 13 | EnforcedStyle: with_fixed_indentation 14 | 15 | Layout/ArgumentAlignment: 16 | EnforcedStyle: with_fixed_indentation 17 | 18 | RSpec: 19 | Language: 20 | Includes: 21 | Examples: 22 | - its_block 23 | - its_call 24 | 25 | RSpec/DescribeClass: 26 | Enabled: false 27 | 28 | RSpec/NestedGroups: 29 | Enabled: false 30 | 31 | RSpec/BeforeAfterAll: 32 | Enabled: false 33 | 34 | # RSpec/EmptyExampleGroup: 35 | # CustomIncludeMethods: [its_call, its_block] 36 | 37 | RSpec/SpecFilePathFormat: 38 | Enabled: false 39 | 40 | RSpec/ImplicitSubject: 41 | Enabled: false 42 | 43 | RSpec/ContextWording: 44 | Enabled: false 45 | 46 | RSpec/MultipleMemoizedHelpers: 47 | Enabled: false 48 | 49 | Layout/BlockEndNewline: 50 | Enabled: false 51 | 52 | # Because 53 | # expect { ... } 54 | # .to send_message.with(...) 55 | # .and send_message.with(...) 56 | Layout/MultilineMethodCallIndentation: 57 | Enabled: false 58 | 59 | Layout/LineLength: 60 | Enabled: false 61 | -------------------------------------------------------------------------------- /lib/yard-junk/janitor/text_reporter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rainbow' 4 | 5 | module YardJunk 6 | class Janitor 7 | # Reporter that just outputs everything in plaintext format. Useful 8 | # for commandline usage. See {BaseReporter} for details about reporters. 9 | # 10 | class TextReporter < BaseReporter 11 | private 12 | 13 | def _stats(**stat) 14 | @io.puts "\n#{colorized_stats(**stat)}" 15 | end 16 | 17 | def colorized_stats(errors:, problems:, duration:) 18 | colorize( 19 | format('%i failures, %i problems', errors, problems), status_color(errors, problems) 20 | ) + format(' (%s to run)', duration) 21 | end 22 | 23 | def colorize(text, color) 24 | Rainbow(text).color(color) 25 | end 26 | 27 | def status_color(errors, problems) 28 | case 29 | when errors.positive? then :red 30 | when problems.positive? then :yellow 31 | else :green 32 | end 33 | end 34 | 35 | def header(title, explanation) 36 | @io.puts 37 | @io.puts title 38 | @io.puts '-' * title.length 39 | @io.puts "#{explanation}\n\n" 40 | end 41 | 42 | def row(msg) 43 | @io.puts msg.to_s # default Message#to_s is good enough 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | require: rubocop-rspec 3 | 4 | AllCops: 5 | Include: 6 | - 'lib/**/*.rb' 7 | - 'spec/**/*.rb' 8 | Exclude: 9 | - 'vendor/**/*' 10 | - 'examples/**/*' 11 | - 'tmp/**/*' 12 | - Gemfile 13 | - Rakefile 14 | - '*.gemspec' 15 | DisplayCopNames: true 16 | TargetRubyVersion: 2.7 17 | NewCops: enable 18 | 19 | Style/PercentLiteralDelimiters: 20 | PreferredDelimiters: 21 | default: '{}' 22 | 23 | Style/SignalException: 24 | EnforcedStyle: semantic 25 | 26 | Style/RegexpLiteral: 27 | Enabled: false 28 | 29 | Style/FormatStringToken: 30 | Enabled: false 31 | 32 | Style/AndOr: 33 | EnforcedStyle: conditionals 34 | 35 | Layout/SpaceInsideHashLiteralBraces: 36 | EnforcedStyle: no_space 37 | 38 | Layout/LineLength: 39 | Max: 100 40 | AllowedPatterns: ['(^| )\# .*'] # ignore long comments 41 | 42 | Metrics/ParameterLists: 43 | CountKeywordArgs: false 44 | 45 | Style/FormatString: 46 | Enabled: false 47 | 48 | Style/EmptyCaseCondition: 49 | Enabled: false 50 | 51 | Lint/AmbiguousOperatorPrecedence: 52 | Enabled: false 53 | 54 | Lint/EmptyWhen: 55 | Enabled: false 56 | 57 | Naming/FileName: 58 | Exclude: 59 | - 'lib/yard-junk.rb' 60 | 61 | Lint/MixedRegexpCaptureTypes: 62 | Enabled: false 63 | 64 | Style/MultilineBlockChain: 65 | Enabled: false 66 | 67 | Style/BlockDelimiters: 68 | Enabled: false 69 | 70 | Style/OpenStructUse: 71 | Enabled: false 72 | 73 | # TODO 74 | Style/Documentation: 75 | Enabled: false 76 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # Yard-Junk changelog 2 | 3 | ## 0.0.10 -- 2024-09-08 4 | 5 | * Update to support newer Rubies (by [@pboling](https://github.com/pboling) at [#42](https://github.com/zverok/yard-junk/pull/42)) 6 | * Drop support for Ruby < 2.7 7 | 8 | ## 0.0.9 -- 2020-12-05 9 | 10 | * Avoid deprecation warnings ([640bc355d](https://github.com/zverok/yard-junk/commit/640bc355d156e892348b80210fc034af25e196cf)) 11 | 12 | ## 0.0.8 -- 2020-11-12 13 | 14 | * Support Ruby 2.7 (and hopefully 3.0) 15 | * Drop support for Rubies below 2.5 :shrug: 16 | 17 | ## 0.0.7 -- 2017-09-21 18 | 19 | * Fix problems with links resolution for RDoc. 20 | 21 | ## 0.0.6 -- 2017-09-20 22 | 23 | * More robust (and more logical) colorization on text output (#25); 24 | * Fast "sanity check" for using in pre-commit hook on large codebases (#24). 25 | 26 | ## 0.0.5 -- 2017-09-11 27 | 28 | * Fix gem conflict with `did_you_mean`. 29 | 30 | ## 0.0.4 -- 2017-09-09 31 | 32 | * Support for partial reports `yard-junk --path path/to/folder` (#13) 33 | 34 | ## 0.0.3 -- 2017-09-07 35 | 36 | * Wiser dependency on `did_you_mean`, should not break CIs now. 37 | * Support for Ruby 2.1 and 2.2. 38 | 39 | ## 0.0.2 -- 2017-09-03 40 | 41 | * Lots of small cleanups and enchancement of README, Gemfile and directory structure ([@olleolleolle]); 42 | * Colorized text output ([@olleolleolle]); 43 | * HTML reporter; 44 | * Options for command line and Rake task. 45 | 46 | ## 0.0.1 -- 2017-08-27 47 | 48 | Yard-Junk was born (Even RubyWeekly [#364](http://rubyweekly.com/issues/364) noticed!) 49 | -------------------------------------------------------------------------------- /spec/yard-junk/janitor/html_reporter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe YardJunk::Janitor::HtmlReporter do 4 | subject(:reporter) { described_class.new(out) } 5 | 6 | let(:out) { StringIO.new } 7 | 8 | describe 'initial' do 9 | its(:html) { is_expected.to eq described_class::HEADER } 10 | end 11 | 12 | describe '#section' do 13 | before do 14 | reporter.section( 15 | 'Section', 16 | 'Explanation', 17 | [ 18 | YardJunk::Logger::Message.new(message: 'Something bad', file: 'file.rb', line: 10), 19 | YardJunk::Logger::Message.new(message: 'Something bad', file: 'file.rb', line: 12) 20 | ] 21 | ) 22 | end 23 | 24 | its(:html) do 25 | # NB: this is not very strict test, but... enough 26 | is_expected.to include('Section') 27 | .and include('Explanation') 28 | .and include('
  • file.rb:10: Something bad
  • ') 29 | end 30 | end 31 | 32 | describe '#stats' do 33 | before do 34 | reporter.stats( 35 | errors: 3, 36 | problems: 2, 37 | duration: 5.2 38 | ) 39 | end 40 | 41 | its(:html) do 42 | is_expected 43 | .to include('3 failures') 44 | .and include('2 problems') 45 | .and include('(ready in 5 seconds)') 46 | end 47 | end 48 | 49 | describe '#finalize' do 50 | subject { reporter.finalize } 51 | 52 | its_block { 53 | is_expected 54 | .to change(reporter, :html).to(include(described_class::FOOTER)) 55 | .and send_message(out, :puts).with(%r{= 3.18' 39 | s.add_dependency 'rainbow' 40 | s.add_dependency 'ostruct' 41 | 42 | s.add_development_dependency 'rubocop' 43 | s.add_development_dependency 'rspec', '>= 3' 44 | s.add_development_dependency 'rubocop-rspec' 45 | s.add_development_dependency 'rspec-its', '~> 1' 46 | s.add_development_dependency 'saharspec' 47 | s.add_development_dependency 'fakefs' 48 | s.add_development_dependency 'simplecov', '~> 0.9' 49 | s.add_development_dependency 'rake' 50 | s.add_development_dependency 'rubygems-tasks' 51 | s.add_development_dependency 'yard' 52 | s.add_development_dependency 'kramdown' 53 | end 54 | -------------------------------------------------------------------------------- /lib/yard-junk/logger/spellcheck.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | begin 4 | require 'did_you_mean' 5 | rescue LoadError # rubocop:disable Lint/SuppressedException 6 | end 7 | 8 | module YardJunk 9 | class Logger 10 | module Spellcheck 11 | # DidYouMean changed API dramatically between 1.0 and 1.1, and different rubies have different 12 | # versions of it bundled. 13 | if !Kernel.const_defined?(:DidYouMean) 14 | def spell_check(*) 15 | [] 16 | end 17 | elsif DidYouMean.const_defined?(:SpellCheckable) # 1.0 + 18 | class SpellChecker < Struct.new(:error, :dictionary) # rubocop:disable Style/StructInheritance 19 | include DidYouMean::SpellCheckable 20 | 21 | def candidates 22 | {error => dictionary} 23 | end 24 | end 25 | 26 | def spell_check(error, dictionary) 27 | SpellChecker.new(error, dictionary).corrections 28 | end 29 | elsif DidYouMean.const_defined?(:SpellChecker) # 1.1+ 30 | def spell_check(error, dictionary) 31 | DidYouMean::SpellChecker.new(dictionary: dictionary).correct(error) 32 | end 33 | elsif DidYouMean.const_defined?(:BaseFinder) # < 1.0 34 | class SpellFinder < Struct.new(:error, :dictionary) # rubocop:disable Style/StructInheritance 35 | include DidYouMean::BaseFinder 36 | 37 | def searches 38 | {error => dictionary} 39 | end 40 | end 41 | 42 | def spell_check(error, dictionary) 43 | SpellFinder.new(error, dictionary).suggestions 44 | end 45 | else # rubocop:disable Lint/DuplicateBranch -- actually, just impossibility catcher 46 | def spell_check(*) 47 | [] 48 | end 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/yard-junk/janitor/yard_options.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module YardJunk 4 | class Janitor 5 | # Allows to properly parse `.yardopts` or other option file YARD supports and gracefully replace 6 | # or remove some of options. 7 | class YardOptions 8 | attr_reader :options, :files, :extra_files 9 | 10 | def initialize 11 | internal = Internal.new 12 | internal.parse_arguments 13 | @options = internal.option_args 14 | @files = internal.files 15 | @extra_files = internal.options.files 16 | end 17 | 18 | def set_files(*files) 19 | # TODO: REALLY fragile :( 20 | @files, @extra_files = files.partition { |f| f =~ /\.(rb|c|cxx|cpp|rake)/ } 21 | self 22 | end 23 | 24 | def remove_option(long, short = nil) 25 | [short, long].compact.each do |o| 26 | i = @options.index(o) 27 | next unless i 28 | 29 | @options.delete_at(i) 30 | @options.delete_at(i) unless @options[i].start_with?('-') # it was argument 31 | end 32 | self 33 | end 34 | 35 | def to_a 36 | (@options + @files) 37 | .tap { |res| res.push('-', *@extra_files) unless @extra_files.empty? } 38 | end 39 | 40 | # The easiest way to think like Yardoc is to become Yardoc, you know. 41 | class Internal < YARD::CLI::Yardoc 42 | attr_reader :option_args 43 | 44 | def optparse(*args) 45 | # remember all passed options... 46 | @all_args = args 47 | super 48 | end 49 | 50 | def parse_files(*args) 51 | # ...and substract what left after they were parsed as options, and only files left 52 | @option_args = @all_args - args 53 | super 54 | end 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /exe/yard-junk: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | $LOAD_PATH.unshift(File.join(File.dirname(File.expand_path(__FILE__)), '..', 'lib')) 5 | 6 | require 'yard' 7 | require 'yard-junk' 8 | require 'optparse' 9 | 10 | formatters = {} 11 | options = {} 12 | 13 | OptionParser.new do |opts| # rubocop:disable Metrics/BlockLength 14 | opts.banner = 'Usage: yard-junk [formatters]' 15 | opts.separator '' 16 | opts.separator 'Formatters' 17 | opts.separator '(you can use several at once, for example --text to print in console ' \ 18 | 'and --html to save HTML report somewhere)' 19 | opts.separator 'Default is: text formatter, printing to STDOUT.' 20 | opts.separator '' 21 | 22 | opts.on('--text [PATH]', 'Plaintext formatter (prints to STDOUT by default)') do |path| 23 | formatters[:text] = path 24 | end 25 | 26 | opts.on('--html [PATH]', 27 | 'HTML formatter, useful as CI artifact (prints to STDOUT by default)') do |path| 28 | formatters[:html] = path 29 | end 30 | 31 | opts.separator '' 32 | opts.separator 'Other options' 33 | 34 | opts.on('-f', '--path PATTERN1,PATTERN2,PATTERN3', 35 | 'Limit output only to this files. ' \ 36 | 'Can be path to file or folder, or glob pattern') do |patterns| 37 | options[:pathes] = patterns.split(',') 38 | end 39 | 40 | opts.on('-s', '--sanity', 41 | 'Sanity check for docs: just catch mistyped tags and similar stuff. ' \ 42 | 'On large codebases, MUCH faster than full check.') do 43 | options[:mode] = :sanity 44 | end 45 | 46 | opts.on_tail('-v', '--version', 'Prints version') do 47 | puts "YardJunk #{YardJunk::VERSION}" 48 | exit 49 | end 50 | 51 | opts.on_tail('-h', '--help', 'Show this message') do 52 | puts opts 53 | exit 54 | end 55 | end.parse! 56 | 57 | formatters = {text: nil} if formatters.empty? 58 | 59 | janitor = YardJunk::Janitor.new(**options) 60 | janitor.run 61 | exit janitor.report(**formatters) 62 | -------------------------------------------------------------------------------- /lib/yard-junk/logger.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'singleton' 4 | 5 | require 'backports/2.7.0/enumerable/filter_map' 6 | 7 | module YardJunk 8 | class Logger 9 | require_relative 'logger/message' 10 | 11 | include Singleton 12 | 13 | DEFAULT_IGNORE = %w[Undocumentable].freeze 14 | 15 | def messages 16 | @messages ||= [] 17 | end 18 | 19 | def register(msg, severity = :warn) 20 | message = 21 | Message.registry.filter_map { |t| 22 | t.try_parse(msg, severity: severity, file: @current_parsed_file) 23 | }.first || Message.new(message: msg, file: @current_parsed_file) 24 | messages << message 25 | puts message.to_s(@format) if output?(message) 26 | end 27 | 28 | def notify(msg) 29 | case msg 30 | when /Parsing (\w\S+)$/ 31 | # TODO: fragile regexp; cleanup it after everything is parsed. 32 | @current_parsed_file = Regexp.last_match(1) 33 | when /^Generating/ # end of parsing of any file 34 | @current_parsed_file = nil 35 | end 36 | end 37 | 38 | def clear 39 | messages.clear 40 | @format = Message::DEFAULT_FORMAT 41 | @ignore = DEFAULT_IGNORE 42 | end 43 | 44 | def format=(fmt) 45 | @format = fmt.to_s 46 | end 47 | 48 | def ignore=(list) 49 | @ignore = Array(list).map(&:to_s).each do |type| 50 | Message.valid_type?(type) or 51 | fail(ArgumentError, "Unrecognized message type to ignore: #{type}") 52 | end 53 | end 54 | 55 | private 56 | 57 | def output?(message) 58 | !@format.empty? && !@ignore.include?(message.type) 59 | end 60 | 61 | module Mixin 62 | def debug(msg) 63 | YardJunk::Logger.instance.notify(msg) 64 | super 65 | end 66 | 67 | def warn(msg) 68 | YardJunk::Logger.instance.register(msg, :warn) 69 | end 70 | 71 | def error(msg) 72 | YardJunk::Logger.instance.register(msg, :error) 73 | end 74 | 75 | def backtrace(exception, level_meth = :error) 76 | super if %i[error fatal].include?(level_meth) 77 | end 78 | end 79 | end 80 | end 81 | 82 | YardJunk::Logger.instance.clear 83 | -------------------------------------------------------------------------------- /lib/yard-junk/janitor/base_reporter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module YardJunk 4 | class Janitor 5 | # This class is a base for reporters that could be passed to 6 | # {Janitor#report}. 7 | # 8 | # Basically, the reporter should define methods: 9 | # * `header(title, explanation)` for printing problems section header; 10 | # * `row(msg)` for printing instance of {Logger::Message}; 11 | # * `_stats(**statistics)` for printing statistics. 12 | # 13 | # Reporter also could redefine `finalize()` method, if it wants to do 14 | # something at the end of a report (like "add footer and save to file"). 15 | # 16 | class BaseReporter 17 | # @overload initialize(io) 18 | # @param io [#puts] Any IO-alike object that defines `puts` method. 19 | # 20 | # @overload initialize(filename) 21 | # @param filename [String] Name of file to save the output. 22 | def initialize(io_or_filename = $stdout) 23 | @io = 24 | case io_or_filename 25 | when ->(i) { i.respond_to?(:puts) } # quacks! 26 | io_or_filename 27 | when String 28 | File.open(io_or_filename, 'w') 29 | else 30 | fail ArgumentError, "Can't create reporter with #{io_or_filename.class}" 31 | end 32 | end 33 | 34 | def finalize; end 35 | 36 | def section(title, explanation, messages) 37 | return if messages.empty? 38 | 39 | header(title, explanation) 40 | 41 | messages 42 | .sort_by { |m| [m.file || '\uFFFF', m.line || 1000, m.message] } 43 | .each(&method(:row)) 44 | end 45 | 46 | def stats(**stat) 47 | _stats(**stat.merge(duration: humanize_duration(stat[:duration]))) 48 | end 49 | 50 | private 51 | 52 | def _stats 53 | fail NotImplementedError 54 | end 55 | 56 | def header(_title, _explanation) 57 | fail NotImplementedError 58 | end 59 | 60 | def row(_message) 61 | fail NotImplementedError 62 | end 63 | 64 | def humanize_duration(duration) 65 | if duration < 60 66 | '%i seconds' % duration 67 | else 68 | '%.1f minutes' % (duration / 60) 69 | end 70 | end 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /spec/yard-junk/logger/message_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe YardJunk::Logger::Message do 4 | include FakeFS::SpecHelpers 5 | 6 | describe '.try_parse' do 7 | subject { klass.try_parse(input) } 8 | 9 | let(:klass) { 10 | Class.new(described_class) { 11 | pattern %r{^(?Unknown tag @(?\S+))( in file `(?[^`]+)` near line (?\d+))?$} 12 | } 13 | } 14 | 15 | before { allow(klass).to receive(:name).and_return('UnknownTag') } 16 | 17 | context 'when matches' do 18 | let(:input) { 'Unknown tag @wrong in file `input/lot_of_errors.rb` near line 15' } 19 | 20 | its(:to_h) { 21 | is_expected.to eq(type: 'UnknownTag', message: 'Unknown tag @wrong', tag: 'wrong', file: 'input/lot_of_errors.rb', line: 15) 22 | } 23 | end 24 | 25 | context 'when partial match' do 26 | let(:input) { 'Unknown tag @wrong' } 27 | 28 | its(:to_h) { 29 | is_expected.to eq(type: 'UnknownTag', message: 'Unknown tag @wrong', tag: 'wrong', file: nil, line: 1) 30 | } 31 | end 32 | 33 | context 'with parsing context' do 34 | subject { klass.try_parse(input, file: 'input/lot_of_errors.rb') } 35 | 36 | let(:input) { 'Unknown tag @wrong' } 37 | 38 | its(:to_h) { 39 | is_expected.to eq(type: 'UnknownTag', message: 'Unknown tag @wrong', tag: 'wrong', file: 'input/lot_of_errors.rb', line: 1) 40 | } 41 | end 42 | 43 | context 'when not matches' do 44 | let(:input) { '@param tag has unknown parameter name: arg3' } 45 | 46 | it { is_expected.to be_nil } 47 | end 48 | 49 | context 'with search_up' do 50 | let(:input) { 'Unknown tag @wrong in file `lot_of_errors.rb` near line 5' } 51 | 52 | before { 53 | klass.search_up '@%{tag}(\W|$)' 54 | File.write 'lot_of_errors.rb', %{ 55 | # @wrong 56 | # 57 | # Something else. 58 | def foo 59 | end 60 | } 61 | } 62 | 63 | its(:to_h) { 64 | is_expected.to include(file: 'lot_of_errors.rb', line: 2) 65 | } 66 | end 67 | end 68 | 69 | describe '#to_s' do 70 | context 'by default' do 71 | subject { klass.new(message: 'Unknown tag @wrong', tag: 'wrong', file: 'lot_of_errors.rb', line: 2) } 72 | 73 | let(:klass) { 74 | Class.new(described_class) 75 | } 76 | 77 | before { allow(klass).to receive(:name).and_return('UnknownTag') } 78 | 79 | its(:to_s) { is_expected.to eq 'lot_of_errors.rb:2: [UnknownTag] Unknown tag @wrong' } 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | yard-junk (0.0.10) 5 | backports (>= 3.18) 6 | ostruct 7 | rainbow 8 | yard 9 | 10 | GEM 11 | remote: https://rubygems.org/ 12 | specs: 13 | ast (2.4.2) 14 | backports (3.25.0) 15 | diff-lcs (1.5.1) 16 | docile (1.4.1) 17 | fakefs (2.5.0) 18 | io-console (0.7.2) 19 | io-console (0.7.2-java) 20 | irb (1.14.0) 21 | rdoc (>= 4.0.0) 22 | reline (>= 0.4.2) 23 | jar-dependencies (0.4.1) 24 | json (2.7.2) 25 | json (2.7.2-java) 26 | kramdown (2.4.0) 27 | rexml 28 | language_server-protocol (3.17.0.3) 29 | ostruct (0.6.0) 30 | parallel (1.26.3) 31 | parser (3.3.5.0) 32 | ast (~> 2.4.1) 33 | racc 34 | psych (5.1.2) 35 | stringio 36 | psych (5.1.2-java) 37 | jar-dependencies (>= 0.1.7) 38 | racc (1.8.1) 39 | racc (1.8.1-java) 40 | rainbow (3.1.1) 41 | rake (13.2.1) 42 | rdoc (6.7.0) 43 | psych (>= 4.0.0) 44 | regexp_parser (2.9.2) 45 | reline (0.5.10) 46 | io-console (~> 0.5) 47 | rexml (3.3.7) 48 | rspec (3.13.0) 49 | rspec-core (~> 3.13.0) 50 | rspec-expectations (~> 3.13.0) 51 | rspec-mocks (~> 3.13.0) 52 | rspec-core (3.13.1) 53 | rspec-support (~> 3.13.0) 54 | rspec-expectations (3.13.2) 55 | diff-lcs (>= 1.2.0, < 2.0) 56 | rspec-support (~> 3.13.0) 57 | rspec-its (1.3.0) 58 | rspec-core (>= 3.0.0) 59 | rspec-expectations (>= 3.0.0) 60 | rspec-mocks (3.13.1) 61 | diff-lcs (>= 1.2.0, < 2.0) 62 | rspec-support (~> 3.13.0) 63 | rspec-support (3.13.1) 64 | rubocop (1.66.1) 65 | json (~> 2.3) 66 | language_server-protocol (>= 3.17.0) 67 | parallel (~> 1.10) 68 | parser (>= 3.3.0.2) 69 | rainbow (>= 2.2.2, < 4.0) 70 | regexp_parser (>= 2.4, < 3.0) 71 | rubocop-ast (>= 1.32.2, < 2.0) 72 | ruby-progressbar (~> 1.7) 73 | unicode-display_width (>= 2.4.0, < 3.0) 74 | rubocop-ast (1.32.3) 75 | parser (>= 3.3.1.0) 76 | rubocop-rspec (3.0.4) 77 | rubocop (~> 1.61) 78 | ruby-progressbar (1.13.0) 79 | ruby2_keywords (0.0.5) 80 | rubygems-tasks (0.2.6) 81 | irb (~> 1.0) 82 | rake (>= 10.0.0) 83 | saharspec (0.0.10) 84 | ruby2_keywords 85 | simplecov (0.22.0) 86 | docile (~> 1.1) 87 | simplecov-html (~> 0.11) 88 | simplecov_json_formatter (~> 0.1) 89 | simplecov-html (0.12.3) 90 | simplecov_json_formatter (0.1.4) 91 | stringio (3.1.1) 92 | unicode-display_width (2.5.0) 93 | yard (0.9.37) 94 | 95 | PLATFORMS 96 | java 97 | ruby 98 | 99 | DEPENDENCIES 100 | fakefs 101 | kramdown 102 | rake 103 | rspec (>= 3) 104 | rspec-its (~> 1) 105 | rubocop 106 | rubocop-rspec 107 | rubygems-tasks 108 | saharspec 109 | simplecov (~> 0.9) 110 | yard 111 | yard-junk! 112 | 113 | BUNDLED WITH 114 | 2.4.22 115 | -------------------------------------------------------------------------------- /lib/yard-junk/janitor/html_reporter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'erb' 4 | require 'ostruct' 5 | 6 | module YardJunk 7 | class Janitor 8 | # Reporter that just outputs everything in HTML format. Useful 9 | # for usage with Jenkins. See {BaseReporter} for details about reporters. 10 | # 11 | class HtmlReporter < BaseReporter 12 | HEADER = <<-HTML 13 | 14 | 15 |
    16 | 17 | YARD-Junk Report 18 | 45 |
    46 | 47 |

    YARD Validation Report

    48 | HTML 49 | 50 | FOOTER = <<-HTML 51 | 52 | 53 | HTML 54 | 55 | SECTION = <<-HTML 56 |

    57 | <%= title %> 58 | (<%= explanation %>) 59 |

    60 | HTML 61 | 62 | ROW = <<-HTML 63 |
  • <%= file %>:<%= line %>: <%= message %>
  • 64 | HTML 65 | 66 | STATS = <<-HTML 67 |

    68 | <%= errors %> failures, 69 | <%= problems %> problems 70 | (ready in <%= duration %>) 71 |

    72 | HTML 73 | 74 | Helper = Class.new(OpenStruct) do 75 | def the_binding 76 | binding 77 | end 78 | end 79 | 80 | attr_reader :html 81 | 82 | def initialize(*) 83 | super 84 | @html = HEADER.dup 85 | end 86 | 87 | def finalize 88 | @html << FOOTER 89 | @io.puts(@html) 90 | end 91 | 92 | private 93 | 94 | def _stats(**stat) 95 | render(STATS, **stat) 96 | end 97 | 98 | def header(title, explanation) 99 | render(SECTION, title: title, explanation: explanation) 100 | end 101 | 102 | def row(message) 103 | render(ROW, **message.to_h) 104 | end 105 | 106 | def render(template, values) 107 | html << 108 | ERB.new(template) 109 | .result(Helper.new(values).the_binding) 110 | end 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /spec/yard-junk/logger_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe YardJunk::Logger do 4 | subject(:logger) { described_class.instance } 5 | 6 | before { 7 | logger.clear 8 | logger.format = nil 9 | } 10 | 11 | describe '#register' do 12 | before { logger.register('Unknown tag @wrong in file `input/lot_of_errors.rb` near line 26') } 13 | 14 | its(:'messages.last') { 15 | is_expected 16 | .to be_a(described_class::Message) 17 | .and have_attributes(message: 'Unknown tag @wrong', extra: {tag: '@wrong'}, file: 'input/lot_of_errors.rb', line: 26) 18 | } 19 | end 20 | 21 | describe '#notify' do 22 | context 'on parsing start' do 23 | before { 24 | logger.notify('Parsing foo/bar.rb') 25 | logger.register('Unknown tag @wrong') 26 | } 27 | 28 | its(:'messages.last') { is_expected.to have_attributes(file: 'foo/bar.rb') } 29 | end 30 | 31 | context 'on parsing end' do 32 | before { 33 | logger.notify('Parsing foo/bar.rb') 34 | logger.notify('Generating asset js/jquery.js') 35 | logger.register('Unknown tag @wrong') 36 | } 37 | 38 | its(:'messages.last') { is_expected.to have_attributes(file: nil) } 39 | end 40 | end 41 | 42 | describe '#format=' do 43 | subject { logger.register('Unknown tag @wrong in file `input/lot_of_errors.rb` near line 26') } 44 | 45 | before { logger.clear } # set format to default 46 | 47 | context 'by default' do 48 | its_block { is_expected.to output("input/lot_of_errors.rb:26: [UnknownTag] Unknown tag @wrong\n").to_stdout } 49 | end 50 | 51 | context 'non-empty format' do 52 | before { logger.format = '%{message} (%{file}:%{line})' } 53 | 54 | its_block { is_expected.to output("Unknown tag @wrong (input/lot_of_errors.rb:26)\n").to_stdout } 55 | end 56 | 57 | context 'empty format' do 58 | before { logger.format = nil } 59 | 60 | its_block { is_expected.not_to output.to_stdout } 61 | end 62 | end 63 | 64 | describe '#ignore=' do 65 | subject { 66 | logger.register('Unknown tag @wrong in file `input/lot_of_errors.rb` near line 26') 67 | logger.register(%{in YARD::Handlers::Ruby::AttributeHandler: Undocumentable OPTIONS 68 | in file 'input/lot_of_errors.rb':38: 69 | 70 | 38: attr_reader *OPTIONS}) 71 | } 72 | 73 | before { logger.clear } # Set output format to default 74 | 75 | context 'by default' do 76 | its_block { is_expected.to output("input/lot_of_errors.rb:26: [UnknownTag] Unknown tag @wrong\n").to_stdout } 77 | end 78 | 79 | context 'set ignores' do 80 | before { logger.ignore = 'UnknownTag' } 81 | 82 | its_block { is_expected.to output("input/lot_of_errors.rb:38: [Undocumentable] Undocumentable OPTIONS: `attr_reader *OPTIONS`\n").to_stdout } 83 | end 84 | 85 | context 'ignore nothing' do 86 | before { logger.ignore = nil } 87 | 88 | its_block { is_expected.to output("input/lot_of_errors.rb:26: [UnknownTag] Unknown tag @wrong\ninput/lot_of_errors.rb:38: [Undocumentable] Undocumentable OPTIONS: `attr_reader *OPTIONS`\n").to_stdout } 89 | end 90 | 91 | context 'set wrong ignores' do 92 | subject { logger.ignore = 'Unknown Tag' } 93 | 94 | its_block { is_expected.to raise_error(ArgumentError, 'Unrecognized message type to ignore: Unknown Tag') } 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /lib/yard-junk/janitor/resolver.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module YardJunk 4 | class Janitor 5 | class Resolver 6 | include YARD::Templates::Helpers::HtmlHelper 7 | include YARD::Templates::Helpers::MarkupHelper 8 | 9 | # This one is copied from real YARD output 10 | OBJECT_MESSAGE_PATTERN = "In file `%{file}':%{line}: " \ 11 | 'Cannot resolve link to %{name} from text: %{link}' 12 | 13 | # ...while this one is totally invented, YARD doesn't check file existance at all 14 | FILE_MESSAGE_PATTERN = "In file `%{file}':%{line}: File '%{name}' does not exist: %{link}" 15 | 16 | def self.resolve_all(yard_options) 17 | YARD::Registry.all.map(&:base_docstring).each { |ds| new(ds, yard_options).resolve } 18 | yard_options.files.each { |file| new(file, yard_options).resolve } 19 | end 20 | 21 | def initialize(object, yard_options) 22 | @options = yard_options 23 | case object 24 | when YARD::CodeObjects::ExtraFileObject 25 | init_file(object) 26 | when YARD::Docstring 27 | init_docstring(object) 28 | else 29 | fail "Unknown object to resolve #{object.class}" 30 | end 31 | end 32 | 33 | def resolve 34 | markup_meth = "html_markup_#{markup}" 35 | return unless respond_to?(markup_meth) 36 | 37 | send(markup_meth, @string) 38 | .gsub(%r{<(code|tt|pre)[^>]*>(.*?)}im, '') 39 | .scan(/{[^}]+}/).flatten 40 | .map(&CGI.method(:unescapeHTML)) 41 | .each(&method(:try_resolve)) 42 | end 43 | 44 | private 45 | 46 | def init_file(file) 47 | @string = file.contents 48 | @file = file.filename 49 | @line = 1 50 | @markup = markup_for_file(file.contents, file.filename) 51 | end 52 | 53 | def init_docstring(docstring) 54 | @string = docstring 55 | @root_object = docstring.object 56 | @file = @root_object.file 57 | @line = @root_object.line 58 | @markup = options.markup 59 | end 60 | 61 | attr_reader :options, :file, :line, :markup 62 | 63 | def try_resolve(link) 64 | name, _comment = link.tr('{}', '').split(/\s+/, 2) 65 | 66 | # See YARD::Templates::Helpers::BaseHelper#linkify for the source of patterns 67 | # TODO: there is also {include:}, {include:file:} and {render:} syntaxes, but I've never seen 68 | # a project using them. /shrug 69 | case name 70 | when %r{://}, /^mailto:/ # that's pattern YARD uses 71 | # do nothing, assume it is correct 72 | when /^file:(\S+?)(?:#(\S+))?$/ 73 | resolve_file(Regexp.last_match[1], link) 74 | else 75 | resolve_code_object(name, link) 76 | end 77 | end 78 | 79 | def resolve_file(name, link) 80 | return if options.files.any? { |f| f.name == name || f.filename == name } 81 | 82 | Logger.instance.register( 83 | FILE_MESSAGE_PATTERN % {file: file, line: line, name: name, link: link} 84 | ) 85 | end 86 | 87 | def resolve_code_object(name, link) 88 | resolved = YARD::Registry.resolve(@root_object, name, true, true) 89 | return unless resolved.is_a?(YARD::CodeObjects::Proxy) 90 | 91 | Logger.instance.register( 92 | OBJECT_MESSAGE_PATTERN % {file: file, line: line, name: name, link: link} 93 | ) 94 | end 95 | 96 | # Used by HtmlHelper for RDoc 97 | def object 98 | @string.object if @string.is_a?(YARD::Docstring) 99 | end 100 | 101 | # Used by HtmlHelper 102 | def serializer 103 | nil 104 | end 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /lib/yard-junk/janitor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'benchmark' 4 | require 'backports/2.3.0/enumerable/grep_v' 5 | require 'backports/2.6.0/array/to_h' 6 | 7 | module YardJunk 8 | class Janitor 9 | def initialize(mode: :full, pathes: nil) 10 | @mode = mode 11 | @files = expand_pathes(pathes) 12 | end 13 | 14 | def run(*opts) 15 | YARD::Registry.clear # Somehow loads all Ruby stdlib classes before Rake task started... 16 | Logger.instance.format = nil # Nothing shouuld be printed 17 | 18 | puts "Running YardJunk janitor (version #{YardJunk::VERSION})...\n\n" 19 | 20 | @duration = Benchmark.realtime do 21 | command = YARD::CLI::Yardoc.new 22 | command.run(*prepare_options(opts)) 23 | Resolver.resolve_all(command.options) unless mode == :sanity 24 | end 25 | 26 | self 27 | end 28 | 29 | def stats 30 | { 31 | errors: errors.count, 32 | problems: problems.count, 33 | duration: @duration || 0 34 | } 35 | end 36 | 37 | def report(*args, **opts) 38 | guess_reporters(*args, **opts).each do |reporter| 39 | reporter.section('Errors', 'severe code or formatting problems', errors) 40 | reporter.section('Problems', 'mistyped tags or other typos in documentation', problems) 41 | 42 | reporter.stats(**stats) 43 | reporter.finalize 44 | end 45 | 46 | exit_code 47 | end 48 | 49 | def exit_code 50 | case 51 | when !errors.empty? then 2 52 | when !problems.empty? then 1 53 | else 0 54 | end 55 | end 56 | 57 | private 58 | 59 | attr_reader :mode, :files, :yardopts 60 | 61 | BASE_OPTS = %w[--no-save --no-progress --no-stats --no-output --no-cache].freeze 62 | 63 | def prepare_options(opts) 64 | case 65 | when mode == :full || mode == :sanity && files.nil? 66 | [*BASE_OPTS, *opts] 67 | when mode == :sanity 68 | # TODO: specs 69 | [*BASE_OPTS, '--no-yardopts', *yardopts_with_files(files)] 70 | else 71 | fail ArgumentError, "Undefined mode: #{mode.inspect}" 72 | end 73 | end 74 | 75 | def yardopts_with_files(files) 76 | # Use all options from .yardopts file, but replace file lists 77 | YardOptions.new.remove_option('--files').set_files(*files) 78 | end 79 | 80 | def messages 81 | # FIXME: dropping Undocumentable here is not DRY 82 | @messages ||= YardJunk::Logger 83 | .instance 84 | .messages 85 | .grep_v(Logger::Undocumentable) 86 | .select { |m| !files || !m.file || files.include?(File.expand_path(m.file)) } 87 | end 88 | 89 | def errors 90 | messages.select(&:error?) 91 | end 92 | 93 | def problems 94 | messages.select(&:warn?) 95 | end 96 | 97 | def expand_pathes(pathes) 98 | return unless pathes 99 | 100 | Array(pathes) 101 | .map { |path| File.directory?(path) ? File.join(path, '**', '*.*') : path } 102 | .flat_map(&Dir.method(:[])) 103 | .map(&File.method(:expand_path)) 104 | end 105 | 106 | # TODO: specs for the logic 107 | def guess_reporters(*symbols, **symbols_with_args) 108 | symbols 109 | .to_h { |sym| [sym, nil] }.merge(symbols_with_args) 110 | .map { |sym, args| ["#{sym.to_s.capitalize}Reporter", args] } 111 | .each { |name,| 112 | Janitor.const_defined?(name) or fail(ArgumentError, "Reporter #{name} not found") 113 | } 114 | .map { |name, args| Janitor.const_get(name).new(*args) } 115 | end 116 | end 117 | end 118 | 119 | require_relative 'janitor/base_reporter' 120 | require_relative 'janitor/text_reporter' 121 | require_relative 'janitor/html_reporter' 122 | require_relative 'janitor/resolver' 123 | require_relative 'janitor/yard_options' 124 | -------------------------------------------------------------------------------- /spec/yard-junk/janitor/resolver_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe YardJunk::Janitor::Resolver do 4 | context 'escaped HTML' do 5 | subject { YardJunk::Logger.instance.messages } 6 | 7 | let(:fake_options) { OpenStruct.new(options.merge(markup: markup)) } 8 | let(:markup) { :markdown } 9 | let(:options) { {files: [readme]} } 10 | let(:readme) { 11 | # Not an instance_double, because resolver checks object class 12 | YARD::CodeObjects::ExtraFileObject.new('README.md', readme_contents).tap do |f| 13 | allow(f).to receive_messages(name: 'README', filename: 'README.md') 14 | end 15 | } 16 | let(:readme_contents) { '' } 17 | 18 | before { 19 | YARD::Registry.clear 20 | YardJunk::Logger.instance.clear 21 | YardJunk::Logger.instance.format = nil 22 | 23 | YARD.parse_string(source) 24 | described_class.resolve_all(fake_options) 25 | } 26 | 27 | context 'code objects' do 28 | let(:source) { %{ 29 | # It meant to be code: {'message' => 'test'} 30 | def foo 31 | end 32 | }} 33 | 34 | # In newer Rubies, it is "smartly" rendered with a smart quotes, in older ones it is not. Thanks, RDoc! 35 | its(:last) { is_expected.to have_attributes(message: /^Cannot resolve link to [‘']message[’'] from text: {[‘']message[’'] => [‘']test[’']}$/, line: 3) } 36 | 37 | context 'for RDoc' do 38 | let(:markup) { :rdoc } 39 | 40 | # In newer Rubies, it is "smartly" rendered with a smart quotes, in older ones it is not. Thanks, RDoc! 41 | its(:last) { is_expected.to have_attributes(message: /^Cannot resolve link to [‘']message[’'] from text: {[‘']message[’'] => [‘']test[’']}$/, line: 3) } 42 | end 43 | end 44 | 45 | context 'file:' do 46 | context 'valid' do 47 | let(:source) { %{ 48 | # {file:README.md} 49 | def foo 50 | end 51 | }} 52 | 53 | its(:last) { is_expected.to be_nil } 54 | end 55 | 56 | context 'invalid' do 57 | let(:source) { %{ 58 | # {file:GettingStarted.md} 59 | def foo 60 | end 61 | }} 62 | 63 | its(:last) { is_expected.to have_attributes(message: "File 'GettingStarted.md' does not exist: {file:GettingStarted.md}", line: 3) } 64 | end 65 | 66 | context 'existing, but not included in YARD list' 67 | context 'included in YARD list, but not existing' 68 | end 69 | 70 | context 'include:' 71 | context 'include:file:' 72 | context 'render:' 73 | 74 | context 'url' do 75 | # With RDoc as a default renderer (both for :rdoc and :markdown) on Ruby 2.6+, it will 76 | # auto-link the http://, and then YARD will fail! 77 | let(:markup) { :markdown } 78 | let(:options) { super().merge(markup_provider: :kramdown) } 79 | let(:source) { %{ 80 | # {http://google.com Google} 81 | def foo 82 | end 83 | }} 84 | 85 | its(:last) { is_expected.to be_nil } 86 | end 87 | 88 | context 'check in files' do 89 | let(:readme_contents) { 'Is it {Foo}?' } 90 | let(:source) { '' } 91 | 92 | its(:last) { is_expected.to have_attributes(message: 'Cannot resolve link to Foo from text: {Foo}') } 93 | 94 | context 'RDoc readme' do 95 | let(:readme) { 96 | # Not an instance_double, because resolver checks object class 97 | YARD::CodeObjects::ExtraFileObject.new('README.rdoc', readme_contents).tap do |f| 98 | allow(f).to receive_messages(name: 'README', filename: 'README.rdoc') 99 | end 100 | } 101 | let(:readme_contents) { 'Is it {Foo}?' } 102 | 103 | its(:last) { is_expected.to have_attributes(message: 'Cannot resolve link to Foo from text: {Foo}') } 104 | end 105 | 106 | context 'Markdown README with rdoc settings' do 107 | let(:markup) { :rdoc } 108 | let(:readme_contents) { 'Is it `{Foo}`?' } 109 | 110 | its(:last) { is_expected.to be_nil } 111 | end 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /spec/yard-junk/janitor/text_reporter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe YardJunk::Janitor::TextReporter do 4 | let(:out) { StringIO.new } 5 | let(:reporter) { described_class.new(out) } 6 | 7 | describe '#section' do 8 | subject { 9 | reporter.section( 10 | 'Section', 11 | 'Explanation', 12 | messages 13 | ) 14 | } 15 | 16 | let(:messages) { 17 | [ 18 | YardJunk::Logger::Message.new(message: 'Something bad', file: 'file.rb', line: 10), 19 | YardJunk::Logger::Message.new(message: 'Something bad', file: 'file.rb', line: 10) 20 | ] 21 | } 22 | 23 | its_block { 24 | is_expected 25 | .to send_message(out, :puts).with(no_args) 26 | .and send_message(out, :puts).with('Section') 27 | .and send_message(out, :puts).with('-------') 28 | .and send_message(out, :puts).with("Explanation\n\n") 29 | .and send_message(reporter, :row).exactly(2).times 30 | } 31 | 32 | context 'ordering' do 33 | let(:first) { YardJunk::Logger::Message.new(message: 'Something bad 1', file: 'file.rb', line: 10) } 34 | let(:second) { YardJunk::Logger::Message.new(message: 'Something bad 2', file: 'file.rb', line: 15) } 35 | let(:third) { YardJunk::Logger::Message.new(message: 'Something bad 2', file: 'other_file.rb', line: 5) } 36 | let(:messages) { [third, second, first] } 37 | 38 | its_block { 39 | is_expected 40 | .to send_message(reporter, :row).with(first).ordered 41 | .and send_message(reporter, :row).with(second).ordered 42 | .and send_message(reporter, :row).with(third).ordered 43 | } 44 | end 45 | 46 | context 'empty messages' do 47 | let(:messages) { [] } 48 | 49 | its_block { is_expected.not_to send_message(out, :puts) } 50 | end 51 | end 52 | 53 | describe '#row' do 54 | subject { reporter.send(:row, YardJunk::Logger::Message.new(message: 'Something bad', file: 'file.rb', line: 10)) } 55 | 56 | its_block { is_expected.to send_message(out, :puts).with('file.rb:10: [UnknownError] Something bad') } 57 | end 58 | 59 | describe '#stats' do 60 | subject { reporter.stats(**stats) } 61 | 62 | shared_context 'with colors' do 63 | around { |ex| 64 | prev = Rainbow.enabled 65 | Rainbow.enabled = true 66 | ex.run 67 | Rainbow.enabled = prev 68 | } 69 | end 70 | 71 | shared_context 'without colors' do 72 | around { |ex| 73 | prev = Rainbow.enabled 74 | Rainbow.enabled = false 75 | ex.run 76 | Rainbow.enabled = prev 77 | } 78 | end 79 | 80 | context 'there are errors' do 81 | let(:stats) { {errors: 3, problems: 2, duration: 5.2} } 82 | 83 | include_context 'with colors' 84 | 85 | its_block { 86 | is_expected 87 | .to send_message(out, :puts) 88 | .with("\n\e[31m3 failures, 2 problems\e[0m (5 seconds to run)") 89 | } 90 | end 91 | 92 | context 'there are problems' do 93 | let(:stats) { {errors: 0, problems: 2, duration: 5.2} } 94 | 95 | include_context 'with colors' 96 | 97 | its_block { 98 | is_expected 99 | .to send_message(out, :puts) 100 | .with("\n\e[33m0 failures, 2 problems\e[0m (5 seconds to run)") 101 | } 102 | end 103 | 104 | context 'everything is ok' do 105 | let(:stats) { {errors: 0, problems: 0, duration: 5.2} } 106 | 107 | include_context 'with colors' 108 | 109 | its_block { 110 | is_expected 111 | .to send_message(out, :puts) 112 | .with("\n\e[32m0 failures, 0 problems\e[0m (5 seconds to run)") 113 | } 114 | end 115 | 116 | context 'TTY does not support colors' do 117 | let(:stats) { {errors: 3, problems: 2, duration: 5.2} } 118 | 119 | include_context 'without colors' 120 | 121 | its_block { 122 | is_expected 123 | .to send_message(out, :puts) 124 | .with("\n3 failures, 2 problems (5 seconds to run)") 125 | } 126 | end 127 | end 128 | 129 | describe '#finalize' do 130 | subject { reporter.finalize } 131 | 132 | its_block { is_expected.not_to send_message(out, :puts) } 133 | end 134 | end 135 | -------------------------------------------------------------------------------- /spec/yard-junk/janitor_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe YardJunk::Janitor do 4 | subject(:janitor) { described_class.new } 5 | 6 | before { YardJunk::Logger.instance.clear } 7 | 8 | describe '#run' do 9 | subject { janitor.run } 10 | 11 | let(:command) { instance_double(YARD::CLI::Yardoc, run: nil, options: OpenStruct.new(files: [])) } 12 | 13 | its_block { 14 | is_expected 15 | .to send_message(YARD::Registry, :clear) 16 | .and send_message(YardJunk::Logger.instance, :format=) 17 | .with(nil).calling_original 18 | .and send_message(YARD::CLI::Yardoc, :new) 19 | .returning(command) 20 | .and send_message(command, :run) 21 | .with('--no-save', '--no-progress', '--no-stats', '--no-output', '--no-cache') 22 | .and send_message(YardJunk::Janitor::Resolver, :resolve_all) 23 | .and output("Running YardJunk janitor (version #{YardJunk::VERSION})...\n\n").to_stdout 24 | } 25 | end 26 | 27 | def data_for_report 28 | logger = YardJunk::Logger.instance 29 | logger.format = nil 30 | logger.register('Unknown tag @wrong in file `input/lot_of_errors.rb` near line 26') 31 | logger.register('@param tag has duplicate parameter name: para in file `input/lot_of_errors.rb\' near line 33') 32 | logger.register('input/circular_ref.rb:5: Detected circular reference tag in `Foo#b\', ignoring all reference tags for this object (@param).', :error) 33 | logger.register("Syntax error in `input/nested/unparseable.rb`:(3,4): syntax error, unexpected '\\n', expecting &. or :: or '[' or '.'") 34 | janitor.instance_variable_set('@duration', 250.6) # :shrug: 35 | end 36 | 37 | describe '#stats' do 38 | subject { janitor.stats } 39 | 40 | context 'initial' do 41 | it { is_expected.to eq(errors: 0, problems: 0, duration: 0) } 42 | end 43 | 44 | context 'after run' do 45 | before { data_for_report } 46 | 47 | it { is_expected.to eq(errors: 2, problems: 2, duration: 250.6) } 48 | end 49 | end 50 | 51 | describe '#exit_code' do 52 | context 'by default' do 53 | its(:exit_code) { is_expected.to eq 0 } 54 | end 55 | 56 | context 'with problems' do 57 | before { data_for_report } 58 | 59 | its(:exit_code) { is_expected.to eq 2 } 60 | end 61 | end 62 | 63 | describe '#report' do 64 | subject { janitor.report(:text) } 65 | 66 | before { 67 | data_for_report 68 | allow(YardJunk::Janitor::TextReporter).to receive(:new).and_return(reporter) 69 | } 70 | 71 | let(:reporter) { instance_double(YardJunk::Janitor::BaseReporter, section: nil, stats: nil, finalize: nil) } 72 | 73 | its_block { 74 | is_expected 75 | .to send_message(reporter, :section) 76 | .with('Errors', 'severe code or formatting problems', 77 | an_instance_of(Array).and(have_attributes(count: 2)) 78 | .and(all(be_a(YardJunk::Logger::Message))).and(all(be_error))) 79 | .and send_message(reporter, :section) 80 | .with('Problems', 'mistyped tags or other typos in documentation', 81 | an_instance_of(Array).and(have_attributes(count: 2)) 82 | .and(all(be_a(YardJunk::Logger::Message))).and(all(be_warn))) 83 | .and send_message(reporter, :stats) 84 | .with(errors: 2, problems: 2, duration: 250.6) 85 | .and send_message(reporter, :finalize) 86 | } 87 | 88 | context 'with pathes specified', skip: 'Tested manually, hard to imitate everything with FakeFS' do 89 | include FakeFS::SpecHelpers 90 | 91 | subject { janitor.report(:text, path: path) } 92 | 93 | before { 94 | # It would be fake files and folders, provided by FakeFS 95 | FileUtils.mkdir_p 'input/nested' 96 | %w[input/lot_of_errors.rb input/circular_ref.rb input/nested/unparseable.rb] 97 | .each { |path| File.write path, '---' } 98 | } 99 | 100 | context 'specific file' do 101 | let(:path) { 'input/lot_of_errors.rb' } 102 | 103 | its_block { 104 | is_expected 105 | .to send_message(reporter, :section) 106 | .with('Errors', 'severe code or formatting problems', []) 107 | .and send_message(reporter, :section) 108 | .with('Problems', 'mistyped tags or other typos in documentation', 109 | an_instance_of(Array).and(have_attributes(count: 2)) 110 | .and(all(be_a(YardJunk::Logger::Message))).and(all(be_warn))) 111 | .and send_message(reporter, :stats) 112 | .with(errors: 0, problems: 2, duration: 250.6) 113 | .and send_message(reporter, :finalize) 114 | } 115 | end 116 | 117 | context 'pattern' do 118 | before { allow(Dir).to receive(:[]).with('input/*.rb').and_return(['input/lot_of_errors.rb', 'input/circular_ref.rb']) } 119 | 120 | let(:path) { 'input/*.rb' } 121 | 122 | its_block { 123 | is_expected 124 | .to send_message(reporter, :section) 125 | .with('Errors', 'severe code or formatting problems', 126 | an_instance_of(Array).and(have_attributes(count: 1)) 127 | .and(all(be_a(YardJunk::Logger::Message))).and(all(be_error))) 128 | .and send_message(reporter, :section) 129 | .with('Problems', 'mistyped tags or other typos in documentation', 130 | an_instance_of(Array).and(have_attributes(count: 2)) 131 | .and(all(be_a(YardJunk::Logger::Message))).and(all(be_warn))) 132 | .and send_message(reporter, :stats) 133 | .with(errors: 0, problems: 2, duration: 250.6) 134 | .and send_message(reporter, :finalize) 135 | } 136 | end 137 | 138 | context 'folder' do 139 | before { allow(Dir).to receive(:[]).with('input/nested').and_return(['input/nested/unparseable.rb']) } 140 | 141 | let(:path) { 'input/nested' } 142 | 143 | its_block { 144 | is_expected 145 | .to send_message(reporter, :section) 146 | .with('Errors', 'severe code or formatting problems', 147 | an_instance_of(Array).and(have_attributes(count: 1)) 148 | .and(all(be_a(YardJunk::Logger::Message))).and(all(be_error))) 149 | .and send_message(reporter, :section) 150 | .with('Problems', 'mistyped tags or other typos in documentation', []) 151 | .and send_message(reporter, :stats) 152 | .with(errors: 1, problems: 0, duration: 250.6) 153 | .and send_message(reporter, :finalize) 154 | } 155 | end 156 | 157 | context 'array of pathes' 158 | end 159 | end 160 | end 161 | -------------------------------------------------------------------------------- /spec/yard-junk/integration/inject_in_parsing_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe 'Integration: catching errors' do 4 | # include FakeFS::SpecHelpers 5 | 6 | subject(:logger) { YardJunk::Logger.instance } 7 | 8 | before(:all) do 9 | YARD::Logger.prepend(YardJunk::Logger::Mixin) 10 | end 11 | 12 | after(:all) do 13 | YARD::Registry.clear 14 | end 15 | 16 | def parse_file(contents) 17 | # It would be "fake" file, provided by FakeFS and stored nowhere 18 | FakeFS do 19 | File.write('test.rb', contents) 20 | YARD.parse('test.rb') 21 | end 22 | end 23 | 24 | before { 25 | logger.clear 26 | logger.format = nil # do not print messages to STDOUT 27 | } 28 | 29 | shared_examples_for 'file parser' do |description, code, message| 30 | context description do 31 | let(:defaults) { {file: 'test.rb', line: 1} } 32 | 33 | before { parse_file(code) } 34 | 35 | its(:'messages.last.to_h') { is_expected 36 | .to eq(defaults.merge(message)) 37 | } 38 | end 39 | end 40 | 41 | it_behaves_like 'file parser', 'unknown tag', 42 | %{ 43 | # @hello world 44 | def foo 45 | end 46 | }, 47 | type: 'UnknownTag', 48 | message: 'Unknown tag @hello', 49 | tag: '@hello', 50 | line: 2 51 | 52 | it_behaves_like 'file parser', 'unknown tag: no location', 53 | %{ 54 | # @hello world 55 | }, 56 | type: 'UnknownTag', 57 | message: 'Unknown tag @hello', 58 | tag: '@hello' 59 | 60 | it_behaves_like 'file parser', 'unknown tag: did you mean?', 61 | %{ 62 | # @raises NoMethodError 63 | def foo 64 | end 65 | }, 66 | type: 'UnknownTag', 67 | message: 'Unknown tag @raises. Did you mean @raise?', 68 | tag: '@raises', 69 | line: 2 70 | 71 | it_behaves_like 'file parser', 'invalid tag format', 72 | %{ 73 | # @example 74 | def bar 75 | end 76 | }, 77 | type: 'InvalidTagFormat', 78 | message: 'Invalid tag format for @example', 79 | tag: '@example', 80 | line: 2 81 | 82 | it_behaves_like 'file parser', 'unknown directive', 83 | %{ 84 | # @!hello world 85 | def foo 86 | end 87 | }, 88 | type: 'UnknownDirective', 89 | message: 'Unknown directive @!hello', 90 | directive: '@!hello', 91 | line: 2 92 | 93 | it_behaves_like 'file parser', 'invalid directive format', 94 | %{ 95 | # @!macro 96 | def bar 97 | end 98 | }, 99 | type: 'InvalidDirectiveFormat', 100 | message: 'Invalid directive format for @!macro', 101 | directive: '@!macro', 102 | line: 2 103 | 104 | it_behaves_like 'file parser', 'unknown parameter', 105 | %{ 106 | # @param notaparam foo 107 | def foo(a) end 108 | }, 109 | type: 'UnknownParam', 110 | message: '@param tag has unknown parameter name: notaparam', 111 | param_name: 'notaparam', 112 | line: 2 113 | 114 | it_behaves_like 'file parser', 'unknown parameter: did you mean', 115 | %{ 116 | # @param tuples foo 117 | def foo(tuple) end 118 | }, 119 | type: 'UnknownParam', 120 | message: '@param tag has unknown parameter name: tuples. Did you mean `tuple`?', 121 | param_name: 'tuples', 122 | line: 2 123 | 124 | it_behaves_like 'file parser', 'unknown parameter without name', 125 | %{ 126 | # @param [String] 127 | def foo(a) end 128 | }, 129 | type: 'MissingParamName', 130 | message: '@param tag has empty parameter name', 131 | line: 2 132 | 133 | it_behaves_like 'file parser', 'unknown parameter for generated method', 134 | %{ 135 | # @!method join(delimiter, null_repr) 136 | # Convert the array to a string by joining 137 | # values with a delimiter (empty string by default) 138 | # and optional filler for NULL values 139 | # Translates to an `array_to_string` call 140 | # 141 | # @param [Object] delimiter 142 | # @param [Object] null 143 | # 144 | # @return [SQL::Attribute] 145 | # 146 | # @api public 147 | }, 148 | type: 'UnknownParam', 149 | message: '@param tag has unknown parameter name: null', 150 | param_name: 'null', 151 | line: 2 152 | 153 | it_behaves_like 'file parser', 'duplicate parameter', 154 | %{ 155 | # @param para 156 | # @param para 157 | def bar(para) 158 | end 159 | }, 160 | type: 'DuplicateParam', 161 | message: '@param tag has duplicate parameter name: para', 162 | param_name: 'para', 163 | line: 3 164 | 165 | syntax_error = case 166 | when RUBY_ENGINE == 'jruby' 167 | 'syntax error, unexpected end-of-file' 168 | when RUBY_VERSION >= '2.6' 169 | 'syntax error, unexpected end-of-input' 170 | else 171 | "syntax error, unexpected end-of-input, expecting '('" 172 | end 173 | 174 | it_behaves_like 'file parser', 'syntax error', 175 | %{ 176 | foo, bar. 177 | }, 178 | type: 'SyntaxError', 179 | message: syntax_error, 180 | line: 3 181 | 182 | it_behaves_like 'file parser', 'circular reference', 183 | %{ 184 | class Foo 185 | # @param (see #b) 186 | def a; end 187 | # @param (see #a) 188 | def b; end 189 | end 190 | }, 191 | type: 'CircularReference', 192 | message: "Detected circular reference tag in `Foo#b', ignoring all reference tags for this object (@param).", 193 | object: 'Foo#b', 194 | context: '@param', 195 | line: 6 196 | 197 | it_behaves_like 'file parser', 'undocumentable', 198 | %{ 199 | attr_reader *OPTIONS 200 | }, 201 | type: 'Undocumentable', 202 | message: 'Undocumentable OPTIONS: `attr_reader *OPTIONS`', 203 | quote: 'attr_reader *OPTIONS', 204 | object: 'OPTIONS', 205 | line: 2 206 | 207 | it_behaves_like 'file parser', 'not recognized', 208 | %{ 209 | Bar::BOOKS = 5 210 | }, 211 | type: 'UnknownNamespace', 212 | message: 'namespace Bar is not recognized', 213 | namespace: 'Bar' 214 | 215 | it_behaves_like 'file parser', 'macro attaching error', 216 | %{ 217 | # @!macro [attach] attached4 218 | # $1 $2 $3 219 | class A 220 | end 221 | }, 222 | type: 'MacroAttachError', 223 | message: 'Attaching macros to non-methods is unsupported, ignoring: A', 224 | object: 'A', 225 | line: 2 226 | 227 | it_behaves_like 'file parser', 'macro name error', 228 | %{ 229 | # @!macro wtf 230 | def foo 231 | end 232 | }, 233 | type: 'MacroNameError', 234 | message: 'Invalid/missing macro name for #foo', 235 | object: '#foo', 236 | line: 3 237 | 238 | it_behaves_like 'file parser', 'redundant braces', 239 | %{ 240 | # @see {invalid} 241 | def foo 242 | end 243 | }, 244 | type: 'RedundantBraces', 245 | message: '@see tag should not be wrapped in {} (causes rendering issues)', 246 | line: 2 247 | 248 | # TODO: DRY! 249 | context 'invalid link' do 250 | include YARD::Templates::Helpers::BaseHelper 251 | include YARD::Templates::Helpers::HtmlHelper 252 | 253 | before { 254 | parse_file(%{ 255 | # Comments here 256 | # And a reference to {InvalidObject} 257 | class MyObject; end 258 | }) 259 | allow(self).to receive(:object).and_return(YARD::Registry.at('MyObject')) 260 | resolve_links(YARD::Registry.at('MyObject').docstring) 261 | } 262 | 263 | its(:'messages.last.to_h') { is_expected 264 | .to eq(type: 'InvalidLink', message: 'Cannot resolve link to InvalidObject from text: ...{InvalidObject}', object: 'InvalidObject', quote: '...{InvalidObject}', file: 'test.rb', line: 3) 265 | } 266 | end 267 | end 268 | -------------------------------------------------------------------------------- /lib/yard-junk/logger/message.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'spellcheck' 4 | 5 | module YardJunk 6 | class Logger 7 | class Message 8 | attr_reader :message, :severity, :file, :line, :extra 9 | 10 | def initialize(message:, severity: :warn, code_object: nil, file: nil, line: nil, **extra) 11 | @message = message.gsub(/\s{2,}/, ' ') 12 | @file = file 13 | @line = line&.to_i 14 | @code_object = code_object 15 | @severity = severity 16 | @extra = extra 17 | end 18 | 19 | %i[error warn].each do |sev| 20 | define_method("#{sev}?") { severity == sev } 21 | end 22 | 23 | def to_h 24 | { 25 | type: type, 26 | message: message, 27 | file: file, 28 | line: line&.to_i || 1 29 | }.merge(extra) 30 | end 31 | 32 | def ==(other) 33 | other.is_a?(self.class) && to_h == other.to_h 34 | end 35 | 36 | DEFAULT_FORMAT = '%{file}:%{line}: [%{type}] %{message}' 37 | 38 | def to_s(format = DEFAULT_FORMAT) 39 | format % to_h 40 | end 41 | 42 | def type 43 | self.class.type 44 | end 45 | 46 | include Spellcheck 47 | 48 | class << self 49 | def registry 50 | @registry ||= [] 51 | end 52 | 53 | def pattern(regexp) 54 | @pattern = regexp 55 | Message.registry << self 56 | end 57 | 58 | def search_up(pattern) 59 | @search_up = pattern 60 | end 61 | 62 | def try_parse(line, **context) 63 | @pattern or fail StandardError, "Pattern is not defined for #{self}" 64 | match = @pattern.match(line) or return nil 65 | data = context.compact 66 | .merge(match.names.map(&:to_sym).zip(match.captures).to_h.compact) 67 | data = guard_line(data) 68 | new(**data) 69 | end 70 | 71 | def type 72 | !name || name.end_with?('::Message') ? 'UnknownError' : name.sub(/^.+::/, '') 73 | end 74 | 75 | def valid_type?(type) 76 | type == 'UnknownError' || registry.any? { |m| m.type == type } 77 | end 78 | 79 | private 80 | 81 | def guard_line(data) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity 82 | # FIXME: Ugly, huh? 83 | data[:file] && data[:line] && @search_up or return data 84 | data = data.merge(line: data[:line].to_i) 85 | data = data.merge(code_object: find_object(data[:file], data[:line])) 86 | lines = File.readlines(data[:file]) rescue (return data) # rubocop:disable Style/RescueModifier 87 | pattern = Regexp.new(@search_up % data.transform_values { |v| Regexp.escape(v.to_s) }) 88 | _, num = lines.map 89 | .with_index { |ln, i| [ln, i + 1] } 90 | .first(data[:line]).reverse 91 | .detect { |ln,| pattern.match(ln) } 92 | num or return data 93 | 94 | data.merge(line: num) 95 | end 96 | 97 | def find_object(file, line) 98 | YARD::Registry.detect { |o| o.file == file && o.line == line } 99 | end 100 | end 101 | end 102 | 103 | # rubocop:disable Layout/LineLength 104 | class UnknownTag < Message 105 | pattern %r{^(?Unknown tag (?@\S+))( in file `(?[^`]+)` near line (?\d+))?$} 106 | search_up '%{tag}(\W|$)' 107 | 108 | def message 109 | corrections.empty? ? super : "#{super}. Did you mean #{corrections.map { |c| "@#{c}" }.join(', ')}?" 110 | end 111 | 112 | private 113 | 114 | def corrections 115 | spell_check(extra[:tag], YARD::Tags::Library.labels.keys.map(&:to_s)) 116 | end 117 | end 118 | 119 | class InvalidTagFormat < Message 120 | pattern %r{^(?Invalid tag format for (?@\S+))( in file `(?[^`]+)` near line (?\d+))?$} 121 | search_up '%{tag}(\W|$)' 122 | end 123 | 124 | class UnknownDirective < Message 125 | pattern %r{^(?Unknown directive (?@!\S+))( in file `(?[^`]+)` near line (?\d+))?$} 126 | search_up '%{directive}(\W|$)' 127 | 128 | # TODO: did_you_mean? 129 | end 130 | 131 | class InvalidDirectiveFormat < Message 132 | pattern %r{^(?Invalid directive format for (?@!\S+))( in file `(?[^`]+)` near line (?\d+))?$} 133 | search_up '%{directive}(\W|$)' 134 | end 135 | 136 | class UnknownParam < Message 137 | pattern %r{^(?@param tag has unknown parameter name: (?\S+))\s+ in file `(?[^']+)' near line (?\d+)$} 138 | search_up '@param(\s+\[.+?\])?\s+?%{param_name}(\W|$)' 139 | 140 | def message 141 | corrections.empty? ? super : "#{super}. Did you mean #{corrections.map { |c| "`#{c}`" }.join(', ')}?" 142 | end 143 | 144 | private 145 | 146 | def corrections 147 | spell_check(extra[:param_name], known_params) 148 | end 149 | 150 | def known_params 151 | @code_object.is_a?(YARD::CodeObjects::MethodObject) or return [] 152 | @code_object.parameters.map(&:first).map { |p| p.tr('*&:', '') } 153 | end 154 | end 155 | 156 | class MissingParamName < Message 157 | pattern %r{^(?@param tag has unknown parameter name):\s+in file `(?[^']+)' near line (?\d+)$} 158 | search_up '@param(\s+\[.+?\])?\s*$' 159 | 160 | def message 161 | '@param tag has empty parameter name' 162 | end 163 | end 164 | 165 | class DuplicateParam < Message 166 | pattern %r{^(?@param tag has duplicate parameter name: (?\S+))\s+ in file `(?[^']+)' near line (?\d+)$} 167 | search_up '@param\s+(\[.+?\]\s+)?%{param_name}(\W|$)' 168 | end 169 | 170 | class RedundantBraces < Message 171 | pattern %r{^(?@see tag \(\#\d+\) should not be wrapped in \{\} \(causes rendering issues\)):\s+in file `(?[^']+)' near line (?\d+)$} 172 | search_up '@see.*{.*}' 173 | 174 | def message 175 | super.sub(/\s+\(\#\d+\)\s+/, ' ') 176 | end 177 | end 178 | 179 | class SyntaxError < Message 180 | pattern %r{^Syntax error in `(?[^`]+)`:\((?\d+),(?:\d+)\): (?.+)$} 181 | 182 | # Honestly, IDK why YARD considers it "warning"... So, rewriting 183 | def severity 184 | :error 185 | end 186 | end 187 | 188 | class CircularReference < Message 189 | pattern %r{^(?.+?):(?\d+): (?Detected circular reference tag in `(?[^']+)', ignoring all reference tags for this object \((?[^)]+)\)\.)$} 190 | end 191 | 192 | class Undocumentable < Message 193 | pattern %r{^in (?:\S+): (?Undocumentable (?.+?))\n\s*in file '(?[^']+)':(?\d+):\s+(?:\d+):\s*(?.+?)\s*$} 194 | 195 | def message 196 | super + ": `#{quote}`" 197 | end 198 | 199 | def quote 200 | extra[:quote] 201 | end 202 | end 203 | 204 | class UnknownNamespace < Message 205 | pattern %r{^(?The proxy (?\S+?) has not yet been recognized).\nIf this class/method is part of your source tree, this will affect your documentation results.\nYou can correct this issue by loading the source file for this object before `(?[^']+)'\n$} 206 | 207 | def namespace 208 | extra[:namespace] 209 | end 210 | 211 | def message 212 | "namespace #{namespace} is not recognized" 213 | end 214 | end 215 | 216 | class MacroAttachError < Message 217 | pattern %r{^(?Attaching macros to non-methods is unsupported, ignoring: (?\S+)) \((?.+?):(?\d+)\)$} 218 | search_up '@!macro \[attach\]' 219 | end 220 | 221 | class MacroNameError < Message 222 | pattern %r{^(?Invalid/missing macro name for (?\S+)) \((?.+?):(?\d+)\)$} 223 | end 224 | 225 | class InvalidLink < Message 226 | pattern %r{^In file `(?[^']+)':(?\d+): (?Cannot resolve link to (?\S+) from text:\s+(?.+))$} 227 | search_up '%{quote}' 228 | end 229 | 230 | class InvalidFileLink < Message 231 | pattern %r{^In file `(?[^']+)':(?\d+): (?File '(?\S+)' does not exist:\s+(?.+))$} 232 | search_up '%{quote}' 233 | end 234 | # rubocop:enable Layout/LineLength 235 | end 236 | end 237 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Yard-Junk: get rid of junk in your YARD docs! 2 | 3 | [![Gem Version](https://badge.fury.io/rb/yard-junk.svg)](http://badge.fury.io/rb/yard-junk) 4 | ![Build Status](https://github.com/zverok/yard-junk/workflows/CI/badge.svg?branch=master) 5 | 6 | Yard-Junk is [yard](https://github.com/lsegal/yard) plugin/patch, that provides: 7 | 8 | * structured documentation error logging; 9 | * documentation errors validator, ready to be integrated into CI pipeline. 10 | 11 | ## Showcase 12 | 13 | Let's generate the docs for the [rom](https://github.com/rom-rb/rom) library. 14 | 15 |
    Output of `yard doc` without JunkYard 16 | 17 | ``` 18 | [warn]: in YARD::Handlers::Ruby::MixinHandler: Undocumentable mixin: YARD::Parser::UndocumentableError for class ROM::Types 19 | in file 'core/lib/rom/types.rb':9: 20 | 21 | 9: include Dry::Types.module 22 | 23 | [warn]: Invalid tag format for @example in file `core/lib/rom/global.rb` near line 41 24 | [warn]: in YARD::Handlers::Ruby::MixinHandler: Undocumentable mixin: YARD::Parser::UndocumentableError for class ROM::Schema 25 | in file 'core/lib/rom/schema.rb':66: 26 | 27 | 66: include Dry::Equalizer(:name, :attributes, :associations) 28 | 29 | [warn]: @param tag has unknown parameter name: 30 | in file `core/lib/rom/schema.rb' near line 149 31 | [warn]: @param tag has unknown parameter name: 32 | in file `core/lib/rom/schema.rb' near line 305 33 | [warn]: @param tag has unknown parameter name: 34 | in file `core/lib/rom/schema.rb' near line 316 35 | [warn]: in YARD::Handlers::Ruby::MixinHandler: Undocumentable mixin: YARD::Parser::UndocumentableError for class ROM::Command 36 | in file 'core/lib/rom/command.rb':30: 37 | 38 | 30: include Dry::Equalizer(:relation, :options) 39 | 40 | [warn]: @param tag has unknown parameter name: Transaction 41 | in file `core/lib/rom/gateway.rb' near line 176 42 | [warn]: in YARD::Handlers::Ruby::MixinHandler: Undocumentable mixin: YARD::Parser::UndocumentableError for class ROM::Pipeline::Composite 43 | in file 'core/lib/rom/pipeline.rb':82: 44 | 45 | 82: include Dry::Equalizer(:left, :right) 46 | 47 | [warn]: in YARD::Handlers::Ruby::MixinHandler: Undocumentable mixin: YARD::Parser::UndocumentableError for class ROM::Registry 48 | in file 'core/lib/rom/registry.rb':13: 49 | 50 | 13: include Dry::Equalizer(:elements) 51 | 52 | [warn]: in YARD::Handlers::Ruby::MixinHandler: Undocumentable mixin: YARD::Parser::UndocumentableError for class ROM::Relation 53 | in file 'core/lib/rom/relation.rb':129: 54 | 55 | 129: include Dry::Equalizer(:name, :dataset) 56 | 57 | [warn]: @param tag has unknown parameter name: options 58 | in file `core/lib/rom/relation.rb' near line 302 59 | [warn]: @param tag has unknown parameter name: new_options 60 | in file `core/lib/rom/relation.rb' near line 411 61 | [warn]: @param tag has unknown parameter name: klass 62 | in file `core/lib/rom/relation.rb' near line 529 63 | [warn]: in YARD::Handlers::Ruby::MixinHandler: Undocumentable mixin: YARD::Parser::UndocumentableError for class ROM::Attribute 64 | in file 'core/lib/rom/attribute.rb':17: 65 | 66 | 17: include Dry::Equalizer(:type, :options) 67 | 68 | [warn]: @param tag has unknown parameter name: 69 | in file `core/lib/rom/attribute.rb' near line 344 70 | [warn]: in YARD::Handlers::Ruby::MixinHandler: Undocumentable mixin: YARD::Parser::UndocumentableError for class ROM::Container 71 | in file 'core/lib/rom/container.rb':101: 72 | 73 | 101: include Dry::Equalizer(:gateways, :relations, :mappers, :commands) 74 | 75 | [warn]: @param tag has unknown parameter name: base 76 | in file `core/lib/rom/plugin_base.rb' near line 41 77 | [warn]: in YARD::Handlers::Ruby::MixinHandler: Undocumentable mixin: YARD::Parser::UndocumentableError for class ROM::Commands::Lazy 78 | in file 'core/lib/rom/commands/lazy.rb':10: 79 | 80 | 10: include Dry::Equalizer(:command, :evaluator) 81 | 82 | [warn]: @param tag has unknown parameter name: The 83 | in file `core/lib/rom/configuration.rb' near line 50 84 | [warn]: @param tag has unknown parameter name: Plugin 85 | in file `core/lib/rom/configuration.rb' near line 50 86 | [warn]: in YARD::Handlers::Ruby::MixinHandler: Undocumentable mixin: YARD::Parser::UndocumentableError for class ROM::Relation::Name 87 | in file 'core/lib/rom/relation/name.rb':17: 88 | 89 | 17: include Dry::Equalizer(:relation, :dataset) 90 | 91 | [warn]: in YARD::Handlers::Ruby::MixinHandler: Undocumentable mixin: YARD::Parser::UndocumentableError for class ROM::Commands::Graph 92 | in file 'core/lib/rom/commands/graph.rb':12: 93 | 94 | 12: include Dry::Equalizer(:root, :nodes) 95 | 96 | [warn]: @param tag has unknown parameter name: names 97 | in file `core/lib/rom/memory/dataset.rb' near line 61 98 | [warn]: in YARD::Handlers::Ruby::MixinHandler: Undocumentable mixin: YARD::Parser::UndocumentableError for class ROM::Relation::Graph 99 | in file 'core/lib/rom/relation/graph.rb':29: 100 | 101 | 29: include Dry::Equalizer(:root, :nodes) 102 | 103 | [warn]: in YARD::Handlers::Ruby::MixinHandler: Undocumentable mixin: YARD::Parser::UndocumentableError for class ROM::PluginRegistryBase 104 | in file 'core/lib/rom/plugin_registry.rb':88: 105 | 106 | 88: include Dry::Equalizer(:elements, :plugin_type) 107 | 108 | [warn]: Unknown tag @raises in file `core/lib/rom/plugin_registry.rb` near line 143 109 | [warn]: Unknown tag @raises in file `core/lib/rom/plugin_registry.rb` near line 190 110 | [warn]: in YARD::Handlers::Ruby::MixinHandler: Undocumentable mixin: YARD::Parser::UndocumentableError for class ROM::Relation::Loaded 111 | in file 'core/lib/rom/relation/loaded.rb':12: 112 | 113 | 12: include Dry::Equalizer(:source, :collection) 114 | 115 | [warn]: Unknown tag @raises in file `core/lib/rom/relation/loaded.rb` near line 94 116 | [warn]: in YARD::Handlers::Ruby::MixinHandler: Undocumentable mixin: YARD::Parser::UndocumentableError for class ROM::Schema::Inferrer 117 | in file 'core/lib/rom/schema/inferrer.rb':27: 118 | 119 | 27: include Dry::Equalizer(:options) 120 | 121 | [warn]: @param tag has unknown parameter name: name 122 | in file `core/lib/rom/command_registry.rb' near line 57 123 | [warn]: in YARD::Handlers::Ruby::MixinHandler: Undocumentable mixin: YARD::Parser::UndocumentableError for class ROM::Relation::Curried 124 | in file 'core/lib/rom/relation/curried.rb':22: 125 | 126 | 22: include Dry::Equalizer(:relation, :options) 127 | 128 | [warn]: Unknown tag @raises in file `core/lib/rom/relation/curried.rb` near line 72 129 | [warn]: @param tag has unknown parameter name: adapter 130 | in file `core/lib/rom/global/plugin_dsl.rb' near line 42 131 | [warn]: @param tag has unknown parameter name: 132 | in file `core/lib/rom/relation/combined.rb' near line 33 133 | [warn]: in YARD::Handlers::Ruby::MixinHandler: Undocumentable mixin: YARD::Parser::UndocumentableError for class ROM::Associations::Abstract 134 | in file 'core/lib/rom/associations/abstract.rb':17: 135 | 136 | 17: include Dry::Equalizer(:definition, :source, :target) 137 | 138 | [warn]: in YARD::Handlers::Ruby::MixinHandler: Undocumentable mixin: YARD::Parser::UndocumentableError for class ROM::Notifications::Event 139 | in file 'core/lib/rom/support/notifications.rb':75: 140 | 141 | 75: include Dry::Equalizer(:id, :payload) 142 | 143 | [warn]: @param tag has unknown parameter name: command 144 | in file `core/lib/rom/commands/class_interface.rb' near line 86 145 | [warn]: @param tag has unknown parameter name: parent 146 | in file `core/lib/rom/commands/class_interface.rb' near line 86 147 | [warn]: @param tag has unknown parameter name: options 148 | in file `core/lib/rom/commands/class_interface.rb' near line 112 149 | [warn]: @param tag has unknown parameter name: 150 | in file `core/lib/rom/commands/class_interface.rb' near line 123 151 | [warn]: in YARD::Handlers::Ruby::MixinHandler: Undocumentable mixin: YARD::Parser::UndocumentableError for class ROM::Commands::Graph::InputEvaluator 152 | in file 'core/lib/rom/commands/graph/input_evaluator.rb':5: 153 | 154 | 5: include Dry::Equalizer(:tuple_path, :excluded_keys) 155 | 156 | [warn]: in YARD::Handlers::Ruby::MixinHandler: Undocumentable mixin: YARD::Parser::UndocumentableError for class ROM::Associations::Definitions::Abstract 157 | in file 'core/lib/rom/associations/definitions/abstract.rb':16: 158 | 159 | 16: include Dry::Equalizer(:source, :target, :result) 160 | 161 | [warn]: @param tag has unknown parameter name: options 162 | in file `core/lib/rom/associations/definitions/abstract.rb' near line 74 163 | [warn]: @param tag has unknown parameter name: options 164 | in file `changeset/lib/rom/changeset.rb' near line 84 165 | [warn]: in YARD::Handlers::Ruby::ClassHandler: Undocumentable superclass (class was added without superclass) 166 | in file 'changeset/lib/rom/changeset/pipe.rb':28: 167 | 168 | 28: class Pipe < Transproc::Transformer[PipeRegistry] 169 | 170 | [warn]: @param tag has unknown parameter name: assoc 171 | in file `changeset/lib/rom/changeset/stateful.rb' near line 222 172 | [warn]: in YARD::Handlers::Ruby::MixinHandler: Undocumentable mixin: YARD::Parser::UndocumentableError for class ROM::Header 173 | in file 'mapper/lib/rom/header.rb':12: 174 | 175 | 12: include Dry::Equalizer(:attributes, :model) 176 | 177 | [warn]: @param tag has unknown parameter name: model 178 | in file `mapper/lib/rom/header.rb' near line 52 179 | [warn]: in YARD::Handlers::Ruby::MixinHandler: Undocumentable mixin: YARD::Parser::UndocumentableError for class ROM::Mapper 180 | in file 'mapper/lib/rom/mapper.rb':11: 181 | 182 | 11: include Dry::Equalizer(:transformers, :header) 183 | 184 | [warn]: in YARD::Handlers::Ruby::MixinHandler: Undocumentable mixin: YARD::Parser::UndocumentableError for class ROM::Header::Attribute 185 | in file 'mapper/lib/rom/header/attribute.rb':14: 186 | 187 | 14: include Dry::Equalizer(:name, :key, :type) 188 | 189 | [warn]: in YARD::Handlers::Ruby::MixinHandler: Undocumentable mixin: YARD::Parser::UndocumentableError for class ROM::Header::Embedded 190 | in file 'mapper/lib/rom/header/attribute.rb':110: 191 | 192 | 110: include Dry::Equalizer(:name, :key, :type, :header) 193 | 194 | [warn]: @param tag has unknown parameter name: 195 | in file `mapper/lib/rom/processor/transproc.rb' near line 215 196 | [warn]: in YARD::Handlers::Ruby::MixinHandler: Undocumentable mixin: YARD::Parser::UndocumentableError for class ROM::Session 197 | in file 'repository/lib/rom/repository/session.rb':8: 198 | 199 | 8: include Dry::Equalizer(:queue, :status) 200 | 201 | [warn]: The proxy Coercible has not yet been recognized. 202 | If this class/method is part of your source tree, this will affect your documentation results. 203 | You can correct this issue by loading the source file for this object before `core/lib/rom/types.rb' 204 | 205 | [warn]: The proxy Coercible has not yet been recognized. 206 | If this class/method is part of your source tree, this will affect your documentation results. 207 | You can correct this issue by loading the source file for this object before `core/lib/rom/types.rb' 208 | 209 | [warn]: The proxy Coercible has not yet been recognized. 210 | If this class/method is part of your source tree, this will affect your documentation results. 211 | You can correct this issue by loading the source file for this object before `core/lib/rom/types.rb' 212 | ``` 213 |
    214 | 215 | Things to notice: 216 | 217 | * irregular and frequently approximate addresses (`in file 'core/lib/rom/types.rb':9`, 218 | `in file 'core/lib/rom/global.rb' near line 41`, sometimes in separate line, sometimes inline), 219 | hard to jump-to with any tool; 220 | * a lot of ununderstood metaprogramming (grep for "Undocumentable mixin") -- nothing to fix here, 221 | but YARD still notifies you; 222 | * verbose and not very informative errors (look at that "Undocumentable mixin" -- and then grep 223 | for "The proxy Coercible has not yet been recognized." and compare). 224 | 225 |
    Output of `yard doc` with Yard-Junk 226 | 227 | ``` 228 | core/lib/rom/global.rb:40: [InvalidTagFormat] Invalid tag format for @example 229 | core/lib/rom/schema.rb:144: [MissingParamName] @param tag has empty parameter name 230 | core/lib/rom/schema.rb:300: [MissingParamName] @param tag has empty parameter name 231 | core/lib/rom/schema.rb:311: [MissingParamName] @param tag has empty parameter name 232 | core/lib/rom/gateway.rb:171: [UnknownParam] @param tag has unknown parameter name: Transaction 233 | core/lib/rom/relation.rb:297: [UnknownParam] @param tag has unknown parameter name: options 234 | core/lib/rom/relation.rb:406: [UnknownParam] @param tag has unknown parameter name: new_options 235 | core/lib/rom/relation.rb:524: [UnknownParam] @param tag has unknown parameter name: klass 236 | core/lib/rom/attribute.rb:339: [MissingParamName] @param tag has empty parameter name 237 | core/lib/rom/plugin_base.rb:38: [UnknownParam] @param tag has unknown parameter name: base. Did you mean `_base`? 238 | core/lib/rom/configuration.rb:46: [UnknownParam] @param tag has unknown parameter name: The 239 | core/lib/rom/configuration.rb:47: [UnknownParam] @param tag has unknown parameter name: Plugin. Did you mean `plugin`? 240 | core/lib/rom/memory/dataset.rb:54: [UnknownParam] @param tag has unknown parameter name: names 241 | core/lib/rom/plugin_registry.rb:140: [UnknownTag] Unknown tag @raises. Did you mean @raise? 242 | core/lib/rom/plugin_registry.rb:187: [UnknownTag] Unknown tag @raises. Did you mean @raise? 243 | core/lib/rom/relation/loaded.rb:91: [UnknownTag] Unknown tag @raises. Did you mean @raise? 244 | core/lib/rom/command_registry.rb:52: [UnknownParam] @param tag has unknown parameter name: name 245 | core/lib/rom/relation/curried.rb:69: [UnknownTag] Unknown tag @raises. Did you mean @raise? 246 | core/lib/rom/global/plugin_dsl.rb:41: [UnknownParam] @param tag has unknown parameter name: adapter 247 | core/lib/rom/relation/combined.rb:28: [MissingParamName] @param tag has empty parameter name 248 | core/lib/rom/commands/class_interface.rb:78: [UnknownParam] @param tag has unknown parameter name: command 249 | core/lib/rom/commands/class_interface.rb:79: [UnknownParam] @param tag has unknown parameter name: parent 250 | core/lib/rom/commands/class_interface.rb:108: [UnknownParam] @param tag has unknown parameter name: options. Did you mean `_options`? 251 | core/lib/rom/commands/class_interface.rb:118: [MissingParamName] @param tag has empty parameter name 252 | core/lib/rom/associations/definitions/abstract.rb:66: [UnknownParam] @param tag has unknown parameter name: options 253 | changeset/lib/rom/changeset.rb:79: [UnknownParam] @param tag has unknown parameter name: options. Did you mean `new_options`? 254 | changeset/lib/rom/changeset/stateful.rb:219: [UnknownParam] @param tag has unknown parameter name: assoc 255 | mapper/lib/rom/header.rb:47: [UnknownParam] @param tag has unknown parameter name: model 256 | mapper/lib/rom/processor/transproc.rb:212: [MissingParamName] @param tag has empty parameter name 257 | core/lib/rom/types.rb:1: [UnknownNamespace] namespace Coercible is not recognized 258 | core/lib/rom/types.rb:1: [UnknownNamespace] namespace Coercible is not recognized 259 | core/lib/rom/types.rb:1: [UnknownNamespace] namespace Coercible is not recognized 260 | ``` 261 |
    262 | 263 | Things to notice: 264 | 265 | * Regular output style with clearly recognizable addresses (and fixed to point at actual line with 266 | the problematic tag, not the method which tag is related for); 267 | * Error classes, allowing grouping, grepping, and configuring (notice no "Undocumentable xxx" errors: 268 | I've just configured `yard-junk` to drop them for this repo); 269 | * Usage of Ruby's bundled `did_you_mean` gem to show reasonable suggestions: 270 | ``` 271 | Unknown tag @raises. Did you mean @raise? 272 | @param tag has unknown parameter name: options. Did you mean `new_options`? 273 | ``` 274 | * Rephrased and cleaned up messages. 275 | 276 |
    `yard-junk` tool output 277 | 278 | ``` 279 | Problems 280 | -------- 281 | mistyped tags or other typos in documentation 282 | 283 | changeset/lib/rom/changeset.rb:79: [UnknownParam] @param tag has unknown parameter name: options. Did you mean `new_options`? 284 | changeset/lib/rom/changeset/stateful.rb:219: [UnknownParam] @param tag has unknown parameter name: assoc 285 | core/lib/rom/associations/definitions/abstract.rb:66: [UnknownParam] @param tag has unknown parameter name: options 286 | core/lib/rom/attribute.rb:339: [MissingParamName] @param tag has empty parameter name 287 | core/lib/rom/command_registry.rb:52: [UnknownParam] @param tag has unknown parameter name: name 288 | core/lib/rom/commands/class_interface.rb:78: [UnknownParam] @param tag has unknown parameter name: command 289 | core/lib/rom/commands/class_interface.rb:79: [UnknownParam] @param tag has unknown parameter name: parent 290 | core/lib/rom/commands/class_interface.rb:108: [UnknownParam] @param tag has unknown parameter name: options. Did you mean `_options`? 291 | core/lib/rom/commands/class_interface.rb:118: [MissingParamName] @param tag has empty parameter name 292 | core/lib/rom/configuration.rb:46: [UnknownParam] @param tag has unknown parameter name: The 293 | core/lib/rom/configuration.rb:47: [UnknownParam] @param tag has unknown parameter name: Plugin. Did you mean `plugin`? 294 | core/lib/rom/gateway.rb:171: [UnknownParam] @param tag has unknown parameter name: Transaction 295 | core/lib/rom/global.rb:40: [InvalidTagFormat] Invalid tag format for @example 296 | core/lib/rom/global/plugin_dsl.rb:41: [UnknownParam] @param tag has unknown parameter name: adapter 297 | core/lib/rom/memory/dataset.rb:54: [UnknownParam] @param tag has unknown parameter name: names 298 | core/lib/rom/plugin_base.rb:38: [UnknownParam] @param tag has unknown parameter name: base. Did you mean `_base`? 299 | core/lib/rom/plugin_registry.rb:140: [UnknownTag] Unknown tag @raises. Did you mean @raise? 300 | core/lib/rom/plugin_registry.rb:187: [UnknownTag] Unknown tag @raises. Did you mean @raise? 301 | core/lib/rom/relation.rb:297: [UnknownParam] @param tag has unknown parameter name: options 302 | core/lib/rom/relation.rb:406: [UnknownParam] @param tag has unknown parameter name: new_options 303 | core/lib/rom/relation.rb:524: [UnknownParam] @param tag has unknown parameter name: klass 304 | core/lib/rom/relation/combined.rb:28: [MissingParamName] @param tag has empty parameter name 305 | core/lib/rom/relation/curried.rb:69: [UnknownTag] Unknown tag @raises. Did you mean @raise? 306 | core/lib/rom/relation/loaded.rb:91: [UnknownTag] Unknown tag @raises. Did you mean @raise? 307 | core/lib/rom/schema.rb:144: [MissingParamName] @param tag has empty parameter name 308 | core/lib/rom/schema.rb:300: [MissingParamName] @param tag has empty parameter name 309 | core/lib/rom/schema.rb:311: [MissingParamName] @param tag has empty parameter name 310 | core/lib/rom/types.rb:1: [UnknownNamespace] namespace Coercible is not recognized 311 | core/lib/rom/types.rb:1: [UnknownNamespace] namespace Coercible is not recognized 312 | core/lib/rom/types.rb:1: [UnknownNamespace] namespace Coercible is not recognized 313 | mapper/lib/rom/header.rb:47: [UnknownParam] @param tag has unknown parameter name: model 314 | mapper/lib/rom/processor/transproc.rb:212: [MissingParamName] @param tag has empty parameter name 315 | 316 | 0 failures, 32 problems (2 seconds to run) 317 | ``` 318 |
    319 | 320 | It is basically the same as above, and: 321 | 322 | * sorted by files/lines instead of "reported when found" approach; 323 | * with short stats at the end; 324 | * returning proper exit code (0 if no problems/parsing errors, non-0 otherwise), which allows `yard-junk` 325 | to be integrated into CI pipeline, and control that new PRs will not screw docs (forgetting to 326 | rename parameters in docs when they are renamed in code, for example). 327 | 328 | As a nice addition, `yard-junk` command uses its own links to code objects resolver, which is 10x 329 | faster (and, eventually, more correct) than YARD's own approach to resolve links when rendering docs. 330 | 331 | ## Usage 332 | 333 | It is a `yard-junk` gem, install it as usual, or add to your `Gemfile`. 334 | 335 | ### Better logs 336 | 337 | Add this to your `.yardopts` file: 338 | ``` 339 | --plugin junk 340 | ``` 341 | 342 | After that, just run `yard` or `yard doc` as usual, and enjoy better logs! You can also setup JunkYard 343 | logs by passing options (in the same `.yardopts`): 344 | ``` 345 | --junk-log-format FORMAT_STR 346 | --junk-log-ignore ERROR_TYPE1,ERROR_TYPE2,... 347 | ``` 348 | 349 | Format is usual Ruby's [#format](https://ruby-doc.org/core-2.2.3/Kernel.html#method-i-format) method 350 | with named fields: 351 | * `message` -- error message; 352 | * `file` -- error file; 353 | * `line` -- error line; 354 | * `type` -- error type. 355 | 356 | Default format is `%{file}:%{line}: [%{type}] %{message}`, as shown above. 357 | 358 | `--junk-log-ignore` option allows to ingore error classes by their type names (shown in logs in `[]`). 359 | By default, `Undocumentable` error is ignored: it is produced as metaprogramming pieces of code like 360 | ```ruby 361 | attr_reader *OPTIONS 362 | ``` 363 | or 364 | ```ruby 365 | include Rails.routes 366 | ``` 367 | ...and typically have no way to fix, while polluting logs with a lot of, well, junk. 368 | 369 | ### Standalone docs check 370 | 371 | Just run `yard-junk` command after gem is installed. Optionally, you can setup "formatters" to use: 372 | 373 | * text formatter: suitable for console, sparse colorized output; 374 | * HTML formatter: suitable as CI artifact, readable in browser. 375 | 376 | Examples: 377 | 378 | * `yard-junk` (default run, outputs text to STDOUT); 379 | * `yard-junk --text` (same as above); 380 | * `yard-junk --text logs/yard.log` (set the output path); 381 | * `yard-junk --text --html build-artifacts/junk-yard.html` (several formatters at once: text to console, 382 | HTML to file). 383 | 384 | You can also specify pathes to report (useful when working on large codebases, when you want to check 385 | only your recent piece of work): 386 | 387 | ``` 388 | yard-junk --path some/path/ 389 | yard-junk --path other/path/*sample*.rb 390 | yard-junk --path specific/path.rb 391 | yard-junk --path several,different/*.rb,patterns.rb 392 | ``` 393 | 394 | Note that `yard-junk` would parse the pathes that set in `.yardopts` as usually, and then 395 | **filter report** by pattern specified. 396 | 397 | ### Rake task (integrating in CI) 398 | 399 | Add this to your `Rakefile`: 400 | 401 | ```ruby 402 | require 'yard-junk/rake' 403 | YardJunk::Rake.define_task 404 | ``` 405 | 406 | and then run it (or add to your `.travis.yml`) as 407 | ``` 408 | rake yard:junk 409 | ``` 410 | 411 | The Rake task also takes formatter arguments, at task-definition time: 412 | 413 | ```ruby 414 | YardJunk::Rake.define_task(:text) # default 415 | YardJunk::Rake.define_task(text: 'logs/yard.log') # text to file 416 | YardJunk::Rake.define_task(:text, html: 'build-artifacts/junk-yard.html') # text to STDOUT, html to file 417 | ``` 418 | 419 | ## Reasons 420 | 421 | Small problems in docs lead to a decrease in readability and usability. But it is hard to check for 422 | all those problems manually due to YARD's cumbersome output, and lack of CI-ready doc checking tools. 423 | 424 | The idea of a regularly structured logger was initially [proposed](https://github.com/lsegal/yard/issues/1007) 425 | as an enhancement for YARD itself, and even some steps were made by YARD's author in that direction, 426 | but the idea was abandoned since. 427 | 428 | Therefore, this independent tool was made. 429 | 430 | ## Caveats 431 | 432 | Sometimes YARD doesn't provide enough information to guess in which line of code the problem is; 433 | in those cases `yard-junk` just writes something like `file.rb:1` (to stay consistent and not break 434 | go-to-file tools). 435 | 436 | ## Roadmap 437 | 438 | * Docs for usage as a system-wide YARD plugin; 439 | * Docs for internals; 440 | * Documentation quality checks as a next level of YARD checker ([#14](https://github.com/zverok/yard-junk/issues/14)). 441 | 442 | ## Some examples of problems found in popular gems: 443 | 444 | **NB: All of those are excellent libs! The showcase is of "how hard it is to maintain docs quality", 445 | not of "how ignorant other programmers are".** 446 | 447 |
    httparty: 2 448 | 449 | ``` 450 | lib/httparty/exceptions.rb:2: [UnknownTag] Unknown tag @abstact. Did you mean @abstract? 451 | lib/httparty/exceptions.rb:20: [MissingParamName] @param tag has empty parameter name 452 | ``` 453 |
    454 | 455 |
    vcr: 7 456 | 457 | ``` 458 | lib/vcr/deprecations.rb:71: [UnknownParam] @param tag has unknown parameter name: name 459 | lib/vcr/deprecations.rb:73: [UnknownParam] @param tag has unknown parameter name: options 460 | lib/vcr/linked_cassette.rb:12: [UnknownParam] @param tag has unknown parameter name: context-owned 461 | lib/vcr/linked_cassette.rb:13: [UnknownParam] @param tag has unknown parameter name: context-unowned 462 | lib/vcr/linked_cassette.rb:55: [UnknownParam] @param tag has unknown parameter name: context-owned 463 | lib/vcr/linked_cassette.rb:56: [UnknownParam] @param tag has unknown parameter name: context-unowned 464 | lib/vcr/test_frameworks/cucumber.rb:27: [UnknownParam] @param tag has unknown parameter name: options 465 | ``` 466 |
    467 | 468 |
    eventmachine: 19 469 | 470 | ``` 471 | lib/em/channel.rb:39: [UnknownParam] @param tag has unknown parameter name: Subscriber 472 | lib/em/connection.rb:603: [InvalidLink] Cannot resolve link to Socket.unpack_sockaddr_in from text: {Socket.unpack_sockaddr_in} 473 | lib/em/connection.rb:726: [InvalidLink] Cannot resolve link to EventMachine.notify_readable from text: {EventMachine.notify_readable} 474 | lib/em/connection.rb:726: [InvalidLink] Cannot resolve link to EventMachine.notify_writable from text: {EventMachine.notify_writable} 475 | lib/em/connection.rb:739: [InvalidLink] Cannot resolve link to EventMachine.notify_readable from text: {EventMachine.notify_readable} 476 | lib/em/connection.rb:739: [InvalidLink] Cannot resolve link to EventMachine.notify_writable from text: {EventMachine.notify_writable} 477 | lib/em/protocols/httpclient2.rb:263: [InvalidLink] Cannot resolve link to |response| from text: {|response| puts response.content } 478 | lib/em/protocols/httpclient2.rb:276: [InvalidLink] Cannot resolve link to |response| from text: {|response| puts response.content } 479 | lib/em/protocols/line_protocol.rb:9: [InvalidLink] Cannot resolve link to line from text: {line} 480 | lib/em/protocols/object_protocol.rb:9: [InvalidLink] Cannot resolve link to 'you from text: {'you said' => obj} 481 | lib/em/protocols/smtpclient.rb:138: [InvalidLink] Cannot resolve link to "Subject" from text: {"Subject" => "Bogus", "CC" => "myboss@example.com"} 482 | lib/em/protocols/smtpclient.rb:138: [InvalidLink] Cannot resolve link to :type=>:plain, from text: {:type=>:plain, :username=>"mickey@disney.com", :password=>"mouse"} 483 | lib/em/protocols/smtpserver.rb:435: [InvalidLink] Cannot resolve link to :cert_chain_file from text: {:cert_chain_file => "/etc/ssl/cert.pem", :private_key_file => "/etc/ssl/private/cert.key"} 484 | lib/em/protocols/socks4.rb:13: [InvalidLink] Cannot resolve link to data from text: {data} 485 | lib/em/spawnable.rb:47: [InvalidLink] Cannot resolve link to xxx from text: {xxx} 486 | lib/eventmachine.rb:215: [InvalidLink] Cannot resolve link to EventMachine.stop from text: {EventMachine.stop} 487 | lib/eventmachine.rb:231: [InvalidLink] Cannot resolve link to EventMachine::Callback from text: {EventMachine::Callback} 488 | lib/eventmachine.rb:319: [UnknownParam] @param tag has unknown parameter name: delay 489 | lib/eventmachine.rb:345: [UnknownParam] @param tag has unknown parameter name: delay 490 | ``` 491 |
    492 | 493 |
    addressable: 8 494 | 495 | ``` 496 | lib/addressable/template.rb:197: [UnknownParam] @param tag has unknown parameter name: *indexes. Did you mean `indexes`? 497 | lib/addressable/uri.rb:296: [UnknownParam] @param tag has unknown parameter name: *uris. Did you mean `uris`? 498 | lib/addressable/uri.rb:1842: [UnknownParam] @param tag has unknown parameter name: The 499 | lib/addressable/uri.rb:1943: [UnknownParam] @param tag has unknown parameter name: The 500 | lib/addressable/uri.rb:1958: [UnknownParam] @param tag has unknown parameter name: The 501 | lib/addressable/uri.rb:2023: [UnknownParam] @param tag has unknown parameter name: The 502 | lib/addressable/uri.rb:2244: [UnknownParam] @param tag has unknown parameter name: *components. Did you mean `components`? 503 | lib/addressable/uri.rb:2275: [UnknownParam] @param tag has unknown parameter name: *components. Did you mean `components`? 504 | ``` 505 |
    506 | 507 |
    hashie: 16 (mostly not escaped code in docs) 508 | 509 | ``` 510 | lib/hashie/extensions/coercion.rb:68: [UnknownParam] @param tag has unknown parameter name: key 511 | lib/hashie/extensions/coercion.rb:69: [UnknownParam] @param tag has unknown parameter name: into 512 | lib/hashie/extensions/deep_find.rb:7: [InvalidLink] Cannot resolve link to user: from text: {user: {location: {address: '123 Street'} 513 | lib/hashie/extensions/deep_find.rb:7: [InvalidLink] Cannot resolve link to user: from text: {user: {location: {address: '123 Street'} 514 | lib/hashie/extensions/deep_find.rb:16: [InvalidLink] Cannot resolve link to location: from text: {location: {address: '123 Street'} 515 | lib/hashie/extensions/deep_find.rb:16: [InvalidLink] Cannot resolve link to location: from text: {location: {address: '123 Street'} 516 | lib/hashie/extensions/deep_find.rb:27: [InvalidLink] Cannot resolve link to users: from text: {users: [{location: {address: '123 Street'} 517 | lib/hashie/extensions/deep_find.rb:27: [InvalidLink] Cannot resolve link to users: from text: {users: [{location: {address: '123 Street'} 518 | lib/hashie/extensions/deep_find.rb:36: [InvalidLink] Cannot resolve link to location: from text: {location: {address: '123 Street'} 519 | lib/hashie/extensions/deep_find.rb:36: [InvalidLink] Cannot resolve link to location: from text: {location: {address: '123 Street'} 520 | lib/hashie/extensions/deep_find.rb:36: [InvalidLink] Cannot resolve link to location: from text: {location: {address: '234 Street'} 521 | lib/hashie/extensions/deep_find.rb:36: [InvalidLink] Cannot resolve link to location: from text: {location: {address: '234 Street'} 522 | lib/hashie/extensions/deep_find.rb:36: [InvalidLink] Cannot resolve link to location: from text: {location: {address: '234 Street'} 523 | lib/hashie/extensions/deep_find.rb:36: [InvalidLink] Cannot resolve link to location: from text: {location: {address: '234 Street'} 524 | lib/hashie/mash.rb:32: [InvalidLink] Cannot resolve link to :a from text: {:a => {:b => 23, :d => {:e => "abc"} 525 | lib/hashie/mash.rb:32: [InvalidLink] Cannot resolve link to :g from text: {:g => 44, :h => 29} 526 | ``` 527 |
    528 | 529 | ## Authors 530 | 531 | * [Victor Shepelev](https://github.com/zverok) 532 | * [Olle Jonsson](https://github.com/olleolleolle) 533 | 534 | ## License 535 | 536 | MIT 537 | --------------------------------------------------------------------------------