i18n_linter 0.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 +7 -0
- data/README.md +95 -0
- data/config/default.yml +36 -0
- data/exe/i18n_linter +28 -0
- data/lib/i18n_linter/config.rb +75 -0
- data/lib/i18n_linter/constants.rb +101 -0
- data/lib/i18n_linter/helpers/digger.rb +31 -0
- data/lib/i18n_linter/linter.rb +112 -0
- data/lib/i18n_linter/options.rb +24 -0
- data/lib/i18n_linter/result.rb +15 -0
- data/lib/i18n_linter/result_set.rb +25 -0
- data/lib/i18n_linter/rules/class_name.rb +14 -0
- data/lib/i18n_linter/rules/constant.rb +18 -0
- data/lib/i18n_linter/rules/environment_variable.rb +19 -0
- data/lib/i18n_linter/rules/http_headers.rb +44 -0
- data/lib/i18n_linter/rules/logger.rb +19 -0
- data/lib/i18n_linter/rules/middle_space.rb +11 -0
- data/lib/i18n_linter/rules/mime_type.rb +27 -0
- data/lib/i18n_linter/rules/puts.rb +19 -0
- data/lib/i18n_linter/rules/query.rb +12 -0
- data/lib/i18n_linter/rules/reg_exp.rb +11 -0
- data/lib/i18n_linter/rules/scope.rb +13 -0
- data/lib/i18n_linter/rules/strftime.rb +14 -0
- data/lib/i18n_linter/rules/underscore.rb +20 -0
- data/lib/i18n_linter/rules/words.rb +11 -0
- data/lib/i18n_linter/rules.rb +30 -0
- data/lib/i18n_linter/runner.rb +54 -0
- data/lib/i18n_linter/string_line.rb +15 -0
- data/lib/i18n_linter/token.rb +15 -0
- data/lib/i18n_linter/version.rb +5 -0
- data/lib/i18n_linter.rb +37 -0
- metadata +143 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 546355f63e25c1190ee40b9754f0c4da3c975eae482294e7e6cdf5d05ac6adf8
|
4
|
+
data.tar.gz: 5c67c90c7145f8cad0be840c4f612532f01a724a2152b702d15479e741e1425c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 22ed44d204dbfa491c700496ee972863baac3cd6c5aacc6f30f6d2276e052adb4fdb68717e763a3cd82f3fd3c786d582562644e3e0a33447ce7a5ea891209a88
|
7
|
+
data.tar.gz: 9c926cc219f5e2ffe66104c74db113abcd2b8f017a3e1bf1f5e1abe9ee4f369efaeeced79a5724c2265bd1a901a60e9dcd0dd6484490515a34041c563338b7dd
|
data/README.md
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
# I18nLinter
|
2
|
+
|
3
|
+
[](https://circleci.com/gh/rootstrap/i18n_linter/tree/master)
|
4
|
+
[](https://codeclimate.com/repos/5c6b0bbdd561465d35008579/maintainability)
|
5
|
+
[](https://codeclimate.com/repos/5c6b0bbdd561465d35008579/test_coverage)
|
6
|
+
|
7
|
+
Internationalization linter for your Ruby on Rails projects.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
All you have to do is run the following command:
|
11
|
+
```bash
|
12
|
+
$ gem install i18n_linter
|
13
|
+
```
|
14
|
+
If you want to install using `bundler`, add this to the `Gemfile` under the `development` group:
|
15
|
+
```ruby
|
16
|
+
gem 'i18n_linter', require: false
|
17
|
+
```
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
Just type `i18n_linter` in a Ruby on Rails project's folder and watch the strings that could be internationalized. Note: only strings in ruby files will be reported.
|
21
|
+
```
|
22
|
+
$ cd my/ruby_on_rails/project
|
23
|
+
$ i18n_linter [options]
|
24
|
+
```
|
25
|
+
|
26
|
+
## Options
|
27
|
+
The available options are:
|
28
|
+
```
|
29
|
+
-f PATTERN, --files=PATTERN Pattern to find files, default: -f '**/*.rb'
|
30
|
+
-o FILE, --out=FILE, Write output to a file instead of STDOUT
|
31
|
+
```
|
32
|
+
|
33
|
+
For example:
|
34
|
+
|
35
|
+
```
|
36
|
+
$ i18n_linter -f users_controller.rb
|
37
|
+
```
|
38
|
+
```
|
39
|
+
$ i18n_linter -f app/controllers/**/*.rb -o i18n_linter_output.txt
|
40
|
+
```
|
41
|
+
|
42
|
+
## Configuration
|
43
|
+
The behavior of I18nLinter can be controlled via the `.i18n_linter.yml` configuration file.
|
44
|
+
It's possible to enable or disable Rules and exclude files from the validations as follows:
|
45
|
+
```ruby
|
46
|
+
Linter:
|
47
|
+
Include:
|
48
|
+
- '**/*.rb'
|
49
|
+
Exclude:
|
50
|
+
- 'spec/**/*'
|
51
|
+
Rules:
|
52
|
+
Uppercase:
|
53
|
+
Enabled: true
|
54
|
+
MiddleSpace:
|
55
|
+
Enabled: false
|
56
|
+
```
|
57
|
+
|
58
|
+
## Example
|
59
|
+
Imagine a source file sample.rb containing:
|
60
|
+
```ruby
|
61
|
+
class UserController < ApplicationController
|
62
|
+
rescue_from ActiveRecord::RecordNotFound, with: :render_not_found
|
63
|
+
|
64
|
+
def show
|
65
|
+
@user = User.find(params[:id])
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def render_not_found
|
71
|
+
render json: { error: "Couldn't find the record" }, status: :not_found
|
72
|
+
end
|
73
|
+
end
|
74
|
+
```
|
75
|
+
I18nLinter will return the following warnings in this file:
|
76
|
+
```
|
77
|
+
$ i18n_linter -f sample.rb
|
78
|
+
|
79
|
+
sample.rb:11:26
|
80
|
+
10: def render_not_found
|
81
|
+
11: render json: { error: "Couldn't find the record" }, status: :not_found
|
82
|
+
12: end
|
83
|
+
----------------
|
84
|
+
```
|
85
|
+
|
86
|
+
## Contributing
|
87
|
+
Bug reports (please use Issues) and pull requests are welcome on GitHub at https://github.com/rootstrap/i18n_linter. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
88
|
+
|
89
|
+
## License
|
90
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
91
|
+
|
92
|
+
## Credits
|
93
|
+
**I18nLinter** is maintained by [Rootstrap](http://www.rootstrap.com) with the help of our [contributors](https://github.com/rootstrap/i18n_linter/contributors).
|
94
|
+
|
95
|
+
[<img src="https://s3-us-west-1.amazonaws.com/rootstrap.com/img/rs.png" width="100"/>](http://www.rootstrap.com)
|
data/config/default.yml
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
Linter:
|
2
|
+
Include:
|
3
|
+
- '**/*.rb'
|
4
|
+
Exclude:
|
5
|
+
- 'config/**/*'
|
6
|
+
- 'spec/**/*'
|
7
|
+
- 'db/**/*'
|
8
|
+
Rules:
|
9
|
+
ClassName:
|
10
|
+
Enabled: true
|
11
|
+
Constant:
|
12
|
+
Enabled: true
|
13
|
+
EnvironmentVariable:
|
14
|
+
Enabled: true
|
15
|
+
HttpHeaders:
|
16
|
+
Enabled: true
|
17
|
+
Logger:
|
18
|
+
Enabled: true
|
19
|
+
MiddleSpace:
|
20
|
+
Enabled: true
|
21
|
+
MimeType:
|
22
|
+
Enabled: true
|
23
|
+
Puts:
|
24
|
+
Enabled: true
|
25
|
+
Query:
|
26
|
+
Enabled: true
|
27
|
+
RegExp:
|
28
|
+
Enabled: true
|
29
|
+
Scope:
|
30
|
+
Enabled: true
|
31
|
+
Strftime:
|
32
|
+
Enabled: true
|
33
|
+
Underscore:
|
34
|
+
Enabled: true
|
35
|
+
Words:
|
36
|
+
Enabled: true
|
data/exe/i18n_linter
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'i18n_linter'
|
4
|
+
require 'i18n_linter/version'
|
5
|
+
require 'i18n_linter/runner'
|
6
|
+
require 'i18n_linter/options'
|
7
|
+
require 'i18n_linter/config'
|
8
|
+
require 'optparse'
|
9
|
+
|
10
|
+
Version = I18nLinter::VERSION
|
11
|
+
|
12
|
+
config = I18nLinter::Config.new
|
13
|
+
options = I18nLinter::Options.new(config)
|
14
|
+
|
15
|
+
opt = OptionParser.new
|
16
|
+
opt.banner = 'Usage: i18n_linter'
|
17
|
+
opt.on('-f PATTERN', '--files=PATTERN', "Pattern to find files, default: -f '**/*.rb'") do |pattern|
|
18
|
+
options.files = [pattern]
|
19
|
+
end
|
20
|
+
opt.on('-o FILE', '--out=FILE', 'Write output to a file instead of STDOUT') do |file|
|
21
|
+
options.out_file = file
|
22
|
+
end
|
23
|
+
|
24
|
+
opt.parse!
|
25
|
+
|
26
|
+
runner = I18nLinter::Runner.new(options, config)
|
27
|
+
ret = runner.run
|
28
|
+
exit(ret)
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
module I18nLinter
|
6
|
+
class Config
|
7
|
+
DOTFILE = '.i18n_linter.yml'
|
8
|
+
I18N_LINTER_HOME = File.realpath(File.join(File.dirname(__FILE__), '..', '..'))
|
9
|
+
DEFAULT_FILE = File.join(I18N_LINTER_HOME, 'config', 'default.yml')
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
path = File.exist?(DOTFILE) ? DOTFILE : DEFAULT_FILE
|
13
|
+
@hash = load_yaml_configuration(path)
|
14
|
+
add_missing_rules(@hash['Rules'])
|
15
|
+
end
|
16
|
+
|
17
|
+
def patterns_to_include
|
18
|
+
linter_patterns['Include'] || []
|
19
|
+
end
|
20
|
+
|
21
|
+
def patterns_to_exclude
|
22
|
+
linter_patterns['Exclude'] || []
|
23
|
+
end
|
24
|
+
|
25
|
+
def enabled_positive_rules
|
26
|
+
all_rules.keys.select { |rule| positive_rule?(rule) && enabled_rule?(rule) }
|
27
|
+
end
|
28
|
+
|
29
|
+
def enabled_negative_rules
|
30
|
+
all_rules.keys.select { |rule| negative_rule?(rule) && enabled_rule?(rule) }
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def [](key)
|
36
|
+
@hash[key]
|
37
|
+
end
|
38
|
+
|
39
|
+
def linter_patterns
|
40
|
+
@linter_patterns ||= self['Linter'] || {}
|
41
|
+
end
|
42
|
+
|
43
|
+
def all_rules
|
44
|
+
@all_rules ||= self['Rules'] || {}
|
45
|
+
end
|
46
|
+
|
47
|
+
def enabled_rule?(rule)
|
48
|
+
all_rules[rule]['Enabled']
|
49
|
+
end
|
50
|
+
|
51
|
+
def positive_rule?(rule)
|
52
|
+
Rules::POSITIVE_RULES.include?(rule)
|
53
|
+
end
|
54
|
+
|
55
|
+
def negative_rule?(rule)
|
56
|
+
Rules::NEGATIVE_RULES.include?(rule)
|
57
|
+
end
|
58
|
+
|
59
|
+
def load_yaml_configuration(path)
|
60
|
+
yaml_code = File.read(path)
|
61
|
+
hash = YAML.safe_load(yaml_code, [Regexp, Symbol], [], false, path) || {}
|
62
|
+
|
63
|
+
raise(TypeError, "Malformed configuration in #{path}") unless hash.is_a?(Hash)
|
64
|
+
|
65
|
+
hash
|
66
|
+
end
|
67
|
+
|
68
|
+
def add_missing_rules(loaded_rules)
|
69
|
+
missing = (Rules::POSITIVE_RULES + Rules::NEGATIVE_RULES) - loaded_rules.keys
|
70
|
+
missing.each do |rule|
|
71
|
+
loaded_rules.store(rule, 'Enabled' => true)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18nLinter
|
4
|
+
class Constants
|
5
|
+
HTTP_HEADERS = %w[
|
6
|
+
Accept
|
7
|
+
Accept-Charset
|
8
|
+
Accept-Encoding
|
9
|
+
Accept-Language
|
10
|
+
Accept-Ranges
|
11
|
+
Access-Control-Allow-Credentials
|
12
|
+
Access-Control-Allow-Headers
|
13
|
+
Access-Control-Allow-Methods
|
14
|
+
Access-Control-Allow-Origin
|
15
|
+
Access-Control-Expose-Headers
|
16
|
+
Access-Control-Max-Age
|
17
|
+
Access-Control-Request-Headers
|
18
|
+
Access-Control-Request-Method
|
19
|
+
Age
|
20
|
+
Allow
|
21
|
+
Alt-Svc
|
22
|
+
Authorization
|
23
|
+
Cache-Control
|
24
|
+
Clear-Site-Data
|
25
|
+
Connection
|
26
|
+
Content-Disposition
|
27
|
+
Content-Encoding
|
28
|
+
Content-Language
|
29
|
+
Content-Length
|
30
|
+
Content-Location
|
31
|
+
Content-Range
|
32
|
+
Content-Security-Policy
|
33
|
+
Content-Security-Policy-Report-Only
|
34
|
+
Content-Type
|
35
|
+
Cookie
|
36
|
+
Cookie2
|
37
|
+
DNT
|
38
|
+
Date
|
39
|
+
ETag
|
40
|
+
Early-Data
|
41
|
+
Expect
|
42
|
+
Expect-CT
|
43
|
+
Expires
|
44
|
+
Feature-Policy
|
45
|
+
Forwarded
|
46
|
+
From
|
47
|
+
Host
|
48
|
+
If-Match
|
49
|
+
If-Modified-Since
|
50
|
+
If-None-Match
|
51
|
+
If-Range
|
52
|
+
If-Unmodified-Since
|
53
|
+
Index
|
54
|
+
Keep-Alive
|
55
|
+
Large-Allocation
|
56
|
+
Last-Modified
|
57
|
+
Location
|
58
|
+
Origin
|
59
|
+
Pragma
|
60
|
+
Proxy-Authenticate
|
61
|
+
Proxy-Authorization
|
62
|
+
Public-Key-Pins
|
63
|
+
Public-Key-Pins-Report-Only
|
64
|
+
Range
|
65
|
+
Referer
|
66
|
+
Referrer-Policy
|
67
|
+
Retry-After
|
68
|
+
Sec-WebSocket-Accept
|
69
|
+
Server
|
70
|
+
Server-Timing
|
71
|
+
Set-Cookie
|
72
|
+
Set-Cookie2
|
73
|
+
SourceMap
|
74
|
+
Strict-Transport-Security
|
75
|
+
TE
|
76
|
+
Timing-Allow-Origin
|
77
|
+
Tk
|
78
|
+
Trailer
|
79
|
+
Transfer-Encoding
|
80
|
+
Upgrade-Insecure-Requests
|
81
|
+
User-Agent
|
82
|
+
Vary
|
83
|
+
Via
|
84
|
+
WWW-Authenticate
|
85
|
+
Warning
|
86
|
+
X-Content-Type-Options
|
87
|
+
X-DNS-Prefetch-Control
|
88
|
+
X-Forwarded-For
|
89
|
+
X-Forwarded-Host
|
90
|
+
X-Forwarded-Proto
|
91
|
+
X-Frame-Options
|
92
|
+
X-XSS-Protection
|
93
|
+
].freeze
|
94
|
+
|
95
|
+
QUERY_METHODS = %w[
|
96
|
+
annotate find find_by create_with distinct eager_load extending extract_associated from group
|
97
|
+
having includes joins left_outer_joins limit lock none offset optimizer_hints order
|
98
|
+
preload readonly references reorder reselect reverse_order select where
|
99
|
+
].freeze
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18nLinter
|
4
|
+
class Digger
|
5
|
+
def initialize(type)
|
6
|
+
@type = type
|
7
|
+
end
|
8
|
+
|
9
|
+
def find(targets, tree)
|
10
|
+
return false unless tree.is_a? Array
|
11
|
+
return true if target_found?(targets, tree)
|
12
|
+
|
13
|
+
tree.each do |item|
|
14
|
+
return true if find(targets, item)
|
15
|
+
end
|
16
|
+
false
|
17
|
+
end
|
18
|
+
|
19
|
+
def target_found?(targets, tree)
|
20
|
+
matches_type?(tree[0]) && matches_target?(tree[1], targets)
|
21
|
+
end
|
22
|
+
|
23
|
+
def matches_type?(item)
|
24
|
+
item == @type
|
25
|
+
end
|
26
|
+
|
27
|
+
def matches_target?(item, targets)
|
28
|
+
targets.include?(item)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'i18n_linter'
|
4
|
+
require 'colorize'
|
5
|
+
|
6
|
+
module I18nLinter
|
7
|
+
class Linter
|
8
|
+
def initialize(options, config)
|
9
|
+
@options = options
|
10
|
+
@config = config
|
11
|
+
@strings = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def lint(filename:, file:)
|
15
|
+
parsed_file = tokenize_file(filename, file)
|
16
|
+
find_strings(filename, parsed_file)
|
17
|
+
compile(filename)
|
18
|
+
end
|
19
|
+
|
20
|
+
def show_errors(results)
|
21
|
+
puts
|
22
|
+
results.each do |result|
|
23
|
+
file = File.readlines(result.filename)
|
24
|
+
line = result.line
|
25
|
+
print_block(result, file, line)
|
26
|
+
end
|
27
|
+
puts
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def tokenize_file(filename, file)
|
33
|
+
Ripper.sexp(file, filename)
|
34
|
+
end
|
35
|
+
|
36
|
+
def get_token(file, index)
|
37
|
+
file[index]
|
38
|
+
end
|
39
|
+
|
40
|
+
def get_string_array(file, current_index)
|
41
|
+
rest_of_file(file, current_index).take_while { |token|
|
42
|
+
!%i[on_tstring_end on_label_end].include?(token.type)
|
43
|
+
}.map(&:content)
|
44
|
+
end
|
45
|
+
|
46
|
+
def rest_of_file(file, current_index)
|
47
|
+
file.last(file.length - current_index)
|
48
|
+
end
|
49
|
+
|
50
|
+
def find_strings(filename, tokens)
|
51
|
+
return unless array?(tokens)
|
52
|
+
|
53
|
+
if array?(tokens[0])
|
54
|
+
tokens.each { |child| find_strings(filename, child) }
|
55
|
+
else
|
56
|
+
check_rules(filename, tokens)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def check_rules(filename, tokens)
|
61
|
+
if string_element?(tokens)
|
62
|
+
string = tokens[1]
|
63
|
+
@strings << StringLine.new(tokens[2], string) if Rules.check_string_rules(@config, string)
|
64
|
+
else
|
65
|
+
test_rules(filename, tokens)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def array?(elem)
|
70
|
+
elem.class == Array
|
71
|
+
end
|
72
|
+
|
73
|
+
def string_element?(elem)
|
74
|
+
elem[0] == :@tstring_content
|
75
|
+
end
|
76
|
+
|
77
|
+
def compile(filename)
|
78
|
+
result_set = ResultSet.new
|
79
|
+
@strings.each do |string_line|
|
80
|
+
result_set.add_result(Result.new(filename, string_line, string_line.string))
|
81
|
+
end
|
82
|
+
@strings = []
|
83
|
+
result_set
|
84
|
+
end
|
85
|
+
|
86
|
+
def test_rules(filename, tokens)
|
87
|
+
return if tokens.empty? || Rules.check_negative_rules(@config, tokens)
|
88
|
+
|
89
|
+
check_rest_of_tokens(filename, tokens)
|
90
|
+
end
|
91
|
+
|
92
|
+
def check_rest_of_tokens(filename, tokens)
|
93
|
+
tokens[1..-1].each { |child| find_strings(filename, child) }
|
94
|
+
end
|
95
|
+
|
96
|
+
def print_block(result, file, line)
|
97
|
+
line_number = line.line_number
|
98
|
+
column_number = line.column_number
|
99
|
+
|
100
|
+
previous_line = file[line_number - 2] if line_number > 2
|
101
|
+
current_line = file[line_number - 1]
|
102
|
+
next_line = file[line_number] if line_number < file.length
|
103
|
+
|
104
|
+
output = "#{result.filename}:#{line_number}:#{column_number}\n".colorize(:green)
|
105
|
+
output += "#{line_number - 1}: #{previous_line}" if previous_line
|
106
|
+
output += "#{line_number}: #{current_line}".colorize(:yellow)
|
107
|
+
output += "#{line_number + 1}: #{next_line}" if next_line
|
108
|
+
|
109
|
+
puts output
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18nLinter
|
4
|
+
class Options
|
5
|
+
attr_writer :files
|
6
|
+
attr_accessor :out_file
|
7
|
+
|
8
|
+
def initialize(config)
|
9
|
+
@config = config
|
10
|
+
end
|
11
|
+
|
12
|
+
def files
|
13
|
+
return Dir[*@files].uniq.sort if @files
|
14
|
+
|
15
|
+
supported_files.sort
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def supported_files
|
21
|
+
(Dir.glob(@config.patterns_to_include, 0) - Dir.glob(@config.patterns_to_exclude, 0)).uniq
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18nLinter
|
4
|
+
class Result
|
5
|
+
attr_reader :filename
|
6
|
+
attr_reader :line
|
7
|
+
attr_reader :string
|
8
|
+
|
9
|
+
def initialize(filename, line, string)
|
10
|
+
@filename = filename
|
11
|
+
@line = line
|
12
|
+
@string = string
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18nLinter
|
4
|
+
class ResultSet
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@results = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def add_result(result)
|
12
|
+
@results << result
|
13
|
+
end
|
14
|
+
|
15
|
+
def each
|
16
|
+
@results.each do |result|
|
17
|
+
yield result
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def success?
|
22
|
+
count.zero?
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18nLinter
|
4
|
+
module Rules
|
5
|
+
class ClassName
|
6
|
+
CLASS_NAME_LABEL = 'class_name:'
|
7
|
+
|
8
|
+
def check(tokens)
|
9
|
+
tokens[0] == :assoc_new &&
|
10
|
+
I18nLinter::Digger.new(:@label).find([CLASS_NAME_LABEL], tokens[1])
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18nLinter
|
4
|
+
module Rules
|
5
|
+
class Constant
|
6
|
+
def check(tokens)
|
7
|
+
tokens[0] == :assign && constant_assign(tokens)
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def constant_assign(tokens)
|
13
|
+
assign_tokens = tokens[1]
|
14
|
+
assign_tokens[0] == :var_field && assign_tokens[1][0] == :@const
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18nLinter
|
4
|
+
module Rules
|
5
|
+
class EnvironmentVariable
|
6
|
+
ENV_CONST = 'ENV'
|
7
|
+
|
8
|
+
def check(tokens)
|
9
|
+
reference_or_method(tokens) && I18nLinter::Digger.new(:@const).find([ENV_CONST], tokens[1])
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def reference_or_method(tokens)
|
15
|
+
%i[aref method_add_arg].include?(tokens[0])
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18nLinter
|
4
|
+
module Rules
|
5
|
+
class HttpHeaders
|
6
|
+
def check(tokens)
|
7
|
+
tokens[0] == :assoc_new && literal_or_symbol(tokens)
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def literal_or_symbol(tokens)
|
13
|
+
literal_symbol_tokens = tokens[1]
|
14
|
+
(literal(literal_symbol_tokens) || symbol(literal_symbol_tokens)) &&
|
15
|
+
string_content(literal_symbol_tokens)
|
16
|
+
end
|
17
|
+
|
18
|
+
def literal(tokens)
|
19
|
+
tokens[0] == :string_literal
|
20
|
+
end
|
21
|
+
|
22
|
+
def symbol(tokens)
|
23
|
+
tokens[0] == :dyna_symbol
|
24
|
+
end
|
25
|
+
|
26
|
+
def string_content(tokens)
|
27
|
+
content_tokens = tokens[1]
|
28
|
+
content_tokens[0] == :string_content && header_string(content_tokens)
|
29
|
+
end
|
30
|
+
|
31
|
+
def header_string(tokens)
|
32
|
+
header_tokens = tokens[1]
|
33
|
+
header_tokens[0] == :@tstring_content && http_header(header_tokens)
|
34
|
+
end
|
35
|
+
|
36
|
+
def http_header(tokens)
|
37
|
+
string = tokens[1].downcase
|
38
|
+
I18nLinter::Constants::HTTP_HEADERS.map(&:downcase).any? do |header|
|
39
|
+
string.include?(header)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18nLinter
|
4
|
+
module Rules
|
5
|
+
class Logger
|
6
|
+
LOGGER_IDENT = 'logger'
|
7
|
+
|
8
|
+
def check(tokens)
|
9
|
+
command_or_method(tokens) && I18nLinter::Digger.new(:@ident).find([LOGGER_IDENT], tokens[1])
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def command_or_method(tokens)
|
15
|
+
%i[command_call method_add_block method_add_arg].include?(tokens[0])
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'mime/types'
|
4
|
+
|
5
|
+
module I18nLinter
|
6
|
+
module Rules
|
7
|
+
class MimeType
|
8
|
+
def check(tokens)
|
9
|
+
tokens[0] == :string_content && mime_type?(tokens)
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def mime_type?(tokens)
|
15
|
+
string_tokens = tokens[1]
|
16
|
+
return false unless string_tokens
|
17
|
+
|
18
|
+
string_tokens[0] == :@tstring_content && check_mime_types(string_tokens)
|
19
|
+
end
|
20
|
+
|
21
|
+
def check_mime_types(tokens)
|
22
|
+
string = tokens[1]
|
23
|
+
MIME::Types.any? { |mime| string.include?(mime) }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18nLinter
|
4
|
+
module Rules
|
5
|
+
class Puts
|
6
|
+
PUTS_IDENT = 'puts'
|
7
|
+
|
8
|
+
def check(tokens)
|
9
|
+
command_or_method(tokens) && I18nLinter::Digger.new(:@ident).find([PUTS_IDENT], tokens[1])
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def command_or_method(tokens)
|
15
|
+
%i[command method_add_arg].include?(tokens[0])
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18nLinter
|
4
|
+
module Rules
|
5
|
+
class Strftime
|
6
|
+
STRFTIME_IDENT = 'strftime'
|
7
|
+
|
8
|
+
def check(tokens)
|
9
|
+
tokens[0] == :method_add_arg &&
|
10
|
+
I18nLinter::Digger.new(:@ident).find([STRFTIME_IDENT], tokens[1])
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18nLinter
|
4
|
+
module Rules
|
5
|
+
class Underscore
|
6
|
+
def check(tokens)
|
7
|
+
tokens[0] == :string_content && underscore?(tokens)
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def underscore?(tokens)
|
13
|
+
string_tokens = tokens[1]
|
14
|
+
return false unless string_tokens
|
15
|
+
|
16
|
+
string_tokens[0] == :@tstring_content && /\_/ =~ string_tokens[1]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18nLinter
|
4
|
+
module Rules
|
5
|
+
POSITIVE_RULES = %w[MiddleSpace Words].freeze
|
6
|
+
# Rules that filter the strings found
|
7
|
+
NEGATIVE_RULES = %w[
|
8
|
+
ClassName Constant EnvironmentVariable HttpHeaders
|
9
|
+
Logger MimeType Puts Query RegExp Scope Strftime Underscore
|
10
|
+
].freeze
|
11
|
+
|
12
|
+
class << self
|
13
|
+
def check_rule(rule, string_or_tokens)
|
14
|
+
Kernel.const_get("I18nLinter::Rules::#{rule}").new.check(string_or_tokens)
|
15
|
+
end
|
16
|
+
|
17
|
+
def check_positive_rules(config, string)
|
18
|
+
config.enabled_positive_rules.any? { |rule| Rules.check_rule(rule, string) }
|
19
|
+
end
|
20
|
+
|
21
|
+
def check_negative_rules(config, tokens)
|
22
|
+
config.enabled_negative_rules.any? { |rule| Rules.check_rule(rule, tokens) }
|
23
|
+
end
|
24
|
+
|
25
|
+
def check_string_rules(config, string)
|
26
|
+
check_positive_rules(config, string)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'i18n_linter/linter'
|
4
|
+
require 'i18n_linter/options'
|
5
|
+
require 'i18n_linter/config'
|
6
|
+
|
7
|
+
module I18nLinter
|
8
|
+
class Runner
|
9
|
+
def initialize(options, config)
|
10
|
+
@options = options
|
11
|
+
@linter = I18nLinter.linter.new(options, config)
|
12
|
+
end
|
13
|
+
|
14
|
+
def run
|
15
|
+
$stdout = StringIO.new
|
16
|
+
|
17
|
+
result = @options.files.map { |file|
|
18
|
+
lint_result = lint(file)
|
19
|
+
if lint_result.success?
|
20
|
+
true
|
21
|
+
else
|
22
|
+
@linter.show_errors(lint_result)
|
23
|
+
false
|
24
|
+
end
|
25
|
+
}.all?
|
26
|
+
|
27
|
+
handle_results
|
28
|
+
|
29
|
+
$stdout = STDOUT
|
30
|
+
|
31
|
+
result
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def handle_results
|
37
|
+
output_file = @options.out_file
|
38
|
+
output = $stdout.string
|
39
|
+
|
40
|
+
if output_file
|
41
|
+
File.open(output_file, 'w') do |file|
|
42
|
+
file.write output
|
43
|
+
end
|
44
|
+
else
|
45
|
+
STDOUT.puts output
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def lint(filename)
|
50
|
+
file = File.read(filename)
|
51
|
+
@linter.lint(filename: filename, file: file)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18nLinter
|
4
|
+
class StringLine
|
5
|
+
attr_reader :line_number
|
6
|
+
attr_reader :column_number
|
7
|
+
attr_reader :string
|
8
|
+
|
9
|
+
def initialize(coords, string)
|
10
|
+
@line_number = coords[0]
|
11
|
+
@column_number = coords[1]
|
12
|
+
@string = string
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/i18n_linter.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'i18n_linter/linter'
|
4
|
+
|
5
|
+
require 'i18n_linter/rules'
|
6
|
+
require 'i18n_linter/rules/middle_space'
|
7
|
+
require 'i18n_linter/rules/words'
|
8
|
+
require 'i18n_linter/rules/environment_variable'
|
9
|
+
require 'i18n_linter/rules/class_name'
|
10
|
+
require 'i18n_linter/rules/constant'
|
11
|
+
require 'i18n_linter/rules/logger'
|
12
|
+
require 'i18n_linter/rules/puts'
|
13
|
+
require 'i18n_linter/rules/scope'
|
14
|
+
require 'i18n_linter/rules/strftime'
|
15
|
+
require 'i18n_linter/rules/http_headers'
|
16
|
+
require 'i18n_linter/rules/underscore'
|
17
|
+
require 'i18n_linter/rules/reg_exp'
|
18
|
+
require 'i18n_linter/rules/query'
|
19
|
+
require 'i18n_linter/rules/mime_type'
|
20
|
+
|
21
|
+
require 'i18n_linter/helpers/digger'
|
22
|
+
require 'i18n_linter/options'
|
23
|
+
require 'i18n_linter/constants'
|
24
|
+
require 'i18n_linter/result'
|
25
|
+
require 'i18n_linter/token'
|
26
|
+
require 'i18n_linter/result_set'
|
27
|
+
require 'i18n_linter/string_line'
|
28
|
+
|
29
|
+
require 'ripper'
|
30
|
+
|
31
|
+
module I18nLinter
|
32
|
+
class << self
|
33
|
+
def linter
|
34
|
+
::I18nLinter::Linter
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
metadata
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: i18n_linter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Franco Pariani
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-10-04 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: colorize
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.8.1
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.8.1
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: reek
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '5.2'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '5.2'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.8'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.8'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubocop
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.59.2
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.59.2
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: simplecov
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.13.0
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.13.0
|
83
|
+
description: i18n linter plugin
|
84
|
+
email: franco@rootstrap.com
|
85
|
+
executables:
|
86
|
+
- i18n_linter
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- README.md
|
91
|
+
- config/default.yml
|
92
|
+
- exe/i18n_linter
|
93
|
+
- lib/i18n_linter.rb
|
94
|
+
- lib/i18n_linter/config.rb
|
95
|
+
- lib/i18n_linter/constants.rb
|
96
|
+
- lib/i18n_linter/helpers/digger.rb
|
97
|
+
- lib/i18n_linter/linter.rb
|
98
|
+
- lib/i18n_linter/options.rb
|
99
|
+
- lib/i18n_linter/result.rb
|
100
|
+
- lib/i18n_linter/result_set.rb
|
101
|
+
- lib/i18n_linter/rules.rb
|
102
|
+
- lib/i18n_linter/rules/class_name.rb
|
103
|
+
- lib/i18n_linter/rules/constant.rb
|
104
|
+
- lib/i18n_linter/rules/environment_variable.rb
|
105
|
+
- lib/i18n_linter/rules/http_headers.rb
|
106
|
+
- lib/i18n_linter/rules/logger.rb
|
107
|
+
- lib/i18n_linter/rules/middle_space.rb
|
108
|
+
- lib/i18n_linter/rules/mime_type.rb
|
109
|
+
- lib/i18n_linter/rules/puts.rb
|
110
|
+
- lib/i18n_linter/rules/query.rb
|
111
|
+
- lib/i18n_linter/rules/reg_exp.rb
|
112
|
+
- lib/i18n_linter/rules/scope.rb
|
113
|
+
- lib/i18n_linter/rules/strftime.rb
|
114
|
+
- lib/i18n_linter/rules/underscore.rb
|
115
|
+
- lib/i18n_linter/rules/words.rb
|
116
|
+
- lib/i18n_linter/runner.rb
|
117
|
+
- lib/i18n_linter/string_line.rb
|
118
|
+
- lib/i18n_linter/token.rb
|
119
|
+
- lib/i18n_linter/version.rb
|
120
|
+
homepage: http://rubygems.org/gems/i18n_linter
|
121
|
+
licenses:
|
122
|
+
- MIT
|
123
|
+
metadata: {}
|
124
|
+
post_install_message:
|
125
|
+
rdoc_options: []
|
126
|
+
require_paths:
|
127
|
+
- lib
|
128
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
129
|
+
requirements:
|
130
|
+
- - ">="
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '0'
|
133
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - ">="
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '0'
|
138
|
+
requirements: []
|
139
|
+
rubygems_version: 3.0.4
|
140
|
+
signing_key:
|
141
|
+
specification_version: 4
|
142
|
+
summary: i18n linter plugin
|
143
|
+
test_files: []
|