goodcheck 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,200 @@
1
+ # Goodcheck - Regexp based customizable linter
2
+
3
+ Are you reviewing a pull request if the change contains deprecated API calls?
4
+ Do you want to post a comment to ask the developer if a method call satisfies some condition to use that without causing an issue?
5
+ What if a misspelling like `Github` for `GitHub` can be found automatically?
6
+
7
+ Give Goodcheck a try to do them instead of you! 🎉
8
+
9
+ Goodcheck is a customizable linter.
10
+ You can define pairs of patterns and messages.
11
+ It checks your program and when it detects a piece of text matching with the defined patterns, it prints your message which tells your teammates why it should be revised and how.
12
+ Some part of code reviewing process can be automated.
13
+ Everything you have to do is to define the rules, pairs of patterns and messages, and nothing will bother you. 😆
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ $ gem install goodcheck
19
+ ```
20
+
21
+ Or you can use `bundler`!
22
+
23
+ ## Quickstart
24
+
25
+ ```bash
26
+ $ goodcheck init
27
+ $ vim goodcheck.yml
28
+ $ goodcheck check
29
+ ```
30
+
31
+ The `init` command generates template of `goodcheck.yml` configuration file for you.
32
+ Edit the config file to define patterns you want to check.
33
+ Then run `check` command, and it will print matched texts.
34
+
35
+ ## `goodcheck.yml`
36
+
37
+ An example of configuration is like the following:
38
+
39
+ ```yaml
40
+ rules:
41
+ - id: com.example.github
42
+ pattern: Github
43
+ message: |
44
+ GitHub is GitHub, not Github
45
+
46
+ You may misspelling the name of the service!
47
+ justifications:
48
+ - When you mean a service different from GitHub
49
+ - When GitHub is renamed
50
+ glob:
51
+ - app/views/**/*.html.slim
52
+ - config/locales/**/*.yaml
53
+ pass:
54
+ - <a>Signup via GitHub</a>
55
+ fail:
56
+ - <a>Signup via Github</a>
57
+ ```
58
+
59
+ The *rule* hash contains the following keys.
60
+
61
+ * `id`: a string to identify rules (required)
62
+ * `pattern`: a *pattern* or a sequence of *pattern*s (required)
63
+ * `message`: a string to tell writers why the code piece should be revised (required)
64
+ * `justification`: a sequence of strings to tell writers when a exception can be allowed (optional)
65
+ * `glob`: a *glob* or a sequence of *glob*s (optional)
66
+ * `pass`: a string, or a sequence of strings, which does not match given pattern (optional)
67
+ * `fail`: a string, or a sequence of strings, which does match given pattern (optional)
68
+
69
+ ### *pattern*
70
+
71
+ A *pattern* can be a *literal pattern*, *regexp pattern*, *token pattern*, or a string.
72
+ When a string is given, it is interpreted as a *literal pattern* with `case_insensitive: false`.
73
+
74
+ #### *literal pattern*
75
+
76
+ *literal pattern* allows you to construct a regexp which matches exactly to the `literal` string.
77
+
78
+ ```yaml
79
+ id: com.sample.GitHub
80
+ pattern:
81
+ literal: Github
82
+ case_insensitive: false
83
+ message: Write GitHub, not Github
84
+ ```
85
+
86
+ All regexp meta characters included in the `literal` value will be escaped.
87
+ `case_insensitive` is an optional key and the default is `false`.
88
+
89
+ #### *regexp pattern*
90
+
91
+ *regexp pattern* allows you to write a regexp with meta chars.
92
+
93
+ ```yaml
94
+ id: com.sample.digits
95
+ pattern:
96
+ regexp: \d{4,}
97
+ case_insensitive: true
98
+ multiline: false
99
+ message: Insert delimiters when writing large numbers
100
+ justification:
101
+ - When you are not writing numbers, including phone numbers, zip code, ...
102
+ ```
103
+
104
+ It accepts two optional attributes, `case_insensitive` and `multiline`.
105
+ The default value of `case_insensitive` and `multiline` are `true` and `false` correspondingly.
106
+
107
+ The regexp will be passed to `Regexp.compile`.
108
+ The precise definition of regular expression can be found in the documentation for Ruby.
109
+
110
+ #### *token pattern*
111
+
112
+ *token pattern* compiles to a *tokenized* regexp.
113
+
114
+ ```yaml
115
+ id: com.sample.no-blink
116
+ pattern:
117
+ token: "<blink"
118
+ message: Stop using <blink> tag
119
+ glob: "**/*.html"
120
+ justifications:
121
+ - If Lynx is the major target of the web site
122
+ ```
123
+
124
+ It tries to tokenize the input and generates a regexp which matches sequence of tokens.
125
+ The tokenization is heuristic and may not work well for your programming language.
126
+ In that case, try using *regexp pattern*.
127
+
128
+ The generated regexp of `<blink` is `<\s*blink\b`.
129
+ It matches with `<blink />` and `< BLINK>`, but does not match with `https://www.chromium.org/blink`.
130
+
131
+ ### *glob*
132
+
133
+ A *glob* can be a string, or a hash.
134
+
135
+ ```yaml
136
+ glob:
137
+ pattern: "legacy/**/*.rb"
138
+ encoding: EUC-JP
139
+ ```
140
+
141
+ The hash can have an optional `encoding` attribute.
142
+ You can specify encoding of the file by the names defined for ruby.
143
+ The list of all available encoding names can be found by `$ ruby -e "puts Encoding.name_list"`.
144
+ The default value is `UTF-8`.
145
+
146
+ If you write a string as a `glob`, the string value can be the `pattern` of the glob, without `encoding` attribute.
147
+
148
+ If you omit `glob` attribute in a rule, the rule will be applied to all files given to `goodcheck`.
149
+
150
+ ## Commands
151
+
152
+ ### `goodcheck init [options]`
153
+
154
+ The `init` command generates an example of configuration file.
155
+
156
+ Available options are:
157
+
158
+ * `-c=[CONFIG]`, `--config=[CONFIG]` to specify the configuration file name to generate.
159
+ * `--force` to allow overwriting existing config file.
160
+
161
+ ### `goodcheck check [options] targets...`
162
+
163
+ The `check` command checks your programs under `targets...`.
164
+ You can pass:
165
+
166
+ * Directory paths, or
167
+ * Paths to files.
168
+
169
+ When you omit `targets`, it checks all files in `.`.
170
+
171
+ Available options are:
172
+
173
+ * `-c [CONFIG]`, `--config=[CONFIG]` to specify the configuration file.
174
+ * `-R [rule]`, `--rule=[rule]` to specify the rules you want to check.
175
+ * `--format=[text|json]` to specify output format.
176
+
177
+ ### `goodcheck test [options]`
178
+
179
+ The `test` command tests rules.
180
+ The test contains:
181
+
182
+ * Validation of rule `id` uniqueness.
183
+ * If `pass` examples does not match with any of `pattern`s.
184
+ * If `fail` examples matches with some of `pattern`s.
185
+
186
+ Use `test` command when you add new rule to be sure you are writing rules correctly.
187
+
188
+ Available options is:
189
+
190
+ * `-c [CONFIG]`, `--config=[CONFIG]` to specify the configuration file.
191
+
192
+ ## Development
193
+
194
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
195
+
196
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
197
+
198
+ ## Contributing
199
+
200
+ Bug reports and pull requests are welcome on GitHub at https://github.com/sideci/goodcheck.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "goodcheck"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/exe/goodcheck ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH << File.join(__dir__, "../lib")
4
+ require "goodcheck"
5
+ require "goodcheck/cli"
6
+
7
+ exit Goodcheck::CLI.new(stdout: STDOUT, stderr: STDERR).run(ARGV.dup)
data/goodcheck.gemspec ADDED
@@ -0,0 +1,30 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "goodcheck/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "goodcheck"
8
+ spec.version = Goodcheck::VERSION
9
+ spec.authors = ["Soutaro Matsumoto"]
10
+ spec.email = ["matsumoto@soutaro.com"]
11
+
12
+ spec.summary = "Regexp based customizable linter"
13
+ spec.description = "Regexp based customizable linter"
14
+ spec.homepage = "https://github.com/sideci/goodcheck"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
+ f.match(%r{^(test|spec|features)/})
18
+ end
19
+ spec.bindir = "exe"
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.16"
24
+ spec.add_development_dependency "rake", "~> 10.0"
25
+ spec.add_development_dependency "minitest", "~> 5.0"
26
+
27
+ spec.add_runtime_dependency "activesupport", "~> 5.0"
28
+ spec.add_runtime_dependency "strong_json", "~> 0.5.0"
29
+ spec.add_runtime_dependency "rainbow", "~> 3.0.0"
30
+ end
data/lib/goodcheck.rb ADDED
@@ -0,0 +1,28 @@
1
+ require "strscan"
2
+ require "pathname"
3
+ require "strong_json"
4
+ require "yaml"
5
+ require "json"
6
+ require "active_support/core_ext/hash/indifferent_access"
7
+ require "active_support/core_ext/integer/inflections"
8
+ require "rainbow"
9
+
10
+ require "goodcheck/version"
11
+
12
+ require "goodcheck/glob"
13
+ require "goodcheck/buffer"
14
+ require "goodcheck/location"
15
+ require "goodcheck/reporters/text"
16
+ require "goodcheck/reporters/json"
17
+ require "goodcheck/array_helper"
18
+ require "goodcheck/analyzer"
19
+ require "goodcheck/issue"
20
+ require "goodcheck/rule"
21
+ require "goodcheck/matcher"
22
+ require "goodcheck/pattern"
23
+ require "goodcheck/config"
24
+ require "goodcheck/config_loader"
25
+ require "goodcheck/commands/config_loading"
26
+ require "goodcheck/commands/check"
27
+ require "goodcheck/commands/init"
28
+ require "goodcheck/commands/test"
@@ -0,0 +1,46 @@
1
+ module Goodcheck
2
+ class Analyzer
3
+ attr_reader :rule
4
+ attr_reader :buffer
5
+
6
+ def initialize(rule:, buffer:)
7
+ @rule = rule
8
+ @buffer = buffer
9
+ end
10
+
11
+ def scan(&block)
12
+ if block_given?
13
+ issues = []
14
+
15
+ rule.patterns.each do |pattern|
16
+ scanner = StringScanner.new(buffer.content)
17
+
18
+ break_head = pattern.regexp.source.start_with?("\\b")
19
+ after_break = true
20
+
21
+ until scanner.eos?
22
+ case
23
+ when scanner.scan(pattern.regexp)
24
+ next if break_head && !after_break
25
+
26
+ text = scanner.matched
27
+ range = (scanner.pos - text.bytesize) .. scanner.pos
28
+ unless issues.any? {|issue| issue.range == range }
29
+ issues << Issue.new(buffer: buffer, range: range, rule: rule, text: text)
30
+ end
31
+ when scanner.scan(/.\b/m)
32
+ after_break = true
33
+ else
34
+ scanner.scan(/./m)
35
+ after_break = false
36
+ end
37
+ end
38
+ end
39
+
40
+ issues.each(&block)
41
+ else
42
+ enum_for(:scan, &block)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,12 @@
1
+ module Goodcheck
2
+ module ArrayHelper
3
+ def array(obj)
4
+ case obj
5
+ when Hash
6
+ [obj]
7
+ else
8
+ Array(obj)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,50 @@
1
+ module Goodcheck
2
+ class Buffer
3
+ attr_reader :path
4
+ attr_reader :content
5
+
6
+ def initialize(path:, content:)
7
+ @path = path
8
+ @content = content
9
+ end
10
+
11
+ def line_starts
12
+ unless @line_starts
13
+ @line_starts = []
14
+
15
+ start_position = 0
16
+
17
+ content.lines.each do |line|
18
+ range = start_position..(start_position + line.bytesize)
19
+ @line_starts << range
20
+ start_position = range.end
21
+ end
22
+ end
23
+
24
+ @line_starts
25
+ end
26
+
27
+ def location_for_position(position)
28
+ line_index = line_starts.bsearch_index do |range|
29
+ position < range.end
30
+ end
31
+
32
+ if line_index
33
+ [line_index + 1, position - line_starts[line_index].begin]
34
+ end
35
+ end
36
+
37
+ def line(line_number)
38
+ content.lines[line_number-1]
39
+ end
40
+
41
+ def position_for_location(line, column)
42
+ if (range = line_starts[line-1])
43
+ pos = range.begin + column
44
+ if pos < range.end
45
+ pos
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,112 @@
1
+ require "optparse"
2
+
3
+ module Goodcheck
4
+ class CLI
5
+ attr_reader :stdout
6
+ attr_reader :stderr
7
+
8
+ def initialize(stdout:, stderr:)
9
+ @stdout = stdout
10
+ @stderr = stderr
11
+ end
12
+
13
+ COMMANDS = {
14
+ init: "Generate a sample configuration file",
15
+ check: "Run check with a configuration",
16
+ test: "Test your configuration",
17
+ help: "Print this message"
18
+ }
19
+
20
+
21
+ def run(args)
22
+ command = args.shift&.to_sym
23
+
24
+ if COMMANDS.key?(command)
25
+ __send__(command, args)
26
+ else
27
+ help(args)
28
+ end
29
+ rescue => exn
30
+ stderr.puts exn.inspect
31
+ exn.backtrace.each do |bt|
32
+ stderr.puts " #{bt}"
33
+ end
34
+ 1
35
+ end
36
+
37
+ def check(args)
38
+ config_path = Pathname("goodcheck.yml")
39
+ targets = []
40
+ rules = []
41
+ format = nil
42
+
43
+ OptionParser.new("Usage: goodcheck check [options] dirs...") do |opts|
44
+ opts.on("-c CONFIG", "--config=CONFIG") do |config|
45
+ config_path = Pathname(config)
46
+ end
47
+ opts.on("-R RULE", "--rule=RULE") do |rule|
48
+ rules << rule
49
+ end
50
+ opts.on("--format=FORMAT") do |f|
51
+ format = f
52
+ end
53
+ end.parse!(args)
54
+
55
+ if args.empty?
56
+ targets << Pathname(".")
57
+ else
58
+ targets.push *args.map {|arg| Pathname(arg) }
59
+ end
60
+
61
+ reporter = case format
62
+ when "text", nil
63
+ Reporters::Text.new(stdout: stdout)
64
+ when "json"
65
+ Reporters::JSON.new(stdout: stdout, stderr: stderr)
66
+ else
67
+ stderr.puts "Unknown format: #{format}"
68
+ return 1
69
+ end
70
+
71
+ Commands::Check.new(reporter: reporter, config_path: config_path, rules: rules, targets: targets, stderr: stderr).run
72
+ end
73
+
74
+ def test(args)
75
+ config_path = Pathname("goodcheck.yml")
76
+
77
+ OptionParser.new("Usage: goodcheck test [options]") do |opts|
78
+ opts.on("-c CONFIG", "--config=CONFIG") do |config|
79
+ config_path = Pathname(config)
80
+ end
81
+ end.parse!(args)
82
+
83
+ Commands::Test.new(stdout: stdout, stderr: stderr, config_path: config_path).run
84
+ end
85
+
86
+ def init(args)
87
+ config_path = Pathname("goodcheck.yml")
88
+ force = false
89
+
90
+ OptionParser.new("Usage: goodcheck init [options]") do |opts|
91
+ opts.on("-c CONFIG", "--config=CONFIG") do |config|
92
+ config_path = Pathname(config)
93
+ end
94
+ opts.on("--force") do
95
+ force = true
96
+ end
97
+ end.parse!(args)
98
+
99
+ Commands::Init.new(stdout: stdout, stderr: stderr, path: config_path, force: force).run
100
+ end
101
+
102
+ def help(args)
103
+ stdout.puts "Usage: goodcheck <command> [options] [args...]"
104
+ stdout.puts ""
105
+ stdout.puts "Commands:"
106
+ COMMANDS.each do |c, msg|
107
+ stdout.puts " goodcheck #{c}\t#{msg}"
108
+ end
109
+ 0
110
+ end
111
+ end
112
+ end