Skip to content

Commit

Permalink
Allows adding AST-matchers via classes
Browse files Browse the repository at this point in the history
- Adds `add_ast_matcher` to tasks similar to scanners.
- Adds a class for matching Rails model translations.
- User.human_attribute_name('name') and User.model_name.human(count: 2).
  • Loading branch information
davidwessman authored and glebm committed Apr 10, 2022
1 parent ec8d473 commit 31ba3e8
Show file tree
Hide file tree
Showing 11 changed files with 237 additions and 28 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ If you have implemented a custom adapter please share it on [the wiki][wiki].

### Usage search

i18n-tasks uses an AST scanner for `.rb` files, and a regexp scanner for all other files.
i18n-tasks uses an AST scanner for `.rb` and `.html.erb` files, and a regexp scanner for all other files.
New scanners can be added easily: please refer to [this example](https://github.com/glebm/i18n-tasks/wiki/A-custom-scanner-example).

See the `search` section in the [config file][config] for all available configuration options.
Expand Down Expand Up @@ -441,10 +441,10 @@ Custom tasks can be added easily, see the examples [on the wiki](https://github.
[badge-ci]: https://github.com/glebm/i18n-tasks/actions/workflows/tests.yml/badge.svg
[coverage]: https://codeclimate.com/github/glebm/i18n-tasks
[badge-coverage]: https://api.codeclimate.com/v1/badges/5d173e90ada8df07cedc/test_coverage
[config]: https://github.com/glebm/i18n-tasks/blob/master/templates/config/i18n-tasks.yml
[config]: https://github.com/glebm/i18n-tasks/blob/main/templates/config/i18n-tasks.yml
[wiki]: https://github.com/glebm/i18n-tasks/wiki "i18n-tasks wiki"
[i18n-gem]: https://github.com/svenfuchs/i18n "svenfuchs/i18n on Github"
[screenshot-i18n-tasks]: https://i.imgur.com/XZBd8l7.png "i18n-tasks screenshot"
[screenshot-find]: https://i.imgur.com/VxBrSfY.png "i18n-tasks find output screenshot"
[adapter-example]: https://github.com/glebm/i18n-tasks/blob/master/lib/i18n/tasks/data/file_system_base.rb
[adapter-example]: https://github.com/glebm/i18n-tasks/blob/main/lib/i18n/tasks/data/file_system_base.rb
[custom-scanner-docs]: https://github.com/glebm/i18n-tasks/wiki/A-custom-scanner-example
11 changes: 11 additions & 0 deletions lib/i18n/tasks.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,17 @@ def add_commands(commands_module)
::I18n::Tasks::Commands.send :include, commands_module
self
end

# Add AST-matcher to i18n-tasks
#
# @param matcher_class_name
# @return self
def add_ast_matcher(matcher_class_name)
matchers = I18n::Tasks::Configuration::DEFAULTS[:search][:ast_matchers]
matchers << matcher_class_name
matchers.uniq!
self
end
end

@verbose = !ENV['VERBOSE'].nil?
Expand Down
4 changes: 4 additions & 0 deletions lib/i18n/tasks/scanners/ast_matchers/base_matcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ def initialize(scanner:)
@scanner = scanner
end

def convert_to_key_occurrences(send_node, method_name, location: send_node.loc)
raise("Not implemented")
end

protected

# If the node type is of `%i(sym str int false true)`, return the value as a string.
Expand Down
69 changes: 69 additions & 0 deletions lib/i18n/tasks/scanners/ast_matchers/rails_model_matcher.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# frozen_string_literal: true

require 'i18n/tasks/scanners/results/occurrence'

module I18n::Tasks::Scanners::AstMatchers
class RailsModelMatcher < BaseMatcher
def convert_to_key_occurrences(send_node, _method_name, location: send_node.loc)
human_attribute_name_to_key_occurences(send_node: send_node, location: location) ||
model_name_human_to_key_occurences(send_node: send_node, location: location)
end

private

def human_attribute_name_to_key_occurences(send_node:, location:)
children = Array(send_node&.children)
receiver = children[0]
method_name = children[1]

return unless method_name == :human_attribute_name && receiver.type == :const

value = children[2]

model_name = underscore(receiver.to_a.last)
attribute = extract_string(value)
key = "activerecord.attributes.#{model_name}.#{attribute}"
[
key,
I18n::Tasks::Scanners::Results::Occurrence.from_range(
raw_key: key,
range: location.expression
)
]
end

# User.model_name.human(count: 2)
# s(:send,
# s(:send,
# s(:const, nil, :User), :model_name), :human,
# s(:hash,
# s(:pair,
# s(:sym, :count),
# s(:int, 2))))
def model_name_human_to_key_occurences(send_node:, location:)
children = Array(send_node&.children)
return unless children[1] == :human

base_children = Array(children[0]&.children)
class_node = base_children[0]

return unless class_node&.type == :const && base_children[1] == :model_name

model_name = underscore(class_node.to_a.last)
key = "activerecord.models.#{model_name}"
[
key,
I18n::Tasks::Scanners::Results::Occurrence.from_range(
raw_key: key,
range: location.expression
)
]
end

def underscore(value)
value = value.dup.to_s
value.gsub!(/(.)([A-Z])/, '\1_\2')
value.downcase!
end
end
end
8 changes: 7 additions & 1 deletion lib/i18n/tasks/scanners/ruby_ast_scanner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ def setup_matchers
)
end
else
%i[t t! translate translate!].map do |message|
matchers = %i[t t! translate translate!].map do |message|
AstMatchers::MessageReceiversMatcher.new(
receivers: [
AST::Node.new(:const, [nil, :I18n]),
Expand All @@ -132,6 +132,12 @@ def setup_matchers
scanner: self
)
end

Array(config[:ast_matchers]).each do |class_name|
matchers << ActiveSupport::Inflector.constantize(class_name).new(scanner: self)
end

matchers
end
end
end
Expand Down
1 change: 1 addition & 0 deletions lib/i18n/tasks/used_keys.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ module UsedKeys # rubocop:disable Metrics/ModuleLength
['::I18n::Tasks::Scanners::ErbAstScanner', { only: %w[*.erb] }],
['::I18n::Tasks::Scanners::PatternWithScopeScanner', { exclude: %w[*.erb *.rb] }]
],
ast_matchers: [],
strict: true
}.freeze

Expand Down
2 changes: 2 additions & 0 deletions spec/fixtures/used_keys/a.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ def self.whot
Service.translate(:what)
I18n.t('activerecord.attributes.absolute.attribute')
translate('activerecord.attributes.absolute.attribute')
Archive.human_attribute_name(:name)
User.model_name.human(count: 2)
end

SCOPE_CONSTANT = 'path.in.translation.file'.freeze
Expand Down
6 changes: 4 additions & 2 deletions spec/fixtures/used_keys/app/views/application/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
<% what = t 'a' %>
I18n.t("this_should_not")
<h2>
<% # i18n-tasks-use t('activerecord.models.first.one') %>
<%= First.model_name.human(count: 1) %>
<% # i18n-tasks-use t('comment.absolute.attribute') %>
<%= Translate.absolute.attribute %>
<%= MeetingNote.model_name.human(count: 1) %>
<%= AgendaItem.human_attribute_name(:title) %>
</h2>
<h3>
<%= t('with_parameter', parameter: "erb is the best") %>
Expand Down
93 changes: 74 additions & 19 deletions spec/used_keys_erb_spec.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
# frozen_string_literal: true

require 'spec_helper'
require 'i18n/tasks/scanners/ast_matchers/rails_model_matcher'

RSpec.describe 'UsedKeysErb' do
let!(:task) { I18n::Tasks::BaseTask.new }
around do |ex|
I18n::Tasks::Configuration::DEFAULTS[:search][:ast_matchers].clear
ast_matchers.each do |matcher|
I18n::Tasks.add_ast_matcher(matcher)
end
task.config[:search] = { paths: paths }
TestCodebase.in_test_app_dir(directory: 'spec/fixtures/used_keys') { ex.run }
end
Expand All @@ -13,11 +18,15 @@
%w[app/views/application/show.html.erb]
}

let(:ast_matchers) {
%w[I18n::Tasks::Scanners::AstMatchers::RailsModelMatcher]
}

it '#used_keys' do
used_keys = task.used_tree
expect(used_keys.size).to eq(1)
leaves = used_keys.leaves.to_a
expect(leaves.size).to eq(7)
expect(leaves.size).to eq(9)

expect_node_key_data(
leaves[0],
Expand All @@ -41,30 +50,65 @@
]
)
)

expect_node_key_data(
leaves[1],
'activerecord.models.meeting_note',
occurrences: make_occurrences(
[
{
path: 'app/views/application/show.html.erb',
pos: 184,
line_num: 7, line_pos: 5,
line: " <%= MeetingNote.model_name.human(count: 1) %>",
raw_key: 'activerecord.models.meeting_note'
},
]
)
)

expect_node_key_data(
leaves[2],
'activerecord.attributes.agenda_item.title',
occurrences: make_occurrences(
[
{
path: 'app/views/application/show.html.erb',
pos: 232,
line_num: 8, line_pos: 5,
line: " <%= AgendaItem.human_attribute_name(:title) %>",
raw_key: 'activerecord.attributes.agenda_item.title'
},
]
)
)


expect_node_key_data(
leaves[3],
'with_parameter',
occurrences: make_occurrences(
[
{
path: 'app/views/application/show.html.erb',
pos: 202,
line_num: 9, line_pos: 5,
pos: 292,
line_num: 11, line_pos: 5,
line: " <%= t('with_parameter', parameter: \"erb is the best\") %>",
raw_key: 'with_parameter'
}
]
)
)

expect_node_key_data(
leaves[2],
leaves[4],
'scope_a.scope_b.with_scope',
occurrences: make_occurrences(
[
{
path: 'app/views/application/show.html.erb',
pos: 261,
line_num: 10, line_pos: 5,
pos: 351,
line_num: 12, line_pos: 5,
line: " <%= t 'with_scope', scope: \"scope_a.scope_b\", default: t(\".nested_call\") %>",
raw_key: 'scope_a.scope_b.with_scope'
}
Expand All @@ -73,14 +117,14 @@
)

expect_node_key_data(
leaves[3],
leaves[5],
'application.show.nested_call',
occurrences: make_occurrences(
[
{
path: 'app/views/application/show.html.erb',
pos: 313,
line_num: 10, line_pos: 57,
pos: 403,
line_num: 12, line_pos: 57,
line: " <%= t 'with_scope', scope: \"scope_a.scope_b\", default: t(\".nested_call\") %>",
raw_key: '.nested_call'
}
Expand All @@ -89,14 +133,14 @@
)

expect_node_key_data(
leaves[4],
leaves[6],
"application.show.edit",
occurrences: make_occurrences(
[
{
path: 'app/views/application/show.html.erb',
pos: 433,
line_num: 13, line_pos: 41,
pos: 523,
line_num: 15, line_pos: 41,
line: ' <%= link_to(edit_foo_path(foo), title: t(".edit")) do %>',
raw_key: '.edit'
}
Expand All @@ -105,14 +149,14 @@
)

expect_node_key_data(
leaves[5],
leaves[7],
"blacklight.tools.citation",
occurrences: make_occurrences(
[
{
path: 'app/views/application/show.html.erb',
pos: 655,
line_num: 19, line_pos: 25,
pos: 745,
line_num: 21, line_pos: 25,
line: " <% component.title { t('blacklight.tools.citation') } %>",
raw_key: 'blacklight.tools.citation'
}
Expand All @@ -121,16 +165,16 @@
)

expect_node_key_data(
leaves[6],
'activerecord.models.first.one',
leaves[8],
'comment.absolute.attribute',
occurrences: make_occurrences(
[
{
path: 'app/views/application/show.html.erb',
pos: 88,
line_num: 5, line_pos: 4,
line: " <% # i18n-tasks-use t('activerecord.models.first.one') %>",
raw_key: 'activerecord.models.first.one'
line: " <% # i18n-tasks-use t('comment.absolute.attribute') %>",
raw_key: 'comment.absolute.attribute'
}
]
)
Expand Down Expand Up @@ -278,4 +322,15 @@
)
end
end

describe 'without rails_model matcher' do
let(:ast_matchers) { [] }

it '#used_keys' do
used_keys = task.used_tree
expect(used_keys.size).to eq(1)
leaves = used_keys.leaves.to_a
expect(leaves.size).to(eq(7))
end
end
end
Loading

0 comments on commit 31ba3e8

Please sign in to comment.