i18n_linter 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
+ [![CircleCI](https://circleci.com/gh/rootstrap/i18n_linter/tree/master.svg?style=svg&circle-token=15c1ee79b304665b14e47e04dc7577c715de293c)](https://circleci.com/gh/rootstrap/i18n_linter/tree/master)
4
+ [![Maintainability](https://api.codeclimate.com/v1/badges/50f3ec30e8147c3df712/maintainability)](https://codeclimate.com/repos/5c6b0bbdd561465d35008579/maintainability)
5
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/50f3ec30e8147c3df712/test_coverage)](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)
@@ -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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module I18nLinter
4
+ module Rules
5
+ class MiddleSpace
6
+ def check(string)
7
+ /.\s+./ =~ string
8
+ end
9
+ end
10
+ end
11
+ 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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module I18nLinter
4
+ module Rules
5
+ class Query
6
+ def check(tokens)
7
+ tokens[0] == :method_add_arg &&
8
+ I18nLinter::Digger.new(:@ident).find(I18nLinter::Constants::QUERY_METHODS, tokens[1])
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module I18nLinter
4
+ module Rules
5
+ class RegExp
6
+ def check(tokens)
7
+ tokens[0] == :regexp_literal
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module I18nLinter
4
+ module Rules
5
+ class Scope
6
+ SCOPE_IDENT = 'scope'
7
+
8
+ def check(tokens)
9
+ tokens[0] == :command && I18nLinter::Digger.new(:@ident).find([SCOPE_IDENT], tokens[1])
10
+ end
11
+ end
12
+ end
13
+ 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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module I18nLinter
4
+ module Rules
5
+ class Words
6
+ def check(string)
7
+ /^[A-Z].*[a-z]/ =~ string.strip
8
+ end
9
+ end
10
+ end
11
+ 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
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module I18nLinter
4
+ class Token
5
+ attr_reader :coords
6
+ attr_reader :type
7
+ attr_reader :content
8
+
9
+ def initialize(token)
10
+ @coords = token[0]
11
+ @type = token[1]
12
+ @content = token[2]
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module I18nLinter
4
+ VERSION = '0.1.0'
5
+ end
@@ -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: []