├── lib ├── error_highlight │ ├── version.rb │ ├── formatter.rb │ ├── core_ext.rb │ └── base.rb └── error_highlight.rb ├── .gitignore ├── Gemfile ├── .github ├── dependabot.yml └── workflows │ ├── sync-ruby.yml │ └── ruby.yml ├── Rakefile ├── LICENSE.txt ├── error_highlight.gemspec ├── README.md └── test └── test_error_highlight.rb /lib/error_highlight/version.rb: -------------------------------------------------------------------------------- 1 | module ErrorHighlight 2 | VERSION = "0.7.0" 3 | end 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | -------------------------------------------------------------------------------- /lib/error_highlight.rb: -------------------------------------------------------------------------------- 1 | require_relative "error_highlight/base" 2 | require_relative "error_highlight/core_ext" 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem "rake", "~> 13.0" 6 | gem "test-unit", "~> 3.0" 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rake/testtask" 5 | 6 | Rake::TestTask.new(:test) do |t| 7 | t.libs << "test" 8 | t.libs << "lib" 9 | t.test_files = FileList["test/**/test_*.rb"] 10 | end 11 | 12 | task default: :test 13 | -------------------------------------------------------------------------------- /.github/workflows/sync-ruby.yml: -------------------------------------------------------------------------------- 1 | name: Sync ruby 2 | on: 3 | push: 4 | branches: [master] 5 | jobs: 6 | sync: 7 | name: Sync ruby 8 | runs-on: ubuntu-latest 9 | if: ${{ github.repository_owner == 'ruby' }} 10 | steps: 11 | - uses: actions/checkout@v6 12 | 13 | - name: Create GitHub App token 14 | id: app-token 15 | uses: actions/create-github-app-token@v2 16 | with: 17 | app-id: 2060836 18 | private-key: ${{ secrets.RUBY_SYNC_DEFAULT_GEMS_PRIVATE_KEY }} 19 | owner: ruby 20 | repositories: ruby 21 | 22 | - name: Sync to ruby/ruby 23 | uses: convictional/trigger-workflow-and-wait@v1.6.5 24 | with: 25 | owner: ruby 26 | repo: ruby 27 | workflow_file_name: sync_default_gems.yml 28 | github_token: ${{ steps.app-token.outputs.token }} 29 | ref: master 30 | client_payload: | 31 | {"gem":"${{ github.event.repository.name }}","before":"${{ github.event.before }}","after":"${{ github.event.after }}"} 32 | propagate_failure: true 33 | wait_interval: 10 34 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Yusuke Endoh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /error_highlight.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | begin 5 | require_relative "lib/error_highlight/version" 6 | rescue LoadError # Fallback to load version file in ruby core repository 7 | require_relative "version" 8 | end 9 | 10 | Gem::Specification.new do |spec| 11 | spec.name = "error_highlight" 12 | spec.version = ErrorHighlight::VERSION 13 | spec.authors = ["Yusuke Endoh"] 14 | spec.email = ["mame@ruby-lang.org"] 15 | 16 | spec.summary = 'Shows a one-line code snippet with an underline in the error backtrace' 17 | spec.description = 'The gem enhances Exception#message by adding a short explanation where the exception is raised' 18 | spec.homepage = "https://github.com/ruby/error_highlight" 19 | 20 | spec.license = "MIT" 21 | spec.required_ruby_version = Gem::Requirement.new(">= 3.2.0") 22 | 23 | spec.files = Dir.chdir(File.expand_path(__dir__)) do 24 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) } 25 | end 26 | spec.require_paths = ["lib"] 27 | end 28 | -------------------------------------------------------------------------------- /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | name: Ruby 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - 'master' 7 | push: 8 | branches: 9 | - 'master' 10 | 11 | jobs: 12 | ruby-versions: 13 | uses: ruby/actions/.github/workflows/ruby_versions.yml@master 14 | with: 15 | engine: cruby 16 | min_version: 3.2 17 | 18 | build: 19 | needs: ruby-versions 20 | runs-on: ubuntu-latest 21 | strategy: 22 | matrix: 23 | ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} 24 | steps: 25 | - uses: actions/checkout@v6 26 | - uses: ruby/setup-ruby@v1 27 | with: 28 | ruby-version: ${{ matrix.ruby }} 29 | - name: Bundle install 30 | run: | 31 | bundle install 32 | - name: Run the test suite 33 | run: | 34 | RUBYOPT=--disable-error_highlight bundle exec rake TESTOPT=-v 35 | 36 | prism: 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/checkout@v6 40 | - uses: ruby/setup-ruby@v1 41 | with: 42 | ruby-version: head 43 | bundler-cache: true 44 | - name: Run the test suite 45 | run: | 46 | RUBYOPT="--disable-error_highlight --parser=prism" bundle exec rake TESTOPT=-v 47 | -------------------------------------------------------------------------------- /lib/error_highlight/formatter.rb: -------------------------------------------------------------------------------- 1 | module ErrorHighlight 2 | class DefaultFormatter 3 | MIN_SNIPPET_WIDTH = 20 4 | 5 | def self.message_for(spot) 6 | # currently only a one-line code snippet is supported 7 | return "" unless spot[:first_lineno] == spot[:last_lineno] 8 | 9 | snippet = spot[:snippet] 10 | first_column = spot[:first_column] 11 | last_column = spot[:last_column] 12 | ellipsis = "..." 13 | 14 | # truncate snippet to fit in the viewport 15 | if max_snippet_width && snippet.size > max_snippet_width 16 | available_width = max_snippet_width - ellipsis.size 17 | center = first_column - max_snippet_width / 2 18 | 19 | visible_start = last_column < available_width ? 0 : [center, 0].max 20 | visible_end = visible_start + max_snippet_width 21 | visible_start = snippet.size - max_snippet_width if visible_end > snippet.size 22 | 23 | prefix = visible_start.positive? ? ellipsis : "" 24 | suffix = visible_end < snippet.size ? ellipsis : "" 25 | 26 | snippet = prefix + snippet[(visible_start + prefix.size)...(visible_end - suffix.size)] + suffix 27 | snippet << "\n" unless snippet.end_with?("\n") 28 | 29 | first_column -= visible_start 30 | last_column = [last_column - visible_start, snippet.size - 1].min 31 | end 32 | 33 | indent = snippet[0...first_column].gsub(/[^\t]/, " ") 34 | marker = indent + "^" * (last_column - first_column) 35 | 36 | "\n\n#{ snippet }#{ marker }" 37 | end 38 | 39 | def self.max_snippet_width 40 | return if Ractor.current[:__error_highlight_max_snippet_width__] == :disabled 41 | 42 | Ractor.current[:__error_highlight_max_snippet_width__] ||= terminal_width 43 | end 44 | 45 | def self.max_snippet_width=(width) 46 | return Ractor.current[:__error_highlight_max_snippet_width__] = :disabled if width.nil? 47 | 48 | width = width.to_i 49 | 50 | if width < MIN_SNIPPET_WIDTH 51 | warn "'max_snippet_width' adjusted to minimum value of #{MIN_SNIPPET_WIDTH}." 52 | width = MIN_SNIPPET_WIDTH 53 | end 54 | 55 | Ractor.current[:__error_highlight_max_snippet_width__] = width 56 | end 57 | 58 | def self.terminal_width 59 | # lazy load io/console to avoid loading it when 'max_snippet_width' is manually set 60 | require "io/console" 61 | $stderr.winsize[1] if $stderr.tty? 62 | rescue LoadError, NoMethodError, SystemCallError 63 | # skip truncation when terminal window size is unavailable 64 | end 65 | end 66 | 67 | def self.formatter 68 | Ractor.current[:__error_highlight_formatter__] || DefaultFormatter 69 | end 70 | 71 | def self.formatter=(formatter) 72 | Ractor.current[:__error_highlight_formatter__] = formatter 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/error_highlight/core_ext.rb: -------------------------------------------------------------------------------- 1 | require_relative "formatter" 2 | 3 | module ErrorHighlight 4 | module CoreExt 5 | private def generate_snippet 6 | if ArgumentError === self && message =~ /\A(?:wrong number of arguments|missing keyword[s]?|unknown keyword[s]?|no keywords accepted)\b/ 7 | locs = self.backtrace_locations 8 | return "" if locs.size < 2 9 | callee_loc, caller_loc = locs 10 | callee_spot = ErrorHighlight.spot(self, backtrace_location: callee_loc, point_type: :name) 11 | caller_spot = ErrorHighlight.spot(self, backtrace_location: caller_loc, point_type: :name) 12 | if caller_spot && callee_spot && 13 | caller_loc.path == callee_loc.path && 14 | caller_loc.lineno == callee_loc.lineno && 15 | caller_spot == callee_spot 16 | callee_loc = callee_spot = nil 17 | end 18 | ret = +"\n" 19 | [["caller", caller_loc, caller_spot], ["callee", callee_loc, callee_spot]].each do |header, loc, spot| 20 | out = nil 21 | if loc 22 | out = " #{ header }: #{ loc.path }:#{ loc.lineno }" 23 | if spot 24 | _, _, snippet, highlight = ErrorHighlight.formatter.message_for(spot).lines 25 | out += "\n | #{ snippet } #{ highlight }" 26 | else 27 | # do nothing 28 | end 29 | end 30 | ret << "\n" + out if out 31 | end 32 | ret 33 | else 34 | spot = ErrorHighlight.spot(self) 35 | return "" unless spot 36 | return ErrorHighlight.formatter.message_for(spot) 37 | end 38 | end 39 | 40 | if Exception.method_defined?(:detailed_message) 41 | def detailed_message(highlight: false, error_highlight: true, **) 42 | return super unless error_highlight 43 | snippet = generate_snippet 44 | if highlight 45 | snippet = snippet.gsub(/.+/) { "\e[1m" + $& + "\e[m" } 46 | end 47 | super + snippet 48 | end 49 | else 50 | # This is a marker to let `DidYouMean::Correctable#original_message` skip 51 | # the following method definition of `to_s`. 52 | # See https://github.com/ruby/did_you_mean/pull/152 53 | SKIP_TO_S_FOR_SUPER_LOOKUP = true 54 | private_constant :SKIP_TO_S_FOR_SUPER_LOOKUP 55 | 56 | def to_s 57 | msg = super 58 | snippet = generate_snippet 59 | if snippet != "" && !msg.include?(snippet) 60 | msg + snippet 61 | else 62 | msg 63 | end 64 | end 65 | end 66 | end 67 | 68 | NameError.prepend(CoreExt) 69 | 70 | if Exception.method_defined?(:detailed_message) 71 | # ErrorHighlight is enabled for TypeError and ArgumentError only when Exception#detailed_message is available. 72 | # This is because changing ArgumentError#message is highly incompatible. 73 | TypeError.prepend(CoreExt) 74 | ArgumentError.prepend(CoreExt) 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ErrorHighlight 2 | 3 | ## Installation 4 | 5 | Ruby 3.1 will ship with this gem and it will automatically be `require`d when a Ruby process starts up. No special setup is required. 6 | 7 | Note: This gem works only on MRI and requires Ruby 3.1 or later because it depends on MRI's internal APIs that are available since 3.1. 8 | 9 | ## Examples 10 | 11 | ```ruby 12 | 1.time {} 13 | ``` 14 | 15 | ``` 16 | $ ruby test.rb 17 | test.rb:1:in `
': undefined method `time' for 1:Integer (NoMethodError) 18 | 19 | 1.time {} 20 | ^^^^^ 21 | Did you mean? times 22 | ``` 23 | 24 | ## More example 25 | 26 | ```ruby 27 | def extract_value(data) 28 | data[:results].first[:value] 29 | end 30 | ``` 31 | 32 | When `data` is `{ :results => [] }`, the following error message is shown: 33 | 34 | ``` 35 | $ ruby test.rb 36 | test.rb:2:in `extract_value': undefined method `[]' for nil:NilClass (NoMethodError) 37 | 38 | data[:results].first[:value] 39 | ^^^^^^^^ 40 | from test.rb:5:in `
' 41 | ``` 42 | 43 | When `data` is `nil`, it prints: 44 | 45 | ``` 46 | $ ruby test.rb 47 | test.rb:2:in `extract_value': undefined method `[]' for nil:NilClass (NoMethodError) 48 | 49 | data[:results].first[:value] 50 | ^^^^^^^^^^ 51 | from test.rb:5:in `
' 52 | ``` 53 | 54 | ## Using the `ErrorHighlight.spot` 55 | 56 | *Note: This API is experimental, may change in future.* 57 | 58 | You can use the `ErrorHighlight.spot` method to get the snippet data. 59 | Note that the argument must be a RubyVM::AbstractSyntaxTree::Node object that is created with `keep_script_lines: true` option (which is available since Ruby 3.1). 60 | 61 | ```ruby 62 | class Dummy 63 | def test(_dummy_arg) 64 | node = RubyVM::AbstractSyntaxTree.of(caller_locations.first, keep_script_lines: true) 65 | ErrorHighlight.spot(node) 66 | end 67 | end 68 | 69 | pp Dummy.new.test(42) # <- Line 8 70 | # ^^^^^ <- Column 12--17 71 | 72 | #=> {:first_lineno=>8, 73 | # :first_column=>12, 74 | # :last_lineno=>8, 75 | # :last_column=>17, 76 | # :snippet=>"pp Dummy.new.test(42) # <- Line 8\n"} 77 | ``` 78 | 79 | ## Custom Formatter 80 | 81 | If you want to customize the message format for code snippet, use `ErrorHighlight.formatter=` to set your custom object that responds to `message_for` method. 82 | 83 | ```ruby 84 | formatter = Object.new 85 | def formatter.message_for(spot) 86 | marker = " " * spot[:first_column] + "^" + "~" * (spot[:last_column] - spot[:first_column] - 1) 87 | 88 | "\n\n#{ spot[:snippet] }#{ marker }" 89 | end 90 | 91 | ErrorHighlight.formatter = formatter 92 | 93 | 1.time {} 94 | 95 | #=> 96 | # 97 | # test.rb:10:in `
': undefined method `time' for 1:Integer (NoMethodError) 98 | # 99 | # 1.time {} 100 | # ^~~~~ 101 | # Did you mean? times 102 | ``` 103 | 104 | ## Disabling `error_highlight` 105 | 106 | Occasionally, you may want to disable the `error_highlight` gem for e.g. debugging issues in the error object itself. You 107 | can disable it entirely by specifying `--disable-error_highlight` option to the `ruby` command: 108 | 109 | ```bash 110 | $ ruby --disable-error_highlight -e '1.time {}' 111 | -e:1:in `
': undefined method `time' for 1:Integer (NoMethodError) 112 | Did you mean? times 113 | ``` 114 | 115 | ## Contributing 116 | 117 | Bug reports and pull requests are welcome on GitHub at https://github.com/ruby/error_highlight. 118 | 119 | ## License 120 | 121 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 122 | -------------------------------------------------------------------------------- /lib/error_highlight/base.rb: -------------------------------------------------------------------------------- 1 | require_relative "version" 2 | 3 | module ErrorHighlight 4 | # Identify the code fragment where a given exception occurred. 5 | # 6 | # Options: 7 | # 8 | # point_type: :name | :args 9 | # :name (default) points to the method/variable name where the exception occurred. 10 | # :args points to the arguments of the method call where the exception occurred. 11 | # 12 | # backtrace_location: Thread::Backtrace::Location 13 | # It locates the code fragment of the given backtrace_location. 14 | # By default, it uses the first frame of backtrace_locations of the given exception. 15 | # 16 | # Returns: 17 | # { 18 | # first_lineno: Integer, 19 | # first_column: Integer, 20 | # last_lineno: Integer, 21 | # last_column: Integer, 22 | # snippet: String, 23 | # script_lines: [String], 24 | # } | nil 25 | # 26 | # Limitations: 27 | # 28 | # Currently, ErrorHighlight.spot only supports a single-line code fragment. 29 | # Therefore, if the return value is not nil, first_lineno and last_lineno will have 30 | # the same value. If the relevant code fragment spans multiple lines 31 | # (e.g., Array#[] of +ary[(newline)expr(newline)]+), the method will return nil. 32 | # This restriction may be removed in the future. 33 | def self.spot(obj, **opts) 34 | case obj 35 | when Exception 36 | exc = obj 37 | loc = opts[:backtrace_location] 38 | opts = { point_type: opts.fetch(:point_type, :name) } 39 | 40 | unless loc 41 | case exc 42 | when TypeError, ArgumentError 43 | opts[:point_type] = :args 44 | end 45 | 46 | locs = exc.backtrace_locations 47 | return nil unless locs 48 | 49 | loc = locs.first 50 | return nil unless loc 51 | 52 | opts[:name] = exc.name if NameError === obj 53 | end 54 | 55 | return nil unless Thread::Backtrace::Location === loc 56 | 57 | node = 58 | begin 59 | RubyVM::AbstractSyntaxTree.of(loc, keep_script_lines: true) 60 | rescue RuntimeError => error 61 | # RubyVM::AbstractSyntaxTree.of raises an error with a message that 62 | # includes "prism" when the ISEQ was compiled with the prism compiler. 63 | # In this case, we'll try to parse again with prism instead. 64 | raise unless error.message.include?("prism") 65 | prism_find(loc) 66 | end 67 | 68 | Spotter.new(node, **opts).spot 69 | 70 | when RubyVM::AbstractSyntaxTree::Node, Prism::Node 71 | Spotter.new(obj, **opts).spot 72 | 73 | else 74 | raise TypeError, "Exception is expected" 75 | end 76 | 77 | rescue SyntaxError, 78 | SystemCallError, # file not found or something 79 | ArgumentError # eval'ed code 80 | 81 | return nil 82 | end 83 | 84 | # Accepts a Thread::Backtrace::Location object and returns a Prism::Node 85 | # corresponding to the backtrace location in the source code. 86 | def self.prism_find(location) 87 | require "prism" 88 | return nil if Prism::VERSION < "1.0.0" 89 | 90 | absolute_path = location.absolute_path 91 | return unless absolute_path 92 | 93 | node_id = RubyVM::AbstractSyntaxTree.node_id_for_backtrace_location(location) 94 | Prism.parse_file(absolute_path).value.breadth_first_search { |node| node.node_id == node_id } 95 | end 96 | 97 | private_class_method :prism_find 98 | 99 | class Spotter 100 | class NonAscii < Exception; end 101 | private_constant :NonAscii 102 | 103 | def initialize(node, point_type: :name, name: nil) 104 | @node = node 105 | @point_type = point_type 106 | @name = name 107 | 108 | # Not-implemented-yet options 109 | @arg = nil # Specify the index or keyword at which argument caused the TypeError/ArgumentError 110 | @multiline = false # Allow multiline spot 111 | 112 | @fetch = -> (lineno, last_lineno = lineno) do 113 | snippet = @node.script_lines[lineno - 1 .. last_lineno - 1].join("") 114 | snippet += "\n" unless snippet.end_with?("\n") 115 | 116 | # It requires some work to support Unicode (or multibyte) characters. 117 | # Tentatively, we stop highlighting if the code snippet has non-ascii characters. 118 | # See https://github.com/ruby/error_highlight/issues/4 119 | raise NonAscii unless snippet.ascii_only? 120 | 121 | snippet 122 | end 123 | end 124 | 125 | def spot 126 | return nil unless @node 127 | 128 | # In Ruby 3.2 or later, a nested constant access (like `Foo::Bar::Baz`) 129 | # is compiled to one instruction (opt_getconstant_path). 130 | # @node points to the node of the whole `Foo::Bar::Baz` even if `Foo` 131 | # or `Foo::Bar` causes NameError. 132 | # So we try to spot the sub-node that causes the NameError by using 133 | # `NameError#name`. 134 | case @node.type 135 | when :COLON2 136 | subnodes = [] 137 | node = @node 138 | while node.type == :COLON2 139 | node2, const = node.children 140 | subnodes << node if const == @name 141 | node = node2 142 | end 143 | if node.type == :CONST || node.type == :COLON3 144 | if node.children.first == @name 145 | subnodes << node 146 | end 147 | 148 | # If we found only one sub-node whose name is equal to @name, use it 149 | return nil if subnodes.size != 1 150 | @node = subnodes.first 151 | else 152 | # Do nothing; opt_getconstant_path is used only when the const base is 153 | # NODE_CONST (`Foo`) or NODE_COLON3 (`::Foo`) 154 | end 155 | when :constant_path_node 156 | subnodes = [] 157 | node = @node 158 | 159 | begin 160 | subnodes << node if node.name == @name 161 | end while (node = node.parent).is_a?(Prism::ConstantPathNode) 162 | 163 | if node.is_a?(Prism::ConstantReadNode) && node.name == @name 164 | subnodes << node 165 | end 166 | 167 | # If we found only one sub-node whose name is equal to @name, use it 168 | return nil if subnodes.size != 1 169 | @node = subnodes.first 170 | end 171 | 172 | case @node.type 173 | 174 | when :CALL, :QCALL 175 | case @point_type 176 | when :name 177 | spot_call_for_name 178 | when :args 179 | spot_call_for_args 180 | end 181 | 182 | when :ATTRASGN 183 | case @point_type 184 | when :name 185 | spot_attrasgn_for_name 186 | when :args 187 | spot_attrasgn_for_args 188 | end 189 | 190 | when :OPCALL 191 | case @point_type 192 | when :name 193 | spot_opcall_for_name 194 | when :args 195 | spot_opcall_for_args 196 | end 197 | 198 | when :FCALL 199 | case @point_type 200 | when :name 201 | spot_fcall_for_name 202 | when :args 203 | spot_fcall_for_args 204 | end 205 | 206 | when :VCALL 207 | spot_vcall 208 | 209 | when :OP_ASGN1 210 | case @point_type 211 | when :name 212 | spot_op_asgn1_for_name 213 | when :args 214 | spot_op_asgn1_for_args 215 | end 216 | 217 | when :OP_ASGN2 218 | case @point_type 219 | when :name 220 | spot_op_asgn2_for_name 221 | when :args 222 | spot_op_asgn2_for_args 223 | end 224 | 225 | when :CONST 226 | spot_vcall 227 | 228 | when :COLON2 229 | spot_colon2 230 | 231 | when :COLON3 232 | spot_vcall 233 | 234 | when :OP_CDECL 235 | spot_op_cdecl 236 | 237 | when :DEFN 238 | raise NotImplementedError if @point_type != :name 239 | spot_defn 240 | 241 | when :DEFS 242 | raise NotImplementedError if @point_type != :name 243 | spot_defs 244 | 245 | when :LAMBDA 246 | spot_lambda 247 | 248 | when :ITER 249 | spot_iter 250 | 251 | when :call_node 252 | case @point_type 253 | when :name 254 | prism_spot_call_for_name 255 | when :args 256 | prism_spot_call_for_args 257 | end 258 | 259 | when :local_variable_operator_write_node 260 | case @point_type 261 | when :name 262 | prism_spot_local_variable_operator_write_for_name 263 | when :args 264 | prism_spot_local_variable_operator_write_for_args 265 | end 266 | 267 | when :call_operator_write_node 268 | case @point_type 269 | when :name 270 | prism_spot_call_operator_write_for_name 271 | when :args 272 | prism_spot_call_operator_write_for_args 273 | end 274 | 275 | when :index_operator_write_node 276 | case @point_type 277 | when :name 278 | prism_spot_index_operator_write_for_name 279 | when :args 280 | prism_spot_index_operator_write_for_args 281 | end 282 | 283 | when :constant_read_node 284 | prism_spot_constant_read 285 | 286 | when :constant_path_node 287 | prism_spot_constant_path 288 | 289 | when :constant_path_operator_write_node 290 | prism_spot_constant_path_operator_write 291 | 292 | when :def_node 293 | case @point_type 294 | when :name 295 | prism_spot_def_for_name 296 | when :args 297 | raise NotImplementedError 298 | end 299 | 300 | when :lambda_node 301 | case @point_type 302 | when :name 303 | prism_spot_lambda_for_name 304 | when :args 305 | raise NotImplementedError 306 | end 307 | 308 | when :block_node 309 | case @point_type 310 | when :name 311 | prism_spot_block_for_name 312 | when :args 313 | raise NotImplementedError 314 | end 315 | 316 | end 317 | 318 | if @snippet && @beg_column && @end_column && @beg_column < @end_column 319 | return { 320 | first_lineno: @beg_lineno, 321 | first_column: @beg_column, 322 | last_lineno: @end_lineno, 323 | last_column: @end_column, 324 | snippet: @snippet, 325 | script_lines: @node.script_lines, 326 | } 327 | else 328 | return nil 329 | end 330 | 331 | rescue NonAscii 332 | nil 333 | end 334 | 335 | private 336 | 337 | # Example: 338 | # x.foo 339 | # ^^^^ 340 | # x.foo(42) 341 | # ^^^^ 342 | # x&.foo 343 | # ^^^^^ 344 | # x[42] 345 | # ^^^^ 346 | # x += 1 347 | # ^ 348 | def spot_call_for_name 349 | nd_recv, mid, nd_args = @node.children 350 | lineno = nd_recv.last_lineno 351 | lines = @fetch[lineno, @node.last_lineno] 352 | if mid == :[] && lines.match(/\G[\s)]*(\[(?:\s*\])?)/, nd_recv.last_column) 353 | @beg_column = $~.begin(1) 354 | @snippet = lines[/.*\n/] 355 | @beg_lineno = @end_lineno = lineno 356 | if nd_args 357 | if nd_recv.last_lineno == nd_args.last_lineno && @snippet.match(/\s*\]/, nd_args.last_column) 358 | @end_column = $~.end(0) 359 | end 360 | else 361 | if lines.match(/\G[\s)]*?\[\s*\]/, nd_recv.last_column) 362 | @end_column = $~.end(0) 363 | end 364 | end 365 | elsif lines.match(/\G[\s)]*?(\&?\.)(\s*?)(#{ Regexp.quote(mid) }).*\n/, nd_recv.last_column) 366 | lines = $` + $& 367 | @beg_column = $~.begin($2.include?("\n") ? 3 : 1) 368 | @end_column = $~.end(3) 369 | if i = lines[..@beg_column].rindex("\n") 370 | @beg_lineno = @end_lineno = lineno + lines[..@beg_column].count("\n") 371 | @snippet = lines[i + 1..] 372 | @beg_column -= i + 1 373 | @end_column -= i + 1 374 | else 375 | @snippet = lines 376 | @beg_lineno = @end_lineno = lineno 377 | end 378 | elsif mid.to_s =~ /\A\W+\z/ && lines.match(/\G\s*(#{ Regexp.quote(mid) })=.*\n/, nd_recv.last_column) 379 | @snippet = $` + $& 380 | @beg_lineno = @end_lineno = lineno 381 | @beg_column = $~.begin(1) 382 | @end_column = $~.end(1) 383 | end 384 | end 385 | 386 | # Example: 387 | # x.foo(42) 388 | # ^^ 389 | # x[42] 390 | # ^^ 391 | # x += 1 392 | # ^ 393 | def spot_call_for_args 394 | _nd_recv, _mid, nd_args = @node.children 395 | if nd_args && nd_args.first_lineno == nd_args.last_lineno 396 | fetch_line(nd_args.first_lineno) 397 | @beg_column = nd_args.first_column 398 | @end_column = nd_args.last_column 399 | end 400 | # TODO: support @arg 401 | end 402 | 403 | # Example: 404 | # x.foo = 1 405 | # ^^^^^^ 406 | # x[42] = 1 407 | # ^^^^^^ 408 | def spot_attrasgn_for_name 409 | nd_recv, mid, nd_args = @node.children 410 | *nd_args, _nd_last_arg, _nil = nd_args.children 411 | fetch_line(nd_recv.last_lineno) 412 | if mid == :[]= && @snippet.match(/\G[\s)]*(\[)/, nd_recv.last_column) 413 | @beg_column = $~.begin(1) 414 | args_last_column = $~.end(0) 415 | if nd_args.last && nd_recv.last_lineno == nd_args.last.last_lineno 416 | args_last_column = nd_args.last.last_column 417 | end 418 | if @snippet.match(/[\s)]*\]\s*=/, args_last_column) 419 | @end_column = $~.end(0) 420 | end 421 | elsif @snippet.match(/\G[\s)]*(\.\s*#{ Regexp.quote(mid.to_s.sub(/=\z/, "")) }\s*=)/, nd_recv.last_column) 422 | @beg_column = $~.begin(1) 423 | @end_column = $~.end(1) 424 | end 425 | end 426 | 427 | # Example: 428 | # x.foo = 1 429 | # ^ 430 | # x[42] = 1 431 | # ^^^^^^^ 432 | # x[] = 1 433 | # ^^^^^ 434 | def spot_attrasgn_for_args 435 | nd_recv, mid, nd_args = @node.children 436 | fetch_line(nd_recv.last_lineno) 437 | if mid == :[]= && @snippet.match(/\G[\s)]*\[/, nd_recv.last_column) 438 | @beg_column = $~.end(0) 439 | if nd_recv.last_lineno == nd_args.last_lineno 440 | @end_column = nd_args.last_column 441 | end 442 | elsif nd_args && nd_args.first_lineno == nd_args.last_lineno 443 | @beg_column = nd_args.first_column 444 | @end_column = nd_args.last_column 445 | end 446 | # TODO: support @arg 447 | end 448 | 449 | # Example: 450 | # x + 1 451 | # ^ 452 | # +x 453 | # ^ 454 | def spot_opcall_for_name 455 | nd_recv, op, nd_arg = @node.children 456 | fetch_line(nd_recv.last_lineno) 457 | if nd_arg 458 | # binary operator 459 | if @snippet.match(/\G[\s)]*(#{ Regexp.quote(op) })/, nd_recv.last_column) 460 | @beg_column = $~.begin(1) 461 | @end_column = $~.end(1) 462 | end 463 | else 464 | # unary operator 465 | if @snippet[...nd_recv.first_column].match(/(#{ Regexp.quote(op.to_s.sub(/@\z/, "")) })\s*\(?\s*\z/) 466 | @beg_column = $~.begin(1) 467 | @end_column = $~.end(1) 468 | end 469 | end 470 | end 471 | 472 | # Example: 473 | # x + 1 474 | # ^ 475 | def spot_opcall_for_args 476 | _nd_recv, _op, nd_arg = @node.children 477 | if nd_arg && nd_arg.first_lineno == nd_arg.last_lineno 478 | # binary operator 479 | fetch_line(nd_arg.first_lineno) 480 | @beg_column = nd_arg.first_column 481 | @end_column = nd_arg.last_column 482 | end 483 | end 484 | 485 | # Example: 486 | # foo(42) 487 | # ^^^ 488 | # foo 42 489 | # ^^^ 490 | def spot_fcall_for_name 491 | mid, _nd_args = @node.children 492 | fetch_line(@node.first_lineno) 493 | if @snippet.match(/(#{ Regexp.quote(mid) })/, @node.first_column) 494 | @beg_column = $~.begin(1) 495 | @end_column = $~.end(1) 496 | end 497 | end 498 | 499 | # Example: 500 | # foo(42) 501 | # ^^ 502 | # foo 42 503 | # ^^ 504 | def spot_fcall_for_args 505 | _mid, nd_args = @node.children 506 | if nd_args && nd_args.first_lineno == nd_args.last_lineno 507 | fetch_line(nd_args.first_lineno) 508 | @beg_column = nd_args.first_column 509 | @end_column = nd_args.last_column 510 | end 511 | end 512 | 513 | # Example: 514 | # foo 515 | # ^^^ 516 | def spot_vcall 517 | if @node.first_lineno == @node.last_lineno 518 | fetch_line(@node.last_lineno) 519 | @beg_column = @node.first_column 520 | @end_column = @node.last_column 521 | end 522 | end 523 | 524 | # Example: 525 | # x[1] += 42 526 | # ^^^ (for []) 527 | # x[1] += 42 528 | # ^ (for +) 529 | # x[1] += 42 530 | # ^^^^^^ (for []=) 531 | def spot_op_asgn1_for_name 532 | nd_recv, op, nd_args, _nd_rhs = @node.children 533 | fetch_line(nd_recv.last_lineno) 534 | if @snippet.match(/\G[\s)]*(\[)/, nd_recv.last_column) 535 | bracket_beg_column = $~.begin(1) 536 | args_last_column = $~.end(0) 537 | if nd_args && nd_recv.last_lineno == nd_args.last_lineno 538 | args_last_column = nd_args.last_column 539 | end 540 | if @snippet.match(/\s*\](\s*)(#{ Regexp.quote(op) })=()/, args_last_column) 541 | case @name 542 | when :[], :[]= 543 | @beg_column = bracket_beg_column 544 | @end_column = $~.begin(@name == :[] ? 1 : 3) 545 | when op 546 | @beg_column = $~.begin(2) 547 | @end_column = $~.end(2) 548 | end 549 | end 550 | end 551 | end 552 | 553 | # Example: 554 | # x[1] += 42 555 | # ^^^^^^^^ 556 | def spot_op_asgn1_for_args 557 | nd_recv, mid, nd_args, nd_rhs = @node.children 558 | fetch_line(nd_recv.last_lineno) 559 | if mid == :[]= && @snippet.match(/\G\s*\[/, nd_recv.last_column) 560 | @beg_column = $~.end(0) 561 | if nd_recv.last_lineno == nd_rhs.last_lineno 562 | @end_column = nd_rhs.last_column 563 | end 564 | elsif nd_args && nd_args.first_lineno == nd_rhs.last_lineno 565 | @beg_column = nd_args.first_column 566 | @end_column = nd_rhs.last_column 567 | end 568 | # TODO: support @arg 569 | end 570 | 571 | # Example: 572 | # x.foo += 42 573 | # ^^^ (for foo) 574 | # x.foo += 42 575 | # ^ (for +) 576 | # x.foo += 42 577 | # ^^^^^^^ (for foo=) 578 | def spot_op_asgn2_for_name 579 | nd_recv, _qcall, attr, op, _nd_rhs = @node.children 580 | fetch_line(nd_recv.last_lineno) 581 | if @snippet.match(/\G[\s)]*(\.)\s*#{ Regexp.quote(attr) }()\s*(#{ Regexp.quote(op) })(=)/, nd_recv.last_column) 582 | case @name 583 | when attr 584 | @beg_column = $~.begin(1) 585 | @end_column = $~.begin(2) 586 | when op 587 | @beg_column = $~.begin(3) 588 | @end_column = $~.end(3) 589 | when :"#{ attr }=" 590 | @beg_column = $~.begin(1) 591 | @end_column = $~.end(4) 592 | end 593 | end 594 | end 595 | 596 | # Example: 597 | # x.foo += 42 598 | # ^^ 599 | def spot_op_asgn2_for_args 600 | _nd_recv, _qcall, _attr, _op, nd_rhs = @node.children 601 | if nd_rhs.first_lineno == nd_rhs.last_lineno 602 | fetch_line(nd_rhs.first_lineno) 603 | @beg_column = nd_rhs.first_column 604 | @end_column = nd_rhs.last_column 605 | end 606 | end 607 | 608 | # Example: 609 | # Foo::Bar 610 | # ^^^^^ 611 | def spot_colon2 612 | nd_parent, const = @node.children 613 | if nd_parent.last_lineno == @node.last_lineno 614 | fetch_line(nd_parent.last_lineno) 615 | @beg_column = nd_parent.last_column 616 | @end_column = @node.last_column 617 | else 618 | fetch_line(@node.last_lineno) 619 | if @snippet[...@node.last_column].match(/#{ Regexp.quote(const) }\z/) 620 | @beg_lineno = @end_lineno = @node.last_lineno 621 | @beg_column = $~.begin(0) 622 | @end_column = $~.end(0) 623 | end 624 | end 625 | end 626 | 627 | # Example: 628 | # Foo::Bar += 1 629 | # ^^^^^^^^ 630 | def spot_op_cdecl 631 | nd_lhs, op, _nd_rhs = @node.children 632 | *nd_parent_lhs, _const = nd_lhs.children 633 | if @name == op 634 | fetch_line(nd_lhs.last_lineno) 635 | if @snippet.match(/\G\s*(#{ Regexp.quote(op) })=/, nd_lhs.last_column) 636 | @beg_column = $~.begin(1) 637 | @end_column = $~.end(1) 638 | end 639 | else 640 | # constant access error 641 | @end_column = nd_lhs.last_column 642 | if nd_parent_lhs.empty? # example: ::C += 1 643 | if nd_lhs.first_lineno == nd_lhs.last_lineno 644 | fetch_line(nd_lhs.last_lineno) 645 | @beg_column = nd_lhs.first_column 646 | end 647 | else # example: Foo::Bar::C += 1 648 | if nd_parent_lhs.last.last_lineno == nd_lhs.last_lineno 649 | fetch_line(nd_lhs.last_lineno) 650 | @beg_column = nd_parent_lhs.last.last_column 651 | end 652 | end 653 | end 654 | end 655 | 656 | # Example: 657 | # def bar; end 658 | # ^^^ 659 | def spot_defn 660 | mid, = @node.children 661 | fetch_line(@node.first_lineno) 662 | if @snippet.match(/\Gdef\s+(#{ Regexp.quote(mid) }\b)/, @node.first_column) 663 | @beg_column = $~.begin(1) 664 | @end_column = $~.end(1) 665 | end 666 | end 667 | 668 | # Example: 669 | # def Foo.bar; end 670 | # ^^^^ 671 | def spot_defs 672 | nd_recv, mid, = @node.children 673 | fetch_line(nd_recv.last_lineno) 674 | if @snippet.match(/\G\s*(\.\s*#{ Regexp.quote(mid) }\b)/, nd_recv.last_column) 675 | @beg_column = $~.begin(1) 676 | @end_column = $~.end(1) 677 | end 678 | end 679 | 680 | # Example: 681 | # -> { ... } 682 | # ^^ 683 | def spot_lambda 684 | fetch_line(@node.first_lineno) 685 | if @snippet.match(/\G->/, @node.first_column) 686 | @beg_column = $~.begin(0) 687 | @end_column = $~.end(0) 688 | end 689 | end 690 | 691 | # Example: 692 | # lambda { ... } 693 | # ^ 694 | # define_method :foo do 695 | # ^^ 696 | def spot_iter 697 | _nd_fcall, nd_scope = @node.children 698 | fetch_line(nd_scope.first_lineno) 699 | if @snippet.match(/\G(?:do\b|\{)/, nd_scope.first_column) 700 | @beg_column = $~.begin(0) 701 | @end_column = $~.end(0) 702 | end 703 | end 704 | 705 | def fetch_line(lineno) 706 | @beg_lineno = @end_lineno = lineno 707 | @snippet = @fetch[lineno] 708 | end 709 | 710 | # Take a location from the prism parser and set the necessary instance 711 | # variables. 712 | def prism_location(location) 713 | @beg_lineno = location.start_line 714 | @beg_column = location.start_column 715 | @end_lineno = location.end_line 716 | @end_column = location.end_column 717 | @snippet = @fetch[@beg_lineno, @end_lineno] 718 | end 719 | 720 | # Example: 721 | # x.foo 722 | # ^^^^ 723 | # x.foo(42) 724 | # ^^^^ 725 | # x&.foo 726 | # ^^^^^ 727 | # x[42] 728 | # ^^^^ 729 | # x.foo = 1 730 | # ^^^^^^ 731 | # x[42] = 1 732 | # ^^^^^^ 733 | # x + 1 734 | # ^ 735 | # +x 736 | # ^ 737 | # foo(42) 738 | # ^^^ 739 | # foo 42 740 | # ^^^ 741 | # foo 742 | # ^^^ 743 | def prism_spot_call_for_name 744 | # Explicitly turn off foo.() syntax because error_highlight expects this 745 | # to not work. 746 | return nil if @node.name == :call && @node.message_loc.nil? 747 | 748 | location = @node.message_loc || @node.call_operator_loc || @node.location 749 | location = @node.call_operator_loc.join(location) if @node.call_operator_loc&.start_line == location.start_line 750 | 751 | # If the method name ends with "=" but the message does not, then this is 752 | # a method call using the "attribute assignment" syntax 753 | # (e.g., foo.bar = 1). In this case we need to go retrieve the = sign and 754 | # add it to the location. 755 | if (name = @node.name).end_with?("=") && !@node.message.end_with?("=") 756 | location = location.adjoin("=") 757 | end 758 | 759 | prism_location(location) 760 | 761 | if !name.end_with?("=") && !name.match?(/[[:alpha:]_\[]/) 762 | # If the method name is an operator, then error_highlight only 763 | # highlights the first line. 764 | fetch_line(location.start_line) 765 | end 766 | end 767 | 768 | # Example: 769 | # x.foo(42) 770 | # ^^ 771 | # x[42] 772 | # ^^ 773 | # x.foo = 1 774 | # ^ 775 | # x[42] = 1 776 | # ^^^^^^^ 777 | # x[] = 1 778 | # ^^^^^ 779 | # x + 1 780 | # ^ 781 | # foo(42) 782 | # ^^ 783 | # foo 42 784 | # ^^ 785 | def prism_spot_call_for_args 786 | # Disallow highlighting arguments if there are no arguments. 787 | return if @node.arguments.nil? 788 | 789 | # Explicitly turn off foo.() syntax because error_highlight expects this 790 | # to not work. 791 | return nil if @node.name == :call && @node.message_loc.nil? 792 | 793 | if @node.name == :[]= && @node.opening == "[" && (@node.arguments&.arguments || []).length == 1 794 | prism_location(@node.opening_loc.copy(start_offset: @node.opening_loc.start_offset + 1).join(@node.arguments.location)) 795 | else 796 | prism_location(@node.arguments.location) 797 | end 798 | end 799 | 800 | # Example: 801 | # x += 1 802 | # ^ 803 | def prism_spot_local_variable_operator_write_for_name 804 | prism_location(@node.binary_operator_loc.chop) 805 | end 806 | 807 | # Example: 808 | # x += 1 809 | # ^ 810 | def prism_spot_local_variable_operator_write_for_args 811 | prism_location(@node.value.location) 812 | end 813 | 814 | # Example: 815 | # x.foo += 42 816 | # ^^^ (for foo) 817 | # x.foo += 42 818 | # ^ (for +) 819 | # x.foo += 42 820 | # ^^^^^^^ (for foo=) 821 | def prism_spot_call_operator_write_for_name 822 | if !@name.start_with?(/[[:alpha:]_]/) 823 | prism_location(@node.binary_operator_loc.chop) 824 | else 825 | location = @node.message_loc 826 | if @node.call_operator_loc.start_line == location.start_line 827 | location = @node.call_operator_loc.join(location) 828 | end 829 | 830 | location = location.adjoin("=") if @name.end_with?("=") 831 | prism_location(location) 832 | end 833 | end 834 | 835 | # Example: 836 | # x.foo += 42 837 | # ^^ 838 | def prism_spot_call_operator_write_for_args 839 | prism_location(@node.value.location) 840 | end 841 | 842 | # Example: 843 | # x[1] += 42 844 | # ^^^ (for []) 845 | # x[1] += 42 846 | # ^ (for +) 847 | # x[1] += 42 848 | # ^^^^^^ (for []=) 849 | def prism_spot_index_operator_write_for_name 850 | case @name 851 | when :[] 852 | prism_location(@node.opening_loc.join(@node.closing_loc)) 853 | when :[]= 854 | prism_location(@node.opening_loc.join(@node.closing_loc).adjoin("=")) 855 | else 856 | # Explicitly turn off foo[] += 1 syntax when the operator is not on 857 | # the same line because error_highlight expects this to not work. 858 | return nil if @node.binary_operator_loc.start_line != @node.opening_loc.start_line 859 | 860 | prism_location(@node.binary_operator_loc.chop) 861 | end 862 | end 863 | 864 | # Example: 865 | # x[1] += 42 866 | # ^^^^^^^^ 867 | def prism_spot_index_operator_write_for_args 868 | opening_loc = 869 | if @node.arguments.nil? 870 | @node.opening_loc.copy(start_offset: @node.opening_loc.start_offset + 1) 871 | else 872 | @node.arguments.location 873 | end 874 | 875 | prism_location(opening_loc.join(@node.value.location)) 876 | end 877 | 878 | # Example: 879 | # Foo 880 | # ^^^ 881 | def prism_spot_constant_read 882 | prism_location(@node.location) 883 | end 884 | 885 | # Example: 886 | # Foo::Bar 887 | # ^^^^^ 888 | def prism_spot_constant_path 889 | if @node.parent && @node.parent.location.end_line == @node.location.end_line 890 | fetch_line(@node.parent.location.end_line) 891 | prism_location(@node.delimiter_loc.join(@node.name_loc)) 892 | else 893 | fetch_line(@node.location.end_line) 894 | location = @node.name_loc 895 | location = @node.delimiter_loc.join(location) if @node.delimiter_loc.end_line == location.start_line 896 | prism_location(location) 897 | end 898 | end 899 | 900 | # Example: 901 | # Foo::Bar += 1 902 | # ^^^^^^^^ 903 | def prism_spot_constant_path_operator_write 904 | if @name == (target = @node.target).name 905 | prism_location(target.delimiter_loc.join(target.name_loc)) 906 | else 907 | prism_location(@node.binary_operator_loc.chop) 908 | end 909 | end 910 | 911 | # Example: 912 | # def foo() 913 | # ^^^ 914 | def prism_spot_def_for_name 915 | location = @node.name_loc 916 | location = @node.operator_loc.join(location) if @node.operator_loc 917 | prism_location(location) 918 | end 919 | 920 | # Example: 921 | # -> x, y { } 922 | # ^^ 923 | def prism_spot_lambda_for_name 924 | prism_location(@node.operator_loc) 925 | end 926 | 927 | # Example: 928 | # lambda { } 929 | # ^ 930 | # define_method :foo do |x, y| 931 | # ^ 932 | def prism_spot_block_for_name 933 | prism_location(@node.opening_loc) 934 | end 935 | end 936 | 937 | private_constant :Spotter 938 | end 939 | -------------------------------------------------------------------------------- /test/test_error_highlight.rb: -------------------------------------------------------------------------------- 1 | require "test/unit" 2 | 3 | require "error_highlight" 4 | require "did_you_mean" 5 | require "tempfile" 6 | 7 | class ErrorHighlightTest < Test::Unit::TestCase 8 | ErrorHighlight::DefaultFormatter.max_snippet_width = 80 9 | 10 | class DummyFormatter 11 | def self.message_for(corrections) 12 | "" 13 | end 14 | end 15 | 16 | def setup 17 | if defined?(DidYouMean) 18 | @did_you_mean_old_formatter = DidYouMean.formatter 19 | DidYouMean.formatter = DummyFormatter 20 | end 21 | end 22 | 23 | def teardown 24 | if defined?(DidYouMean) 25 | DidYouMean.formatter = @did_you_mean_old_formatter 26 | end 27 | end 28 | 29 | begin 30 | method_not_exist 31 | rescue NameError 32 | if $!.message.include?("`") 33 | def preprocess(msg) 34 | msg 35 | end 36 | else 37 | def preprocess(msg) 38 | msg.sub("`", "'") 39 | end 40 | end 41 | end 42 | 43 | if Exception.method_defined?(:detailed_message) 44 | def assert_error_message(klass, expected_msg, &blk) 45 | omit unless klass < ErrorHighlight::CoreExt 46 | err = assert_raise(klass, &blk) 47 | unless klass == ArgumentError && err.message =~ /\A(?:wrong number of arguments|missing keyword[s]?|unknown keyword[s]?|no keywords accepted)\b/ 48 | spot = ErrorHighlight.spot(err) 49 | if spot 50 | assert_kind_of(Integer, spot[:first_lineno]) 51 | assert_kind_of(Integer, spot[:first_column]) 52 | assert_kind_of(Integer, spot[:last_lineno]) 53 | assert_kind_of(Integer, spot[:last_column]) 54 | assert_kind_of(String, spot[:snippet]) 55 | assert_kind_of(Array, spot[:script_lines]) 56 | end 57 | end 58 | assert_equal(preprocess(expected_msg).chomp, err.detailed_message(highlight: false).sub(/ \((?:NoMethod|Name)Error\)/, "")) 59 | end 60 | else 61 | def assert_error_message(klass, expected_msg, &blk) 62 | omit unless klass < ErrorHighlight::CoreExt 63 | err = assert_raise(klass, &blk) 64 | assert_equal(preprocess(expected_msg).chomp, err.message) 65 | end 66 | end 67 | 68 | if begin; 1.time; rescue; $!.message.end_with?("an instance of Integer"); end 69 | # new message format 70 | NEW_MESSAGE_FORMAT = true 71 | NIL_RECV_MESSAGE = "nil" 72 | ONE_RECV_MESSAGE = "an instance of Integer" 73 | else 74 | NEW_MESSAGE_FORMAT = false 75 | NIL_RECV_MESSAGE = "nil:NilClass" 76 | ONE_RECV_MESSAGE = "1:Integer" 77 | end 78 | 79 | def test_CALL_noarg_1 80 | assert_error_message(NoMethodError, <<~END) do 81 | undefined method `foo' for #{ NIL_RECV_MESSAGE } 82 | 83 | nil.foo + 1 84 | ^^^^ 85 | END 86 | 87 | nil.foo + 1 88 | end 89 | end 90 | 91 | def test_CALL_noarg_2 92 | assert_error_message(NoMethodError, <<~END) do 93 | undefined method `foo' for #{ NIL_RECV_MESSAGE } 94 | 95 | .foo + 1 96 | ^^^^ 97 | END 98 | 99 | nil 100 | .foo + 1 101 | end 102 | end 103 | 104 | def test_CALL_noarg_3 105 | assert_error_message(NoMethodError, <<~END) do 106 | undefined method `foo' for #{ NIL_RECV_MESSAGE } 107 | 108 | foo + 1 109 | ^^^ 110 | END 111 | 112 | nil. 113 | foo + 1 114 | end 115 | end 116 | 117 | def test_CALL_noarg_4 118 | assert_error_message(NoMethodError, <<~END) do 119 | undefined method `foo' for #{ NIL_RECV_MESSAGE } 120 | 121 | (nil).foo + 1 122 | ^^^^ 123 | END 124 | 125 | (nil).foo + 1 126 | end 127 | end 128 | 129 | def test_CALL_arg_1 130 | assert_error_message(NoMethodError, <<~END) do 131 | undefined method `foo' for #{ NIL_RECV_MESSAGE } 132 | 133 | nil.foo (42) 134 | ^^^^ 135 | END 136 | 137 | nil.foo (42) 138 | end 139 | end 140 | 141 | def test_CALL_arg_2 142 | assert_error_message(NoMethodError, <<~END) do 143 | undefined method `foo' for #{ NIL_RECV_MESSAGE } 144 | 145 | .foo ( 146 | ^^^^ 147 | END 148 | 149 | nil 150 | .foo ( 151 | 42 152 | ) 153 | end 154 | end 155 | 156 | def test_CALL_arg_3 157 | assert_error_message(NoMethodError, <<~END) do 158 | undefined method `foo' for #{ NIL_RECV_MESSAGE } 159 | 160 | foo ( 161 | ^^^ 162 | END 163 | 164 | nil. 165 | foo ( 166 | 42 167 | ) 168 | end 169 | end 170 | 171 | def test_CALL_arg_4 172 | assert_error_message(NoMethodError, <<~END) do 173 | undefined method `foo' for #{ NIL_RECV_MESSAGE } 174 | 175 | nil.foo(42) 176 | ^^^^ 177 | END 178 | 179 | nil.foo(42) 180 | end 181 | end 182 | 183 | def test_CALL_arg_5 184 | assert_error_message(NoMethodError, <<~END) do 185 | undefined method `foo' for #{ NIL_RECV_MESSAGE } 186 | 187 | .foo( 188 | ^^^^ 189 | END 190 | 191 | nil 192 | .foo( 193 | 42 194 | ) 195 | end 196 | end 197 | 198 | def test_CALL_arg_6 199 | assert_error_message(NoMethodError, <<~END) do 200 | undefined method `foo' for #{ NIL_RECV_MESSAGE } 201 | 202 | foo( 203 | ^^^ 204 | END 205 | 206 | nil. 207 | foo( 208 | 42 209 | ) 210 | end 211 | end 212 | 213 | def test_CALL_arg_7 214 | assert_error_message(ArgumentError, <<~END) do 215 | tried to create Proc object without a block (ArgumentError) 216 | END 217 | 218 | Proc.new 219 | end 220 | end 221 | 222 | def test_QCALL_1 223 | assert_error_message(NoMethodError, <<~END) do 224 | undefined method `foo' for #{ ONE_RECV_MESSAGE } 225 | 226 | 1&.foo 227 | ^^^^^ 228 | END 229 | 230 | 1&.foo 231 | end 232 | end 233 | 234 | def test_QCALL_2 235 | assert_error_message(NoMethodError, <<~END) do 236 | undefined method `foo' for #{ ONE_RECV_MESSAGE } 237 | 238 | 1&.foo(42) 239 | ^^^^^ 240 | END 241 | 242 | 1&.foo(42) 243 | end 244 | end 245 | 246 | def test_CALL_aref_1 247 | assert_error_message(NoMethodError, <<~END) do 248 | undefined method `[]' for #{ NIL_RECV_MESSAGE } 249 | 250 | nil [ ] 251 | ^^^ 252 | END 253 | 254 | nil [ ] 255 | end 256 | end 257 | 258 | def test_CALL_aref_2 259 | assert_error_message(NoMethodError, <<~END) do 260 | undefined method `[]' for #{ NIL_RECV_MESSAGE } 261 | 262 | nil [0] 263 | ^^^ 264 | END 265 | 266 | nil [0] 267 | end 268 | end 269 | 270 | def test_CALL_aref_3 271 | assert_error_message(NoMethodError, <<~END) do 272 | undefined method `[]' for #{ NIL_RECV_MESSAGE } 273 | END 274 | 275 | nil [ 276 | 0 277 | ] 278 | end 279 | end 280 | 281 | def test_CALL_aref_4 282 | v = Object.new 283 | recv = NEW_MESSAGE_FORMAT ? "an instance of Object" : v.inspect 284 | assert_error_message(NoMethodError, <<~END) do 285 | undefined method `[]' for #{ recv } 286 | 287 | v &.[](0) 288 | ^^^^ 289 | END 290 | 291 | v &.[](0) 292 | end 293 | end 294 | 295 | def test_CALL_aref_5 296 | assert_error_message(NoMethodError, <<~END) do 297 | undefined method `[]' for #{ NIL_RECV_MESSAGE } 298 | 299 | (nil)[ ] 300 | ^^^ 301 | END 302 | 303 | (nil)[ ] 304 | end 305 | end 306 | 307 | def test_CALL_aset 308 | assert_error_message(NoMethodError, <<~END) do 309 | undefined method `[]=' for #{ NIL_RECV_MESSAGE } 310 | 311 | nil.[]= 312 | ^^^^ 313 | END 314 | 315 | nil.[]= 316 | end 317 | end 318 | 319 | def test_CALL_op_asgn 320 | v = nil 321 | assert_error_message(NoMethodError, <<~END) do 322 | undefined method `+' for #{ NIL_RECV_MESSAGE } 323 | 324 | v += 42 325 | ^ 326 | END 327 | 328 | v += 42 329 | end 330 | end 331 | 332 | def test_CALL_special_call_1 333 | assert_error_message(NoMethodError, <<~END) do 334 | undefined method `call' for #{ NIL_RECV_MESSAGE } 335 | END 336 | 337 | nil.() 338 | end 339 | end 340 | 341 | def test_CALL_special_call_2 342 | assert_error_message(NoMethodError, <<~END) do 343 | undefined method `call' for #{ NIL_RECV_MESSAGE } 344 | END 345 | 346 | nil.(42) 347 | end 348 | end 349 | 350 | def test_CALL_send 351 | assert_error_message(NoMethodError, <<~END) do 352 | undefined method `foo' for #{ NIL_RECV_MESSAGE } 353 | 354 | nil.send(:foo, 42) 355 | ^^^^^ 356 | END 357 | 358 | nil.send(:foo, 42) 359 | end 360 | end 361 | 362 | def test_ATTRASGN_1 363 | assert_error_message(NoMethodError, <<~END) do 364 | undefined method `[]=' for #{ NIL_RECV_MESSAGE } 365 | 366 | nil [ ] = 42 367 | ^^^^^ 368 | END 369 | 370 | nil [ ] = 42 371 | end 372 | end 373 | 374 | def test_ATTRASGN_2 375 | assert_error_message(NoMethodError, <<~END) do 376 | undefined method `[]=' for #{ NIL_RECV_MESSAGE } 377 | 378 | nil [0] = 42 379 | ^^^^^ 380 | END 381 | 382 | nil [0] = 42 383 | end 384 | end 385 | 386 | def test_ATTRASGN_3 387 | assert_error_message(NoMethodError, <<~END) do 388 | undefined method `foo=' for #{ NIL_RECV_MESSAGE } 389 | 390 | nil.foo = 42 391 | ^^^^^^ 392 | END 393 | 394 | nil.foo = 42 395 | end 396 | end 397 | 398 | def test_ATTRASGN_4 399 | assert_error_message(NoMethodError, <<~END) do 400 | undefined method `[]=' for #{ NIL_RECV_MESSAGE } 401 | 402 | (nil)[0] = 42 403 | ^^^^^ 404 | END 405 | 406 | (nil)[0] = 42 407 | end 408 | end 409 | 410 | def test_ATTRASGN_5 411 | assert_error_message(NoMethodError, <<~END) do 412 | undefined method `foo=' for #{ NIL_RECV_MESSAGE } 413 | 414 | (nil).foo = 42 415 | ^^^^^^ 416 | END 417 | 418 | (nil).foo = 42 419 | end 420 | end 421 | 422 | def test_OPCALL_binary_1 423 | assert_error_message(NoMethodError, <<~END) do 424 | undefined method `+' for #{ NIL_RECV_MESSAGE } 425 | 426 | nil + 42 427 | ^ 428 | END 429 | 430 | nil + 42 431 | end 432 | end 433 | 434 | def test_OPCALL_binary_2 435 | assert_error_message(NoMethodError, <<~END) do 436 | undefined method `+' for #{ NIL_RECV_MESSAGE } 437 | 438 | nil + # comment 439 | ^ 440 | END 441 | 442 | nil + # comment 443 | 42 444 | end 445 | end 446 | 447 | def test_OPCALL_binary_3 448 | assert_error_message(NoMethodError, <<~END) do 449 | undefined method `+' for #{ NIL_RECV_MESSAGE } 450 | 451 | (nil) + 42 452 | ^ 453 | END 454 | 455 | (nil) + 42 456 | end 457 | end 458 | 459 | def test_OPCALL_unary_1 460 | assert_error_message(NoMethodError, <<~END) do 461 | undefined method `+@' for #{ NIL_RECV_MESSAGE } 462 | 463 | + nil 464 | ^ 465 | END 466 | 467 | + nil 468 | end 469 | end 470 | 471 | def test_OPCALL_unary_2 472 | assert_error_message(NoMethodError, <<~END) do 473 | undefined method `+@' for #{ NIL_RECV_MESSAGE } 474 | 475 | +(nil) 476 | ^ 477 | END 478 | 479 | +(nil) 480 | end 481 | end 482 | 483 | def test_FCALL_1 484 | assert_error_message(NoMethodError, <<~END) do 485 | undefined method `foo' for #{ NIL_RECV_MESSAGE } 486 | 487 | nil.instance_eval { foo() } 488 | ^^^ 489 | END 490 | 491 | nil.instance_eval { foo() } 492 | end 493 | end 494 | 495 | def test_FCALL_2 496 | assert_error_message(NoMethodError, <<~END) do 497 | undefined method `foo' for #{ NIL_RECV_MESSAGE } 498 | 499 | nil.instance_eval { foo(42) } 500 | ^^^ 501 | END 502 | 503 | nil.instance_eval { foo(42) } 504 | end 505 | end 506 | 507 | def test_VCALL_2 508 | assert_error_message(NameError, <<~END) do 509 | undefined local variable or method `foo' for #{ NIL_RECV_MESSAGE } 510 | 511 | nil.instance_eval { foo } 512 | ^^^ 513 | END 514 | 515 | nil.instance_eval { foo } 516 | end 517 | end 518 | 519 | def test_OP_ASGN1_aref_1 520 | v = nil 521 | 522 | assert_error_message(NoMethodError, <<~END) do 523 | undefined method `[]' for #{ NIL_RECV_MESSAGE } 524 | 525 | v [0] += 42 526 | ^^^ 527 | END 528 | 529 | v [0] += 42 530 | end 531 | end 532 | 533 | def test_OP_ASGN1_aref_2 534 | v = nil 535 | 536 | assert_error_message(NoMethodError, <<~END) do 537 | undefined method `[]' for #{ NIL_RECV_MESSAGE } 538 | 539 | v [0] += # comment 540 | ^^^ 541 | END 542 | 543 | v [0] += # comment 544 | 42 545 | end 546 | end 547 | 548 | def test_OP_ASGN1_aref_3 549 | v = nil 550 | 551 | assert_error_message(NoMethodError, <<~END) do 552 | undefined method `[]' for #{ NIL_RECV_MESSAGE } 553 | END 554 | 555 | v [ 556 | 0 557 | ] += # comment 558 | 42 559 | end 560 | end 561 | 562 | def test_OP_ASGN1_aref_4 563 | v = nil 564 | 565 | assert_error_message(NoMethodError, <<~END) do 566 | undefined method `[]' for #{ NIL_RECV_MESSAGE } 567 | 568 | (v)[0] += 42 569 | ^^^ 570 | END 571 | 572 | (v)[0] += 42 573 | end 574 | end 575 | 576 | def test_OP_ASGN1_op_1 577 | v = Object.new 578 | def v.[](x); nil; end 579 | 580 | assert_error_message(NoMethodError, <<~END) do 581 | undefined method `+' for #{ NIL_RECV_MESSAGE } 582 | 583 | v [0] += 42 584 | ^ 585 | END 586 | 587 | v [0] += 42 588 | end 589 | end 590 | 591 | def test_OP_ASGN1_op_2 592 | v = Object.new 593 | def v.[](x); nil; end 594 | 595 | assert_error_message(NoMethodError, <<~END) do 596 | undefined method `+' for #{ NIL_RECV_MESSAGE } 597 | 598 | v [0 ] += # comment 599 | ^ 600 | END 601 | 602 | v [0 ] += # comment 603 | 42 604 | end 605 | end 606 | 607 | def test_OP_ASGN1_op_3 608 | v = Object.new 609 | def v.[](x); nil; end 610 | 611 | assert_error_message(NoMethodError, <<~END) do 612 | undefined method `+' for #{ NIL_RECV_MESSAGE } 613 | END 614 | 615 | v [ 616 | 0 617 | ] += 618 | 42 619 | end 620 | end 621 | 622 | def test_OP_ASGN1_op_4 623 | v = Object.new 624 | def v.[](x); nil; end 625 | 626 | assert_error_message(NoMethodError, <<~END) do 627 | undefined method `+' for #{ NIL_RECV_MESSAGE } 628 | 629 | (v)[0] += 42 630 | ^ 631 | END 632 | 633 | (v)[0] += 42 634 | end 635 | end 636 | 637 | def test_OP_ASGN1_aset_1 638 | v = Object.new 639 | def v.[](x); 1; end 640 | 641 | assert_error_message(NoMethodError, <<~END) do 642 | undefined method `[]=' for #{ v.inspect } 643 | 644 | v [0] += 42 645 | ^^^^^^ 646 | END 647 | 648 | v [0] += 42 649 | end 650 | end 651 | 652 | def test_OP_ASGN1_aset_2 653 | v = Object.new 654 | def v.[](x); 1; end 655 | 656 | assert_error_message(NoMethodError, <<~END) do 657 | undefined method `[]=' for #{ v.inspect } 658 | 659 | v [0] += # comment 660 | ^^^^^^ 661 | END 662 | 663 | v [0] += # comment 664 | 42 665 | end 666 | end 667 | 668 | def test_OP_ASGN1_aset_3 669 | v = Object.new 670 | def v.[](x); 1; end 671 | 672 | assert_error_message(NoMethodError, <<~END) do 673 | undefined method `[]=' for #{ v.inspect } 674 | END 675 | 676 | v [ 677 | 0 678 | ] += 679 | 42 680 | end 681 | end 682 | 683 | def test_OP_ASGN1_aset_4 684 | v = Object.new 685 | def v.[](x); 1; end 686 | 687 | assert_error_message(NoMethodError, <<~END) do 688 | undefined method `[]=' for #{ v.inspect } 689 | 690 | (v)[0] += 42 691 | ^^^^^^ 692 | END 693 | 694 | (v)[0] += 42 695 | end 696 | end 697 | 698 | def test_OP_ASGN2_read_1 699 | v = nil 700 | 701 | assert_error_message(NoMethodError, <<~END) do 702 | undefined method `foo' for #{ NIL_RECV_MESSAGE } 703 | 704 | v.foo += 42 705 | ^^^^ 706 | END 707 | 708 | v.foo += 42 709 | end 710 | end 711 | 712 | def test_OP_ASGN2_read_2 713 | v = nil 714 | 715 | assert_error_message(NoMethodError, <<~END) do 716 | undefined method `foo' for #{ NIL_RECV_MESSAGE } 717 | 718 | v.foo += # comment 719 | ^^^^ 720 | END 721 | 722 | v.foo += # comment 723 | 42 724 | end 725 | end 726 | 727 | def test_OP_ASGN2_read_3 728 | v = nil 729 | 730 | assert_error_message(NoMethodError, <<~END) do 731 | undefined method `foo' for #{ NIL_RECV_MESSAGE } 732 | 733 | (v).foo += 42 734 | ^^^^ 735 | END 736 | 737 | (v).foo += 42 738 | end 739 | end 740 | 741 | def test_OP_ASGN2_op_1 742 | v = Object.new 743 | def v.foo; nil; end 744 | 745 | assert_error_message(NoMethodError, <<~END) do 746 | undefined method `+' for #{ NIL_RECV_MESSAGE } 747 | 748 | v.foo += 42 749 | ^ 750 | END 751 | 752 | v.foo += 42 753 | end 754 | end 755 | 756 | def test_OP_ASGN2_op_2 757 | v = Object.new 758 | def v.foo; nil; end 759 | 760 | assert_error_message(NoMethodError, <<~END) do 761 | undefined method `+' for #{ NIL_RECV_MESSAGE } 762 | 763 | v.foo += # comment 764 | ^ 765 | END 766 | 767 | v.foo += # comment 768 | 42 769 | end 770 | end 771 | 772 | def test_OP_ASGN2_op_3 773 | v = Object.new 774 | def v.foo; nil; end 775 | 776 | assert_error_message(NoMethodError, <<~END) do 777 | undefined method `+' for #{ NIL_RECV_MESSAGE } 778 | 779 | (v).foo += 42 780 | ^ 781 | END 782 | 783 | (v).foo += 42 784 | end 785 | end 786 | 787 | def test_OP_ASGN2_write_1 788 | v = Object.new 789 | def v.foo; 1; end 790 | 791 | assert_error_message(NoMethodError, <<~END) do 792 | undefined method `foo=' for #{ v.inspect } 793 | 794 | v.foo += 42 795 | ^^^^^^^ 796 | END 797 | 798 | v.foo += 42 799 | end 800 | end 801 | 802 | def test_OP_ASGN2_write_2 803 | v = Object.new 804 | def v.foo; 1; end 805 | 806 | assert_error_message(NoMethodError, <<~END) do 807 | undefined method `foo=' for #{ v.inspect } 808 | 809 | v.foo += # comment 810 | ^^^^^^^ 811 | END 812 | 813 | v.foo += # comment 814 | 42 815 | end 816 | end 817 | 818 | def test_OP_ASGN2_write_3 819 | v = Object.new 820 | def v.foo; 1; end 821 | 822 | assert_error_message(NoMethodError, <<~END) do 823 | undefined method `foo=' for #{ v.inspect } 824 | 825 | (v).foo += 42 826 | ^^^^^^^ 827 | END 828 | 829 | (v).foo += 42 830 | end 831 | end 832 | 833 | def test_CONST 834 | assert_error_message(NameError, <<~END) do 835 | uninitialized constant ErrorHighlightTest::NotDefined 836 | 837 | 1 + NotDefined + 1 838 | ^^^^^^^^^^ 839 | END 840 | 841 | 1 + NotDefined + 1 842 | end 843 | end 844 | 845 | def test_COLON2_1 846 | assert_error_message(NameError, <<~END) do 847 | uninitialized constant ErrorHighlightTest::NotDefined 848 | 849 | ErrorHighlightTest::NotDefined 850 | ^^^^^^^^^^^^ 851 | END 852 | 853 | ErrorHighlightTest::NotDefined 854 | end 855 | end 856 | 857 | def test_COLON2_2 858 | assert_error_message(NameError, <<~END) do 859 | uninitialized constant ErrorHighlightTest::NotDefined 860 | 861 | NotDefined 862 | ^^^^^^^^^^ 863 | END 864 | 865 | ErrorHighlightTest:: 866 | NotDefined 867 | end 868 | end 869 | 870 | def test_COLON2_3 871 | assert_error_message(NameError, <<~END) do 872 | uninitialized constant ErrorHighlightTest::NotDefined 873 | 874 | ErrorHighlightTest::NotDefined::Foo 875 | ^^^^^^^^^^^^ 876 | END 877 | 878 | ErrorHighlightTest::NotDefined::Foo 879 | end 880 | end 881 | 882 | def test_COLON2_4 883 | assert_error_message(NameError, <<~END) do 884 | uninitialized constant ErrorHighlightTest::NotDefined 885 | 886 | ::ErrorHighlightTest::NotDefined::Foo 887 | ^^^^^^^^^^^^ 888 | END 889 | 890 | ::ErrorHighlightTest::NotDefined::Foo 891 | end 892 | end 893 | 894 | def test_COLON2_5 895 | # Unfortunately, we cannot identify which `NotDefined` caused the NameError 896 | assert_error_message(NameError, <<~END) do 897 | uninitialized constant ErrorHighlightTest::NotDefined 898 | END 899 | 900 | ErrorHighlightTest::NotDefined::NotDefined 901 | end 902 | end 903 | 904 | def test_COLON3 905 | assert_error_message(NameError, <<~END) do 906 | uninitialized constant NotDefined 907 | 908 | ::NotDefined 909 | ^^^^^^^^^^^^ 910 | END 911 | 912 | ::NotDefined 913 | end 914 | end 915 | 916 | module OP_CDECL_TEST 917 | Nil = nil 918 | end 919 | 920 | def test_OP_CDECL_read_1 921 | assert_error_message(NameError, <<~END) do 922 | uninitialized constant ErrorHighlightTest::OP_CDECL_TEST::NotDefined 923 | 924 | OP_CDECL_TEST::NotDefined += 1 925 | ^^^^^^^^^^^^ 926 | END 927 | 928 | OP_CDECL_TEST::NotDefined += 1 929 | end 930 | end 931 | 932 | def test_OP_CDECL_read_2 933 | assert_error_message(NameError, <<~END) do 934 | uninitialized constant ErrorHighlightTest::OP_CDECL_TEST::NotDefined 935 | 936 | OP_CDECL_TEST::NotDefined += # comment 937 | ^^^^^^^^^^^^ 938 | END 939 | 940 | OP_CDECL_TEST::NotDefined += # comment 941 | 1 942 | end 943 | end 944 | 945 | def test_OP_CDECL_read_3 946 | assert_error_message(NameError, <<~END) do 947 | uninitialized constant ErrorHighlightTest::OP_CDECL_TEST::NotDefined 948 | END 949 | 950 | OP_CDECL_TEST:: 951 | NotDefined += 1 952 | end 953 | end 954 | 955 | def test_OP_CDECL_op_1 956 | assert_error_message(NoMethodError, <<~END) do 957 | undefined method `+' for #{ NIL_RECV_MESSAGE } 958 | 959 | OP_CDECL_TEST::Nil += 1 960 | ^ 961 | END 962 | 963 | OP_CDECL_TEST::Nil += 1 964 | end 965 | end 966 | 967 | def test_OP_CDECL_op_2 968 | assert_error_message(NoMethodError, <<~END) do 969 | undefined method `+' for #{ NIL_RECV_MESSAGE } 970 | 971 | OP_CDECL_TEST::Nil += # comment 972 | ^ 973 | END 974 | 975 | OP_CDECL_TEST::Nil += # comment 976 | 1 977 | end 978 | end 979 | 980 | def test_OP_CDECL_op_3 981 | assert_error_message(NoMethodError, <<~END) do 982 | undefined method `+' for #{ NIL_RECV_MESSAGE } 983 | 984 | Nil += 1 985 | ^ 986 | END 987 | 988 | OP_CDECL_TEST:: 989 | Nil += 1 990 | end 991 | end 992 | 993 | def test_OP_CDECL_toplevel_1 994 | assert_error_message(NameError, <<~END) do 995 | uninitialized constant NotDefined 996 | 997 | ::NotDefined += 1 998 | ^^^^^^^^^^^^ 999 | END 1000 | 1001 | ::NotDefined += 1 1002 | end 1003 | end 1004 | 1005 | def test_OP_CDECL_toplevel_2 1006 | recv = NEW_MESSAGE_FORMAT ? "class ErrorHighlightTest" : "ErrorHighlightTest:Class" 1007 | assert_error_message(NoMethodError, <<~END) do 1008 | undefined method `+' for #{ recv } 1009 | 1010 | ::ErrorHighlightTest += 1 1011 | ^ 1012 | END 1013 | 1014 | ::ErrorHighlightTest += 1 1015 | end 1016 | end 1017 | 1018 | def test_explicit_raise_name_error 1019 | assert_error_message(NameError, <<~END) do 1020 | NameError 1021 | 1022 | raise NameError 1023 | ^^^^^ 1024 | END 1025 | 1026 | raise NameError 1027 | end 1028 | end 1029 | 1030 | def test_explicit_raise_no_method_error 1031 | assert_error_message(NoMethodError, <<~END) do 1032 | NoMethodError 1033 | 1034 | raise NoMethodError 1035 | ^^^^^ 1036 | END 1037 | 1038 | raise NoMethodError 1039 | end 1040 | end 1041 | 1042 | def test_const_get 1043 | assert_error_message(NameError, <<~END) do 1044 | uninitialized constant ErrorHighlightTest::NotDefined 1045 | 1046 | ErrorHighlightTest.const_get(:NotDefined) 1047 | ^^^^^^^^^^ 1048 | END 1049 | 1050 | ErrorHighlightTest.const_get(:NotDefined) 1051 | end 1052 | end 1053 | 1054 | def test_local_variable_get 1055 | b = binding 1056 | assert_error_message(NameError, <<~END) do 1057 | local variable `foo' is not defined for #{ b.inspect } 1058 | 1059 | b.local_variable_get(:foo) 1060 | ^^^^^^^^^^^^^^^^^^^ 1061 | END 1062 | 1063 | b.local_variable_get(:foo) 1064 | end 1065 | end 1066 | 1067 | def test_multibyte 1068 | assert_error_message(NoMethodError, <<~END) do 1069 | undefined method `あいうえお' for #{ NIL_RECV_MESSAGE } 1070 | END 1071 | 1072 | nil.あいうえお 1073 | end 1074 | end 1075 | 1076 | def test_args_CALL_1 1077 | assert_error_message(TypeError, <<~END) do 1078 | nil can't be coerced into Integer (TypeError) 1079 | 1080 | 1.+(nil) 1081 | ^^^ 1082 | END 1083 | 1084 | 1.+(nil) 1085 | end 1086 | end 1087 | 1088 | def test_args_CALL_2 1089 | v = [] 1090 | assert_error_message(TypeError, <<~END) do 1091 | no implicit conversion from nil to integer (TypeError) 1092 | 1093 | v[nil] 1094 | ^^^ 1095 | END 1096 | 1097 | v[nil] 1098 | end 1099 | end 1100 | 1101 | def test_args_ATTRASGN_1 1102 | v = method(:raise).to_proc 1103 | recv = NEW_MESSAGE_FORMAT ? "an instance of Proc" : v.inspect 1104 | assert_error_message(NoMethodError, <<~END) do 1105 | undefined method `[]=' for #{ recv } 1106 | 1107 | v [ ] = 1 1108 | ^^^^^ 1109 | END 1110 | 1111 | v [ ] = 1 1112 | end 1113 | end 1114 | 1115 | def test_args_ATTRASGN_2 1116 | v = [] 1117 | assert_error_message(TypeError, <<~END) do 1118 | no implicit conversion from nil to integer (TypeError) 1119 | 1120 | v [nil] = 1 1121 | ^^^^^^^^ 1122 | END 1123 | 1124 | v [nil] = 1 1125 | end 1126 | end 1127 | 1128 | def test_args_ATTRASGN_3 1129 | assert_error_message(TypeError, <<~END) do 1130 | no implicit conversion of String into Integer (TypeError) 1131 | 1132 | $stdin.lineno = "str" 1133 | ^^^^^ 1134 | END 1135 | 1136 | $stdin.lineno = "str" 1137 | end 1138 | end 1139 | 1140 | def test_args_OPCALL 1141 | assert_error_message(TypeError, <<~END) do 1142 | nil can't be coerced into Integer (TypeError) 1143 | 1144 | 1 + nil 1145 | ^^^ 1146 | END 1147 | 1148 | 1 + nil 1149 | end 1150 | end 1151 | 1152 | def test_args_FCALL_1 1153 | assert_error_message(TypeError, <<~END) do 1154 | no implicit conversion of Symbol into String (TypeError) 1155 | 1156 | "str".instance_eval { gsub("foo", :sym) } 1157 | ^^^^^^^^^^^ 1158 | END 1159 | 1160 | "str".instance_eval { gsub("foo", :sym) } 1161 | end 1162 | end 1163 | 1164 | def test_args_FCALL_2 1165 | assert_error_message(TypeError, <<~END) do 1166 | no implicit conversion of Symbol into String (TypeError) 1167 | 1168 | "str".instance_eval { gsub "foo", :sym } 1169 | ^^^^^^^^^^^ 1170 | END 1171 | 1172 | "str".instance_eval { gsub "foo", :sym } 1173 | end 1174 | end 1175 | 1176 | def test_args_OP_ASGN1_aref_1 1177 | v = [] 1178 | 1179 | assert_error_message(TypeError, <<~END) do 1180 | no implicit conversion from nil to integer (TypeError) 1181 | 1182 | v [nil] += 42 1183 | ^^^^^^^^^^ 1184 | END 1185 | 1186 | v [nil] += 42 1187 | end 1188 | end 1189 | 1190 | def test_args_OP_ASGN1_aref_2 1191 | v = method(:raise).to_proc 1192 | 1193 | assert_error_message(ArgumentError, <<~END) do 1194 | ArgumentError (ArgumentError) 1195 | 1196 | v [ArgumentError] += 42 1197 | ^^^^^^^^^^^^^^^^^^^^ 1198 | END 1199 | 1200 | v [ArgumentError] += 42 1201 | end 1202 | end 1203 | 1204 | def test_args_OP_ASGN1_op 1205 | v = [1] 1206 | 1207 | assert_error_message(TypeError, <<~END) do 1208 | nil can't be coerced into Integer (TypeError) 1209 | 1210 | v [0] += nil 1211 | ^^^^^^^^^ 1212 | END 1213 | 1214 | v [0] += nil 1215 | end 1216 | end 1217 | 1218 | def test_args_OP_ASGN2 1219 | v = Object.new 1220 | def v.foo; 1; end 1221 | 1222 | assert_error_message(TypeError, <<~END) do 1223 | nil can't be coerced into Integer (TypeError) 1224 | 1225 | v.foo += nil 1226 | ^^^ 1227 | END 1228 | 1229 | v.foo += nil 1230 | end 1231 | end 1232 | 1233 | def test_custom_formatter 1234 | custom_formatter = Object.new 1235 | def custom_formatter.message_for(spot) 1236 | "\n\n" + spot.except(:script_lines).inspect 1237 | end 1238 | 1239 | original_formatter, ErrorHighlight.formatter = ErrorHighlight.formatter, custom_formatter 1240 | 1241 | assert_error_message(NoMethodError, <<~END) do 1242 | undefined method `time' for #{ ONE_RECV_MESSAGE } 1243 | 1244 | #{{ first_lineno: __LINE__ + 3, first_column: 7, last_lineno: __LINE__ + 3, last_column: 12, snippet: " 1.time {}\n" }.inspect} 1245 | END 1246 | 1247 | 1.time {} 1248 | end 1249 | 1250 | ensure 1251 | ErrorHighlight.formatter = original_formatter 1252 | end 1253 | 1254 | def test_hard_tabs 1255 | Tempfile.create(["error_highlight_test", ".rb"], binmode: true) do |tmp| 1256 | tmp << "\t \t1.time {}\n" 1257 | tmp.close 1258 | 1259 | assert_error_message(NoMethodError, <<~END.gsub("_", "\t")) do 1260 | undefined method `time' for #{ ONE_RECV_MESSAGE } 1261 | 1262 | _ _1.time {} 1263 | _ _ ^^^^^ 1264 | END 1265 | 1266 | load tmp.path 1267 | end 1268 | end 1269 | end 1270 | 1271 | def test_no_final_newline 1272 | Tempfile.create(["error_highlight_test", ".rb"], binmode: true) do |tmp| 1273 | tmp << "1.time {}" 1274 | tmp.close 1275 | 1276 | assert_error_message(NoMethodError, <<~END) do 1277 | undefined method `time' for #{ ONE_RECV_MESSAGE } 1278 | 1279 | 1.time {} 1280 | ^^^^^ 1281 | END 1282 | 1283 | load tmp.path 1284 | end 1285 | end 1286 | end 1287 | 1288 | def test_errors_on_small_terminal_window_at_the_end 1289 | assert_error_message(NoMethodError, <<~END) do 1290 | undefined method `time' for #{ ONE_RECV_MESSAGE } 1291 | 1292 | ...0000000000000000000000000000000000000000000000000000000000000000 + 1.time {} 1293 | ^^^^^ 1294 | END 1295 | 1296 | 100000000000000000000000000000000000000000000000000000000000000000000000000000 + 1.time {} 1297 | end 1298 | end 1299 | 1300 | def test_errors_on_small_terminal_window_at_the_beginning 1301 | assert_error_message(NoMethodError, <<~END) do 1302 | undefined method `time' for #{ ONE_RECV_MESSAGE } 1303 | 1304 | 1.time { 10000000000000000000000000000000000000000000000000000000000000... 1305 | ^^^^^ 1306 | END 1307 | 1308 | 1.time { 100000000000000000000000000000000000000000000000000000000000000000000000000000 } 1309 | 1310 | end 1311 | end 1312 | 1313 | def test_errors_on_small_terminal_window_at_the_middle_near_beginning 1314 | assert_error_message(NoMethodError, <<~END) do 1315 | undefined method `time' for #{ ONE_RECV_MESSAGE } 1316 | 1317 | 100000000000000000000000000000000000000 + 1.time { 1000000000000000000000... 1318 | ^^^^^ 1319 | END 1320 | 1321 | 100000000000000000000000000000000000000 + 1.time { 100000000000000000000000000000000000000 } 1322 | end 1323 | end 1324 | 1325 | def test_errors_on_small_terminal_window_at_the_middle 1326 | assert_error_message(NoMethodError, <<~END) do 1327 | undefined method `time' for #{ ONE_RECV_MESSAGE } 1328 | 1329 | ...000000000000000000000000000000000 + 1.time { 10000000000000000000000000000... 1330 | ^^^^^ 1331 | END 1332 | 1333 | 10000000000000000000000000000000000000000000000000000000000000000000000 + 1.time { 1000000000000000000000000000000 } 1334 | end 1335 | end 1336 | 1337 | def test_errors_on_extremely_small_terminal_window 1338 | custom_max_width = 30 1339 | original_max_width = ErrorHighlight::DefaultFormatter.max_snippet_width 1340 | 1341 | ErrorHighlight::DefaultFormatter.max_snippet_width = custom_max_width 1342 | 1343 | assert_error_message(NoMethodError, <<~END) do 1344 | undefined method `time' for #{ ONE_RECV_MESSAGE } 1345 | 1346 | ...00000000 + 1.time { 1000... 1347 | ^^^^^ 1348 | END 1349 | 1350 | 100000000000000 + 1.time { 100000000000000 } 1351 | end 1352 | ensure 1353 | ErrorHighlight::DefaultFormatter.max_snippet_width = original_max_width 1354 | end 1355 | 1356 | def test_errors_on_terminal_window_smaller_than_min_width 1357 | custom_max_width = 5 1358 | original_max_width = ErrorHighlight::DefaultFormatter.max_snippet_width 1359 | min_snippet_width = ErrorHighlight::DefaultFormatter::MIN_SNIPPET_WIDTH 1360 | 1361 | warning = nil 1362 | original_warn = Warning.instance_method(:warn) 1363 | Warning.class_eval do 1364 | remove_method(:warn) 1365 | define_method(:warn) {|str| warning = str} 1366 | end 1367 | begin 1368 | ErrorHighlight::DefaultFormatter.max_snippet_width = custom_max_width 1369 | ensure 1370 | Warning.class_eval do 1371 | remove_method(:warn) 1372 | define_method(:warn, original_warn) 1373 | end 1374 | end 1375 | assert_match "'max_snippet_width' adjusted to minimum value of #{min_snippet_width}", warning 1376 | 1377 | assert_error_message(NoMethodError, <<~END) do 1378 | undefined method `time' for #{ ONE_RECV_MESSAGE } 1379 | 1380 | ...000 + 1.time {... 1381 | ^^^^^ 1382 | END 1383 | 1384 | 100000000000000 + 1.time { 100000000000000 } 1385 | end 1386 | ensure 1387 | ErrorHighlight::DefaultFormatter.max_snippet_width = original_max_width 1388 | end 1389 | 1390 | def test_errors_on_terminal_window_when_truncation_is_disabled 1391 | custom_max_width = nil 1392 | original_max_width = ErrorHighlight::DefaultFormatter.max_snippet_width 1393 | 1394 | ErrorHighlight::DefaultFormatter.max_snippet_width = custom_max_width 1395 | 1396 | assert_error_message(NoMethodError, <<~END) do 1397 | undefined method `time' for #{ ONE_RECV_MESSAGE } 1398 | 1399 | 10000000000000000000000000000000000000000000000000000000000000000000000 + 1.time { 1000000000000000000000000000000 } 1400 | ^^^^^ 1401 | END 1402 | 1403 | 10000000000000000000000000000000000000000000000000000000000000000000000 + 1.time { 1000000000000000000000000000000 } 1404 | end 1405 | ensure 1406 | ErrorHighlight::DefaultFormatter.max_snippet_width = original_max_width 1407 | end 1408 | 1409 | def test_errors_on_small_terminal_window_when_larger_than_viewport 1410 | assert_error_message(NoMethodError, <<~END) do 1411 | undefined method `timessssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss!' for #{ ONE_RECV_MESSAGE } 1412 | 1413 | 1.timesssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss... 1414 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 1415 | END 1416 | 1417 | 1.timessssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss! 1418 | end 1419 | end 1420 | 1421 | def test_errors_on_small_terminal_window_when_exact_size_of_viewport 1422 | assert_error_message(NoMethodError, <<~END) do 1423 | undefined method `timessssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss!' for #{ ONE_RECV_MESSAGE } 1424 | 1425 | 1.timessssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss!... 1426 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 1427 | END 1428 | 1429 | 1.timessssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss! * 1000 1430 | end 1431 | end 1432 | 1433 | def test_simulate_funcallv_from_embedded_ruby 1434 | assert_error_message(NoMethodError, <<~END) do 1435 | undefined method `foo' for #{ NIL_RECV_MESSAGE } 1436 | END 1437 | 1438 | nil.foo + 1 1439 | rescue NoMethodError => exc 1440 | def exc.backtrace_locations = [] 1441 | raise 1442 | end 1443 | end 1444 | 1445 | begin 1446 | ->{}.call(1) 1447 | rescue ArgumentError => exc 1448 | MethodDefLocationSupported = 1449 | RubyVM::AbstractSyntaxTree.respond_to?(:node_id_for_backtrace_location) && 1450 | RubyVM::AbstractSyntaxTree.node_id_for_backtrace_location(exc.backtrace_locations.first) 1451 | end 1452 | 1453 | def process_callee_snippet(str) 1454 | return str if MethodDefLocationSupported 1455 | 1456 | str.sub(/\n +\|.*\n +\^+\n\z/, "") 1457 | end 1458 | 1459 | WRONG_NUMBER_OF_ARGUMENTS_LINENO = __LINE__ + 1 1460 | def wrong_number_of_arguments_test(x, y) 1461 | x + y 1462 | end 1463 | 1464 | def test_wrong_number_of_arguments_for_method 1465 | lineno = __LINE__ 1466 | assert_error_message(ArgumentError, process_callee_snippet(<<~END)) do 1467 | wrong number of arguments (given 1, expected 2) (ArgumentError) 1468 | 1469 | caller: #{ __FILE__ }:#{ lineno + 12 } 1470 | | wrong_number_of_arguments_test(1) 1471 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 1472 | callee: #{ __FILE__ }:#{ WRONG_NUMBER_OF_ARGUMENTS_LINENO } 1473 | | def wrong_number_of_arguments_test(x, y) 1474 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 1475 | END 1476 | 1477 | wrong_number_of_arguments_test(1) 1478 | end 1479 | end 1480 | 1481 | KEYWORD_TEST_LINENO = __LINE__ + 1 1482 | def keyword_test(kw1:, kw2:, kw3:) 1483 | kw1 + kw2 + kw3 1484 | end 1485 | 1486 | def test_missing_keyword 1487 | lineno = __LINE__ 1488 | assert_error_message(ArgumentError, process_callee_snippet(<<~END)) do 1489 | missing keyword: :kw3 (ArgumentError) 1490 | 1491 | caller: #{ __FILE__ }:#{ lineno + 12 } 1492 | | keyword_test(kw1: 1, kw2: 2) 1493 | ^^^^^^^^^^^^ 1494 | callee: #{ __FILE__ }:#{ KEYWORD_TEST_LINENO } 1495 | | def keyword_test(kw1:, kw2:, kw3:) 1496 | ^^^^^^^^^^^^ 1497 | END 1498 | 1499 | keyword_test(kw1: 1, kw2: 2) 1500 | end 1501 | end 1502 | 1503 | def test_missing_keywords # multiple missing keywords 1504 | lineno = __LINE__ 1505 | assert_error_message(ArgumentError, process_callee_snippet(<<~END)) do 1506 | missing keywords: :kw2, :kw3 (ArgumentError) 1507 | 1508 | caller: #{ __FILE__ }:#{ lineno + 12 } 1509 | | keyword_test(kw1: 1) 1510 | ^^^^^^^^^^^^ 1511 | callee: #{ __FILE__ }:#{ KEYWORD_TEST_LINENO } 1512 | | def keyword_test(kw1:, kw2:, kw3:) 1513 | ^^^^^^^^^^^^ 1514 | END 1515 | 1516 | keyword_test(kw1: 1) 1517 | end 1518 | end 1519 | 1520 | def test_unknown_keyword 1521 | lineno = __LINE__ 1522 | assert_error_message(ArgumentError, process_callee_snippet(<<~END)) do 1523 | unknown keyword: :kw4 (ArgumentError) 1524 | 1525 | caller: #{ __FILE__ }:#{ lineno + 12 } 1526 | | keyword_test(kw1: 1, kw2: 2, kw3: 3, kw4: 4) 1527 | ^^^^^^^^^^^^ 1528 | callee: #{ __FILE__ }:#{ KEYWORD_TEST_LINENO } 1529 | | def keyword_test(kw1:, kw2:, kw3:) 1530 | ^^^^^^^^^^^^ 1531 | END 1532 | 1533 | keyword_test(kw1: 1, kw2: 2, kw3: 3, kw4: 4) 1534 | end 1535 | end 1536 | 1537 | def test_unknown_keywords 1538 | lineno = __LINE__ 1539 | assert_error_message(ArgumentError, process_callee_snippet(<<~END)) do 1540 | unknown keywords: :kw4, :kw5 (ArgumentError) 1541 | 1542 | caller: #{ __FILE__ }:#{ lineno + 12 } 1543 | | keyword_test(kw1: 1, kw2: 2, kw3: 3, kw4: 4, kw5: 5) 1544 | ^^^^^^^^^^^^ 1545 | callee: #{ __FILE__ }:#{ KEYWORD_TEST_LINENO } 1546 | | def keyword_test(kw1:, kw2:, kw3:) 1547 | ^^^^^^^^^^^^ 1548 | END 1549 | 1550 | keyword_test(kw1: 1, kw2: 2, kw3: 3, kw4: 4, kw5: 5) 1551 | end 1552 | end 1553 | 1554 | WRONG_NUBMER_OF_ARGUMENTS_TEST2_LINENO = __LINE__ + 1 1555 | def wrong_number_of_arguments_test2( 1556 | long_argument_name_x, 1557 | long_argument_name_y, 1558 | long_argument_name_z 1559 | ) 1560 | long_argument_name_x + long_argument_name_y + long_argument_name_z 1561 | end 1562 | 1563 | def test_wrong_number_of_arguments_for_method2 1564 | lineno = __LINE__ 1565 | assert_error_message(ArgumentError, process_callee_snippet(<<~END)) do 1566 | wrong number of arguments (given 1, expected 3) (ArgumentError) 1567 | 1568 | caller: #{ __FILE__ }:#{ lineno + 12 } 1569 | | wrong_number_of_arguments_test2(1) 1570 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 1571 | callee: #{ __FILE__ }:#{ WRONG_NUBMER_OF_ARGUMENTS_TEST2_LINENO } 1572 | | def wrong_number_of_arguments_test2( 1573 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 1574 | END 1575 | 1576 | wrong_number_of_arguments_test2(1) 1577 | end 1578 | end 1579 | 1580 | def test_wrong_number_of_arguments_for_lambda_literal 1581 | v = -> {} 1582 | lineno = __LINE__ 1583 | assert_error_message(ArgumentError, process_callee_snippet(<<~END)) do 1584 | wrong number of arguments (given 1, expected 0) (ArgumentError) 1585 | 1586 | caller: #{ __FILE__ }:#{ lineno + 12 } 1587 | | v.call(1) 1588 | ^^^^^ 1589 | callee: #{ __FILE__ }:#{ lineno - 1 } 1590 | | v = -> {} 1591 | ^^ 1592 | END 1593 | 1594 | v.call(1) 1595 | end 1596 | end 1597 | 1598 | def test_wrong_number_of_arguments_for_lambda_method 1599 | v = lambda { } 1600 | lineno = __LINE__ 1601 | assert_error_message(ArgumentError, process_callee_snippet(<<~END)) do 1602 | wrong number of arguments (given 1, expected 0) (ArgumentError) 1603 | 1604 | caller: #{ __FILE__ }:#{ lineno + 12 } 1605 | | v.call(1) 1606 | ^^^^^ 1607 | callee: #{ __FILE__ }:#{ lineno - 1 } 1608 | | v = lambda { } 1609 | ^ 1610 | END 1611 | 1612 | v.call(1) 1613 | end 1614 | end 1615 | 1616 | DEFINE_METHOD_TEST_LINENO = __LINE__ + 1 1617 | define_method :define_method_test do |x, y| 1618 | x + y 1619 | end 1620 | 1621 | def test_wrong_number_of_arguments_for_define_method 1622 | lineno = __LINE__ 1623 | assert_error_message(ArgumentError, process_callee_snippet(<<~END)) do 1624 | wrong number of arguments (given 1, expected 2) (ArgumentError) 1625 | 1626 | caller: #{ __FILE__ }:#{ lineno + 12 } 1627 | | define_method_test(1) 1628 | ^^^^^^^^^^^^^^^^^^ 1629 | callee: #{ __FILE__ }:#{ DEFINE_METHOD_TEST_LINENO } 1630 | | define_method :define_method_test do |x, y| 1631 | ^^ 1632 | END 1633 | 1634 | define_method_test(1) 1635 | end 1636 | end 1637 | 1638 | def test_spoofed_filename 1639 | Tempfile.create(["error_highlight_test", ".rb"], binmode: true) do |tmp| 1640 | tmp << "module Dummy\nend\n" 1641 | tmp.close 1642 | 1643 | recv = NEW_MESSAGE_FORMAT ? "an instance of String" : '"dummy":String' 1644 | assert_error_message(NameError, <<~END) do 1645 | undefined local variable or method `foo' for #{ recv } 1646 | END 1647 | 1648 | "dummy".instance_eval do 1649 | eval <<-END, nil, tmp.path 1650 | foo 1651 | END 1652 | end 1653 | end 1654 | end 1655 | end 1656 | 1657 | def raise_name_error 1658 | 1.time 1659 | end 1660 | 1661 | def test_spot_with_backtrace_location 1662 | lineno = __LINE__ 1663 | begin 1664 | raise_name_error 1665 | rescue NameError => exc 1666 | end 1667 | 1668 | spot = ErrorHighlight.spot(exc).except(:script_lines) 1669 | assert_equal(lineno - 4, spot[:first_lineno]) 1670 | assert_equal(lineno - 4, spot[:last_lineno]) 1671 | assert_equal(5, spot[:first_column]) 1672 | assert_equal(10, spot[:last_column]) 1673 | assert_equal(" 1.time\n", spot[:snippet]) 1674 | 1675 | spot = ErrorHighlight.spot(exc, backtrace_location: exc.backtrace_locations[1]).except(:script_lines) 1676 | assert_equal(lineno + 2, spot[:first_lineno]) 1677 | assert_equal(lineno + 2, spot[:last_lineno]) 1678 | assert_equal(6, spot[:first_column]) 1679 | assert_equal(22, spot[:last_column]) 1680 | assert_equal(" raise_name_error\n", spot[:snippet]) 1681 | end 1682 | 1683 | def test_spot_with_node 1684 | omit unless RubyVM::AbstractSyntaxTree.respond_to?(:node_id_for_backtrace_location) 1685 | 1686 | # We can't revisit instruction sequences to find node ids if the prism 1687 | # compiler was used instead of the parse.y compiler. In that case, we'll 1688 | # omit some tests. 1689 | omit if RubyVM::InstructionSequence.compile("").to_a[4][:parser] == :prism 1690 | 1691 | begin 1692 | raise_name_error 1693 | rescue NameError => exc 1694 | end 1695 | 1696 | bl = exc.backtrace_locations.first 1697 | expected_spot = ErrorHighlight.spot(exc, backtrace_location: bl) 1698 | ast = RubyVM::AbstractSyntaxTree.parse_file(__FILE__, keep_script_lines: true) 1699 | node_id = RubyVM::AbstractSyntaxTree.node_id_for_backtrace_location(bl) 1700 | node = find_node_by_id(ast, node_id) 1701 | actual_spot = ErrorHighlight.spot(node) 1702 | 1703 | assert_equal expected_spot, actual_spot 1704 | end 1705 | 1706 | module SingletonMethodWithSpacing 1707 | LINENO = __LINE__ + 1 1708 | def self . baz(x:) 1709 | x 1710 | end 1711 | end 1712 | 1713 | def test_singleton_method_with_spacing_missing_keyword 1714 | lineno = __LINE__ 1715 | assert_error_message(ArgumentError, process_callee_snippet(<<~END)) do 1716 | missing keyword: :x (ArgumentError) 1717 | 1718 | caller: #{ __FILE__ }:#{ lineno + 12 } 1719 | | SingletonMethodWithSpacing.baz 1720 | ^^^^ 1721 | callee: #{ __FILE__ }:#{ SingletonMethodWithSpacing::LINENO } 1722 | | def self . baz(x:) 1723 | ^^^^^ 1724 | END 1725 | 1726 | SingletonMethodWithSpacing.baz 1727 | end 1728 | end 1729 | 1730 | module SingletonMethodMultipleKwargs 1731 | LINENO = __LINE__ + 1 1732 | def self.run(shop_id:, param1:) 1733 | shop_id + param1 1734 | end 1735 | end 1736 | 1737 | def test_singleton_method_multiple_missing_keywords 1738 | lineno = __LINE__ 1739 | assert_error_message(ArgumentError, process_callee_snippet(<<~END)) do 1740 | missing keywords: :shop_id, :param1 (ArgumentError) 1741 | 1742 | caller: #{ __FILE__ }:#{ lineno + 12 } 1743 | | SingletonMethodMultipleKwargs.run 1744 | ^^^^ 1745 | callee: #{ __FILE__ }:#{ SingletonMethodMultipleKwargs::LINENO } 1746 | | def self.run(shop_id:, param1:) 1747 | ^^^^ 1748 | END 1749 | 1750 | SingletonMethodMultipleKwargs.run 1751 | end 1752 | end 1753 | 1754 | private 1755 | 1756 | def find_node_by_id(node, node_id) 1757 | return node if node.node_id == node_id 1758 | 1759 | node.children.each do |child| 1760 | next unless child.is_a?(RubyVM::AbstractSyntaxTree::Node) 1761 | found = find_node_by_id(child, node_id) 1762 | return found if found 1763 | end 1764 | 1765 | return false 1766 | end 1767 | end 1768 | --------------------------------------------------------------------------------