repper 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '0889e10cd309dbdd42315e1049c2f7e10a9ab46b949b08c390614965aa2c1483'
4
- data.tar.gz: 8ef2f61c38c6e23abcd933c1e3e462602c19d153824c7632114673b57b291ad5
3
+ metadata.gz: 6493f978abbb5042a52adf386b6222ed571ba0de09b0926e0fb16104a3b60ad4
4
+ data.tar.gz: 52604ae6ebccc88a5608de1b57c7e8e0de85ba55aaf1704a2e066c6ae4fe93b5
5
5
  SHA512:
6
- metadata.gz: a78030f011bb7d74c0263b2e6353114bdd94c9bdd5ee05f36ec7712dd5ad719f23050cea5627fb51cfc8ebe8bdf8b7813ec2094ea163c0253c58996e1fa5997c
7
- data.tar.gz: 28b9f8a9243497e1be1ae2243c81817e2dc566532b314b03b9aa3410a947a9eb50f16b79b9b33a8e76e189c63efbc98b173acf160f814ca9bff2e91deb1d839f
6
+ metadata.gz: 0306346ad34c63bec6558c6ef9516e675e6a8c6b5caa59d92c3ded9fee74687c74cb69e293df474552e8c9204f0c6dfb78d9fd35d03a7b4c9aa8b86edfd2c28c
7
+ data.tar.gz: 2c3ebb13aa9795f4992057da6cb318a2bc16b571ad15c52a4117f27c88e4803526a15a06dd0ce0d62eb1a5cd4133dc6a7ebee6ac03e2cd0e4f611571681b8309
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  ## [Unreleased]
2
2
 
3
- ## [1.0.0] - 2022-04-30
3
+ ...
4
+
5
+ ## [1.1.0] - 2022-05-29
6
+
7
+ ### Added
8
+
9
+ - codemod capability and corresponding `exe/repper` executable
10
+ - new format `Repper::Format::Extended`
11
+ - `Regexp#original_inspect` when using `Regexp#inspect` override
12
+
13
+ ### Fixed
14
+
15
+ - use the more appropriate `:inline` format when passing `format: nil | false`
16
+
17
+ ## [1.0.0] - 2022-05-09
4
18
 
5
19
  - Initial release
data/README.md CHANGED
@@ -1,9 +1,8 @@
1
- <h1>
2
- Repper
3
- <img alt='Rapper necklace with dollar pendant' src='https://user-images.githubusercontent.com/10758879/167485870-5c49284d-a783-453e-8be0-a3597c2ef97c.png' height='46' align='right' />
4
- </h1>
1
+ <img alt='Rapper necklace with dollar pendant' src='https://user-images.githubusercontent.com/10758879/167485870-5c49284d-a783-453e-8be0-a3597c2ef97c.png' height='64' align='right' />
5
2
 
6
- Repper is a regular expression pretty printer for Ruby.
3
+ # Repper
4
+
5
+ Repper is a regular expression pretty printer and formatter for Ruby.
7
6
 
8
7
  ## Installation
9
8
 
@@ -11,20 +10,28 @@ Repper is a regular expression pretty printer for Ruby.
11
10
 
12
11
  ## Usage
13
12
 
14
- `repper` can be integrated into the REPL (e.g. IRB) through core extensions or used manually.
13
+ `repper` can be integrated into the REPL (e.g. IRB) through core extensions for Regexp pretty-printing, integrated into editors to format Regexps, or called manually.
15
14
 
16
15
  There are also a few customization options.
17
16
 
18
- ### Full REPL integration (recommended)
17
+ ### REPL integration
18
+
19
+ #### Via Regexp#inspect (recommended)
19
20
 
20
21
  `require 'repper/core_ext/regexp'` in your `~/.irbrc` or `~/.pryrc` to override `Regexp#inspect` and automatically use `repper` to display Regexps:
21
22
 
22
- <img width="313" alt="screenshot1" src="https://user-images.githubusercontent.com/10758879/167497359-e5bb94db-1382-465b-903a-3e114721b7a6.png">
23
+ <img width="475" alt="screenshot1" src="https://user-images.githubusercontent.com/10758879/167719748-60f4013a-c8d4-4a62-843a-d9f27057bcd3.png">
23
24
 
24
- ### Extending Kernel#pp
25
+ #### Via Kernel#pp
25
26
 
26
27
  Alternatively, `require 'repper/core_ext/kernel'` to make the `pp` command give nicer output for Regexps (which will look like above by default).
27
28
 
29
+ ### Editor integration
30
+
31
+ Use [vscode-repper](https://github.com/jaynetics/vscode-repper) to format Regexps in VSCode.
32
+
33
+ ![vscode-repper](https://user-images.githubusercontent.com/10758879/170892739-e2f408f2-e239-4b13-8d28-c14fb7a9dbb9.gif)
34
+
28
35
  ### Using Repper manually
29
36
 
30
37
  ```ruby
@@ -36,11 +43,13 @@ Repper.render(/foo/) # returns the pretty print String
36
43
 
37
44
  #### Customizing the format
38
45
 
39
- The default format is the annotated, indented format shown above.
40
-
41
- If you want to see the indented structure without annotations, use the `:structured` format.
46
+ Multiple formats are available out of the box:
42
47
 
43
- If you only want colorization you can use the `:inline` format.
48
+ - `:annotated` is the default, verbose format, shown above
49
+ - `:inline` adds only colorization and does not restructure the Regexp
50
+ - `:structured` is like `:annotated`, just without annotations
51
+ - `:x` (or `:extended`) returns a lightly formatted but equivalent Regexp
52
+ - this format is used for the repper executable and [vscode-repper](https://github.com/jaynetics/vscode-repper)
44
53
 
45
54
  You can change the format globally:
46
55
 
@@ -50,14 +59,14 @@ Repper.format = :structured
50
59
 
51
60
  Or pick a format on a case-by-case basis:
52
61
 
53
- <img width="445" alt="screenshot2" src="https://user-images.githubusercontent.com/10758879/167497599-105f39c7-91e0-4954-bce3-d04ad7266695.png">
62
+ <img width="711" alt="screenshot2" src="https://user-images.githubusercontent.com/10758879/167719567-ae8ee42f-839e-4ce4-af56-a139044d3436.png">
54
63
 
55
64
  Or create your own format:
56
65
 
57
66
  ```ruby
58
67
  require 'csv'
59
68
 
60
- csv_format = ->(elements, _theme) { elements.map(&:text).to_csv }
69
+ csv_format = ->(tokens, _theme) { tokens.map(&:text).to_csv }
61
70
  Repper.render(/re[\p{pe}\r]$/, format: csv_format)
62
71
  => "/,re,[,\\p{pe},\\r,],$,/\n"
63
72
  ```
@@ -69,7 +78,8 @@ The color theme can also be set globally or passed on call:
69
78
  ```ruby
70
79
  Repper.theme = :monokai # a nicer theme, if the terminal supports it
71
80
  ```
72
- <img width="316" alt="screenshot3" src="https://user-images.githubusercontent.com/10758879/167497895-0cdc017f-5c77-4b15-afaa-207f7eb887cc.png">
81
+
82
+ <img width="478" alt="screenshot3" src="https://user-images.githubusercontent.com/10758879/167719807-9170ba92-48d1-4669-a05d-a72f962b961d.png">
73
83
 
74
84
  ```ruby
75
85
  Repper.call(/foo/, theme: nil) # render without colors
data/exe/repper ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative "../lib/repper"
4
+
5
+ Repper::Command.call(ARGV)
@@ -0,0 +1,68 @@
1
+ module Repper
2
+ # Formatter for Ruby code containing Regexp literals
3
+ module Codemod
4
+ module_function
5
+
6
+ def call(code)
7
+ formatted_code = code.dup
8
+
9
+ regexp_locations(code).reverse.each do |loc|
10
+ beg_idx = code[/\A(.*\n){#{loc.beg_line}}/].size + loc.beg_char
11
+ end_idx = code[/\A(.*\n){#{loc.end_line}}/].size + loc.end_char
12
+ range = beg_idx..end_idx
13
+
14
+ /\A(?<start>\/|%r.)(?<source>.*)(?<stop>[^a-z])(?<flags>[a-z]*)\z/m =~
15
+ code[range]
16
+
17
+ tokens = Tokenizer.call(source, delimiters: [start, stop], flags: flags)
18
+ formatted_regexp = Format::Extended.call(tokens, Theme::Plain)
19
+
20
+ # indent consistently by applying leading line indentation to all lines
21
+ lead_indentation = code.lines[loc.beg_line][/^ */]
22
+ formatted_regexp.gsub!("\n", "\n#{lead_indentation}")
23
+
24
+ formatted_code[range] = formatted_regexp
25
+ end
26
+
27
+ formatted_code
28
+ end
29
+
30
+ require 'ripper'
31
+
32
+ def regexp_locations(code)
33
+ embed_level = 0
34
+ beg_tokens = {}
35
+
36
+ Ripper.lex(code).each.with_object([]) do |token, acc|
37
+ case token[1]
38
+ when :on_regexp_beg
39
+ beg_tokens[embed_level] = token
40
+ # nested regexp literals are not supported a.t.m., so if we're
41
+ # in an embed, discard the location of the surrounding regexp.
42
+ beg_tokens[embed_level - 1] = nil
43
+ when :on_regexp_end
44
+ next unless beg_token = beg_tokens[embed_level]
45
+
46
+ acc << Location.new(
47
+ beg_line: beg_token[0][0] - 1, # note: using 0-indexed line values
48
+ beg_char: beg_token[0][1],
49
+ end_line: token[0][0] - 1,
50
+ end_char: token[0][1] + (token[2].length - 1), # lex includes flags
51
+ )
52
+ when :on_embexpr_beg # embedded expression a.k.a. interpolation (#{...})
53
+ embed_level += 1
54
+ when :on_embexpr_end
55
+ embed_level -= 1
56
+ when :on_const, :on_ident
57
+ # OK when embedded - Regexp::Parser will treat them as literals
58
+ else
59
+ # other embedded expressions are not supported
60
+ beg_tokens[embed_level - 1] = nil
61
+ end
62
+ end
63
+ end
64
+
65
+ Location = Struct.new(:beg_line, :beg_char, :end_line, :end_char,
66
+ keyword_init: true)
67
+ end
68
+ end
@@ -0,0 +1,39 @@
1
+ module Repper
2
+ # Service object for exe/repper
3
+ module Command
4
+ module_function
5
+
6
+ PARSE_ERROR_EXIT_STATUS = 1
7
+
8
+ def call(argv)
9
+ if argv.count == 0
10
+ format_stdio
11
+ else
12
+ format_files(argv)
13
+ end
14
+ end
15
+
16
+ def format_stdio
17
+ input = STDIN.read
18
+ output = format(input)
19
+ print output
20
+ end
21
+
22
+ def format(string)
23
+ Codemod.call(string)
24
+ rescue Repper::Error => e
25
+ warn "Parsing failed: #{e.class} - #{e.message}"
26
+ exit PARSE_ERROR_EXIT_STATUS
27
+ end
28
+
29
+ def format_files(paths)
30
+ paths.grep(/\.rb\z/).each { |path| format_file(path) }
31
+ end
32
+
33
+ def format_file(path)
34
+ code = File.read(path)
35
+ formatted_code = format(code)
36
+ File.write(path, formatted_code)
37
+ end
38
+ end
39
+ end
@@ -1,4 +1,6 @@
1
1
  require 'repper'
2
2
  require_relative 'regexp_ext'
3
3
 
4
+ Regexp.alias_method :original_inspect, :inspect
5
+
4
6
  ::Regexp.prepend(Repper::RegexpExt)
@@ -1,7 +1,8 @@
1
1
  module Repper
2
2
  module Format
3
- Annotated = ->(elements, theme) do
4
- table = Tabulo::Table.new(elements.reject(&:whitespace?), **TABULO_STYLE)
3
+ # A structured and colorized format with annotations about token types.
4
+ Annotated = ->(tokens, theme) do
5
+ table = Tabulo::Table.new(tokens.reject(&:whitespace?), **TABULO_STYLE)
5
6
  table.add_column(
6
7
  :indented_text,
7
8
  styler: ->(_, string, cell) { theme.colorize(string, cell.source.type) }
@@ -0,0 +1,41 @@
1
+ module Repper
2
+ module Format
3
+ # A lightly structured format that retains parsability
4
+ # and functional equivalence, for use in code.
5
+ Extended = ->(tokens, theme) do
6
+ run_types = %i[escape literal nonposixclass nonproperty
7
+ posixclass property set type]
8
+ forms_run = ->(el){ run_types.include?(el.type) || el.subtype == :dot }
9
+ prev = nil
10
+
11
+ tokens.each.with_object(''.dup) do |tok, acc|
12
+ # drop existing x-mode whitespace, if any
13
+ if tok.whitespace?
14
+ next
15
+ # keep some tokens in line:
16
+ # - option switches and conditions for syntactic correctness
17
+ # - quantifiers and codepoint runs for conciseness
18
+ elsif tok.type == :quantifier ||
19
+ tok.subtype == :options_switch && tok.text == ')' ||
20
+ tok.subtype == :condition ||
21
+ prev && forms_run.call(prev) && forms_run.call(tok)
22
+ acc << theme.colorize(tok.inlined_text, tok.type)
23
+ # keep comments in line, too, but with padding
24
+ elsif tok.comment?
25
+ acc << " #{theme.colorize(tok.inlined_text, tok.type)}"
26
+ # render root start as wtokl as empty root end in same line
27
+ elsif tok.subtype == :root && (prev.nil? || prev.subtype == :root)
28
+ acc << theme.colorize(tok.text, tok.type)
29
+ # tokse, if root is not empty, ensure x-flag is present at end
30
+ elsif tok.subtype == :root
31
+ acc << "\n#{theme.colorize(tok.text.sub(/x?\z/, 'x'), tok.type)}"
32
+ # render other tokens on their own lines for an indented structure,
33
+ # e.g. groups, alternations, anchors, assertions, ...
34
+ else
35
+ acc << "\n#{theme.colorize(tok.indented_text, tok.type)}"
36
+ end
37
+ prev = tok
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,7 +1,8 @@
1
1
  module Repper
2
2
  module Format
3
- Inline = ->(elements, theme) do
4
- elements.map { |el| theme.colorize(el.text, el.type) }.join
3
+ # A format that only adds color but does not change structure.
4
+ Inline = ->(tokens, theme) do
5
+ tokens.map { |tok| theme.colorize(tok.text, tok.type) }.join
5
6
  end
6
7
  end
7
8
  end
@@ -1,7 +1,8 @@
1
1
  module Repper
2
2
  module Format
3
- Plain = ->(elements, *) do
4
- elements.map(&:text).join
3
+ # A no-op format, equivalent to the original Regexp#inspect.
4
+ Plain = ->(tokens, *) do
5
+ tokens.map(&:text).join
5
6
  end
6
7
  end
7
8
  end
@@ -1,7 +1,8 @@
1
1
  module Repper
2
2
  module Format
3
- Structured = ->(elements, theme) do
4
- table = Tabulo::Table.new(elements.reject(&:whitespace?), **TABULO_STYLE)
3
+ # A structured format with colorization.
4
+ Structured = ->(tokens, theme) do
5
+ table = Tabulo::Table.new(tokens.reject(&:whitespace?), **TABULO_STYLE)
5
6
  table.add_column(
6
7
  :indented_text,
7
8
  styler: ->(_, string, cell) { theme.colorize(string, cell.source.type) }
@@ -4,7 +4,7 @@ module Repper
4
4
  module Format
5
5
  TABULO_STYLE = {
6
6
  border: :blank,
7
- header_frequency: nil,
7
+ header_frequency: nil, # i.e. omit column headers
8
8
  truncation_indicator: '…',
9
9
  wrap_body_cells_to: 1,
10
10
  }
data/lib/repper/format.rb CHANGED
@@ -3,8 +3,9 @@ module Repper
3
3
  def self.cast(arg)
4
4
  case arg
5
5
  when ::Proc then arg
6
+ when :x then Format::Extended
6
7
  when ::Symbol, ::String then Format.const_get(arg.capitalize) rescue nil
7
- when false, nil then Format::Plain
8
+ when false, nil then Format::Inline
8
9
  end || raise(Repper::ArgumentError, "unknown format #{arg.inspect}")
9
10
  end
10
11
  end
@@ -1,14 +1,27 @@
1
1
  module Repper
2
- Element = Struct.new(:type, :subtype, :level, :text, :id, keyword_init: true) do
2
+ Token = Struct.new(:type, :subtype, :level, :text, :id, keyword_init: true) do
3
3
  def indented_text
4
- inlined_text = text.gsub(/[\n\r\t\v]/) { |ws| ws.inspect.delete(?") }
5
4
  "#{' ' * level}#{inlined_text}"
6
5
  end
7
6
 
7
+ def inlined_text
8
+ if comment?
9
+ text.strip
10
+ else
11
+ text
12
+ .gsub(/(?<!\\) /, '\\ ')
13
+ .gsub(/[\n\r\t\v]/) { |ws| ws.inspect.delete(?") }
14
+ end
15
+ end
16
+
8
17
  def whitespace?
9
18
  subtype == :whitespace
10
19
  end
11
20
 
21
+ def comment?
22
+ subtype == :comment
23
+ end
24
+
12
25
  def annotation
13
26
  case [type, subtype]
14
27
  in [_, :root]
@@ -1,28 +1,28 @@
1
1
  require 'regexp_parser'
2
2
 
3
3
  module Repper
4
- module Parser
4
+ module Tokenizer
5
5
  module_function
6
6
 
7
- def call(regexp)
8
- tree = ::Regexp::Parser.parse(regexp)
9
- flatten(tree)
7
+ def call(regexp, delimiters: ['/', '/'], flags: nil)
8
+ tree = Regexp::Parser.parse(regexp, options: flags =~ /x/ && Regexp::EXTENDED)
9
+ flatten(tree, delimiters: delimiters, flags: flags)
10
10
  rescue ::Regexp::Parser::Error => e
11
11
  raise e.extend(Repper::Error)
12
12
  end
13
13
 
14
14
  # Turn Regexp::Parser AST back into a flat Array of visual elements
15
15
  # that match the Regexp notation.
16
- def flatten(exp, acc = [])
16
+ def flatten(exp, acc = [], delimiters: nil, flags: nil)
17
17
  # Add opening entry.
18
- exp.is?(:root) && acc << make_element(exp, '/')
18
+ exp.is?(:root) && acc << make_token(exp, delimiters[0])
19
19
 
20
20
  # Ignore nesting of invisible intermediate branches for better visuals.
21
21
  exp.is?(:sequence) && exp.nesting_level -= 1
22
22
 
23
23
  exp.parts.each do |part|
24
24
  if part.instance_of?(::String)
25
- acc << make_element(exp, part)
25
+ acc << make_token(exp, part)
26
26
  else # part.is_a?(Regexp::Expression::Base)
27
27
  flatten(part, acc)
28
28
  end
@@ -31,13 +31,16 @@ module Repper
31
31
  exp.quantified? && flatten(exp.quantifier, acc)
32
32
 
33
33
  # Add closing entry.
34
- exp.is?(:root) && acc << make_element(exp, "/#{exp.options.keys.join}")
34
+ exp.is?(:root) && begin
35
+ flags ||= exp.options.keys.join
36
+ acc << make_token(exp, "#{delimiters[1]}#{flags.chars.uniq.sort.join}")
37
+ end
35
38
 
36
39
  acc
37
40
  end
38
41
 
39
- def make_element(exp, text)
40
- Element.new(
42
+ def make_token(exp, text)
43
+ Token.new(
41
44
  type: exp.type,
42
45
  subtype: exp.token,
43
46
  level: exp.nesting_level,
@@ -1,3 +1,3 @@
1
1
  module Repper
2
- VERSION = "1.0.0"
2
+ VERSION = "1.1.0"
3
3
  end
data/lib/repper.rb CHANGED
@@ -1,7 +1,9 @@
1
- require_relative "repper/element"
1
+ require_relative "repper/codemod"
2
+ require_relative "repper/command"
2
3
  require_relative "repper/errors"
3
4
  require_relative "repper/format"
4
- require_relative "repper/parser"
5
+ require_relative "repper/token"
6
+ require_relative "repper/tokenizer"
5
7
  require_relative "repper/theme"
6
8
  require_relative "repper/version"
7
9
 
@@ -14,18 +16,18 @@ module Repper
14
16
  end
15
17
 
16
18
  def render(regexp, format: self.format, theme: self.theme)
17
- elements = Parser.call(regexp)
18
- format = Format.cast(format)
19
- theme = Theme.cast(theme)
20
- format.call(elements, theme)
19
+ tokens = Tokenizer.call(regexp)
20
+ format = Format.cast(format)
21
+ theme = Theme.cast(theme)
22
+ format.call(tokens, theme)
21
23
  end
22
24
 
23
- def theme=(theme)
24
- @theme = Theme.cast(theme)
25
+ def format=(arg)
26
+ @format = Format.cast(arg)
25
27
  end
26
28
 
27
- def format=(format)
28
- @format = Format.cast(format)
29
+ def theme=(arg)
30
+ @theme = Theme.cast(arg)
29
31
  end
30
32
  end
31
33
 
data/repper.gemspec CHANGED
@@ -6,7 +6,7 @@ Gem::Specification.new do |spec|
6
6
  spec.authors = ["Janosch Müller"]
7
7
  spec.email = ["janosch84@gmail.com"]
8
8
 
9
- spec.summary = "Regexp pretty printer for Ruby"
9
+ spec.summary = "Regexp pretty printer and formatter for Ruby"
10
10
  spec.homepage = "https://github.com/jaynetics/repper"
11
11
  spec.license = "MIT"
12
12
  spec.required_ruby_version = ">= 2.7.0"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: repper
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Janosch Müller
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-05-09 00:00:00.000000000 Z
11
+ date: 2022-05-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rainbow
@@ -55,7 +55,8 @@ dependencies:
55
55
  description:
56
56
  email:
57
57
  - janosch84@gmail.com
58
- executables: []
58
+ executables:
59
+ - repper
59
60
  extensions: []
60
61
  extra_rdoc_files: []
61
62
  files:
@@ -65,24 +66,28 @@ files:
65
66
  - LICENSE.txt
66
67
  - README.md
67
68
  - Rakefile
69
+ - exe/repper
68
70
  - lib/repper.rb
71
+ - lib/repper/codemod.rb
72
+ - lib/repper/command.rb
69
73
  - lib/repper/core_ext/kernel.rb
70
74
  - lib/repper/core_ext/kernel_ext.rb
71
75
  - lib/repper/core_ext/regexp.rb
72
76
  - lib/repper/core_ext/regexp_ext.rb
73
- - lib/repper/element.rb
74
77
  - lib/repper/errors.rb
75
78
  - lib/repper/format.rb
76
79
  - lib/repper/format/annotated.rb
80
+ - lib/repper/format/extended.rb
77
81
  - lib/repper/format/inline.rb
78
82
  - lib/repper/format/plain.rb
79
83
  - lib/repper/format/structured.rb
80
84
  - lib/repper/format/tabulo_style.rb
81
- - lib/repper/parser.rb
82
85
  - lib/repper/theme.rb
83
86
  - lib/repper/theme/default.rb
84
87
  - lib/repper/theme/monokai.rb
85
88
  - lib/repper/theme/plain.rb
89
+ - lib/repper/token.rb
90
+ - lib/repper/tokenizer.rb
86
91
  - lib/repper/version.rb
87
92
  - repper.gemspec
88
93
  - repper.svg
@@ -112,5 +117,5 @@ requirements: []
112
117
  rubygems_version: 3.4.0.dev
113
118
  signing_key:
114
119
  specification_version: 4
115
- summary: Regexp pretty printer for Ruby
120
+ summary: Regexp pretty printer and formatter for Ruby
116
121
  test_files: []