goodcheck 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +38 -0
- data/LICENSE +661 -0
- data/README.md +200 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/goodcheck +7 -0
- data/goodcheck.gemspec +30 -0
- data/lib/goodcheck.rb +28 -0
- data/lib/goodcheck/analyzer.rb +46 -0
- data/lib/goodcheck/array_helper.rb +12 -0
- data/lib/goodcheck/buffer.rb +50 -0
- data/lib/goodcheck/cli.rb +112 -0
- data/lib/goodcheck/commands/check.rb +89 -0
- data/lib/goodcheck/commands/config_loading.rb +13 -0
- data/lib/goodcheck/commands/init.rb +48 -0
- data/lib/goodcheck/commands/test.rb +95 -0
- data/lib/goodcheck/config.rb +28 -0
- data/lib/goodcheck/config_loader.rb +90 -0
- data/lib/goodcheck/glob.rb +11 -0
- data/lib/goodcheck/issue.rb +29 -0
- data/lib/goodcheck/location.rb +23 -0
- data/lib/goodcheck/matcher.rb +21 -0
- data/lib/goodcheck/pattern.rb +57 -0
- data/lib/goodcheck/reporters/json.rb +50 -0
- data/lib/goodcheck/reporters/text.rb +34 -0
- data/lib/goodcheck/rule.rb +21 -0
- data/lib/goodcheck/version.rb +3 -0
- data/sample.yml +28 -0
- metadata +161 -0
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
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
data/exe/goodcheck
ADDED
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,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
|