goodcheck 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/.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
|