rubocop-schema-gen 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +1 -1
- data/.rubocop.yml +37 -1
- data/.ruby-version +1 -1
- data/.travis.yml +5 -1
- data/Gemfile +6 -1
- data/LICENSE +13 -0
- data/README.md +20 -25
- data/assets/templates/cop_schema.yml +1 -3
- data/assets/templates/schema.yml +4 -0
- data/bin/console +1 -0
- data/exe/rubocop-schema-gen +4 -2
- data/lib/rubocop/schema.rb +0 -3
- data/lib/rubocop/schema/ascii_doc/base.rb +53 -0
- data/lib/rubocop/schema/ascii_doc/cop.rb +93 -0
- data/lib/rubocop/schema/ascii_doc/department.rb +21 -0
- data/lib/rubocop/schema/ascii_doc/index.rb +20 -0
- data/lib/rubocop/schema/ascii_doc/stringifier.rb +49 -0
- data/lib/rubocop/schema/{cache.rb → cached_http_client.rb} +7 -11
- data/lib/rubocop/schema/cli.rb +55 -25
- data/lib/rubocop/schema/cop_info_merger.rb +54 -0
- data/lib/rubocop/schema/cop_schema.rb +69 -26
- data/lib/rubocop/schema/defaults_ripper.rb +46 -0
- data/lib/rubocop/schema/document_loader.rb +41 -0
- data/lib/rubocop/schema/extension_spec.rb +59 -0
- data/lib/rubocop/schema/generator.rb +93 -0
- data/lib/rubocop/schema/helpers.rb +51 -0
- data/lib/rubocop/schema/value_objects.rb +23 -3
- data/lib/rubocop/schema/version.rb +1 -1
- data/rubocop-schema.gemspec +4 -3
- data/rubocop-schema.json +4174 -3086
- metadata +34 -11
- data/lib/rubocop/schema/lockfile_inspector.rb +0 -51
- data/lib/rubocop/schema/scraper.rb +0 -183
- data/lib/rubocop/schema/templates.rb +0 -8
@@ -4,19 +4,14 @@ require 'net/http'
|
|
4
4
|
|
5
5
|
module RuboCop
|
6
6
|
module Schema
|
7
|
-
class
|
8
|
-
|
9
|
-
attr_reader :base_url
|
10
|
-
|
11
|
-
def initialize(cache_dir, base_url: nil, &event_handler)
|
7
|
+
class CachedHTTPClient
|
8
|
+
def initialize(cache_dir, &event_handler)
|
12
9
|
@cache_dir = Pathname(cache_dir)
|
13
|
-
@base_url = validate_url(base_url)
|
14
10
|
@event_handler = event_handler
|
15
11
|
end
|
16
12
|
|
17
13
|
def get(url)
|
18
14
|
url = URI(url)
|
19
|
-
url = @base_url + url if @base_url && url.relative?
|
20
15
|
validate_url url
|
21
16
|
|
22
17
|
path = path_for_url(url)
|
@@ -24,7 +19,10 @@ module RuboCop
|
|
24
19
|
|
25
20
|
path.parent.mkpath
|
26
21
|
@event_handler&.call Event.new(type: :request)
|
27
|
-
|
22
|
+
|
23
|
+
res = Net::HTTP.get_response(url)
|
24
|
+
res.body = '' unless res.is_a? Net::HTTPOK
|
25
|
+
res.body.force_encoding(Encoding::UTF_8).tap(&path.method(:write))
|
28
26
|
end
|
29
27
|
|
30
28
|
private
|
@@ -34,13 +32,11 @@ module RuboCop
|
|
34
32
|
|
35
33
|
raise ArgumentError, 'Expected an absolute URL' unless url.absolute?
|
36
34
|
raise ArgumentError, 'Expected an HTTP URL' unless url.is_a? URI::HTTP
|
37
|
-
|
38
|
-
url
|
39
35
|
end
|
40
36
|
|
41
37
|
# @param [URI::HTTP] url
|
42
38
|
def path_for_url(url)
|
43
|
-
@cache_dir + url.scheme + url.hostname + url.path[1
|
39
|
+
@cache_dir + url.scheme + url.hostname + url.path[1..-1]
|
44
40
|
end
|
45
41
|
end
|
46
42
|
end
|
data/lib/rubocop/schema/cli.rb
CHANGED
@@ -1,67 +1,97 @@
|
|
1
1
|
require 'pathname'
|
2
2
|
require 'json'
|
3
3
|
|
4
|
+
require 'rubocop/schema/document_loader'
|
5
|
+
require 'rubocop/schema/cached_http_client'
|
6
|
+
require 'rubocop/schema/generator'
|
7
|
+
require 'rubocop/schema/extension_spec'
|
8
|
+
|
4
9
|
module RuboCop
|
5
10
|
module Schema
|
6
11
|
class CLI
|
7
12
|
# @param [Pathname] working_dir
|
8
13
|
# @param [Hash] env
|
9
14
|
# @param [Array<String>] args
|
10
|
-
|
15
|
+
# @param [String] home
|
16
|
+
# @param [IO] out_file
|
17
|
+
# @param [IO] log_file
|
18
|
+
def initialize(working_dir: Dir.pwd, env: ENV, args: ARGV, home: Dir.home, out_file: nil, log_file: $stderr)
|
11
19
|
@working_dir = Pathname(working_dir)
|
20
|
+
@home_dir = Pathname(home)
|
12
21
|
@env = env
|
13
22
|
@args = args
|
23
|
+
@out_file = out_file
|
24
|
+
@log_file = log_file
|
25
|
+
@out_path = args.first
|
26
|
+
|
27
|
+
raise ArgumentError, 'Cannot accept an out_file and an argument' if @out_file && @out_path
|
14
28
|
end
|
15
29
|
|
16
30
|
def run
|
31
|
+
lockfile_path = @working_dir + 'Gemfile.lock'
|
17
32
|
fail "Cannot read #{lockfile_path}" unless lockfile_path.readable?
|
18
|
-
fail 'RuboCop is not part of this project' unless lockfile.specs.any?
|
19
33
|
|
20
|
-
|
21
|
-
|
34
|
+
spec = ExtensionSpec.from_lockfile(lockfile_path)
|
35
|
+
fail 'RuboCop is not part of this project' if spec.empty?
|
36
|
+
|
37
|
+
assign_outfile(spec)
|
38
|
+
print "Generating #{@out_path} … " if @out_path
|
39
|
+
|
40
|
+
schema = report_duration(lowercase: @out_path) { Generator.new(spec.specs, document_loader).schema }
|
41
|
+
@out_file.puts JSON.pretty_generate schema
|
22
42
|
end
|
23
43
|
|
24
44
|
private
|
25
45
|
|
26
|
-
def
|
27
|
-
|
46
|
+
def assign_outfile(spec)
|
47
|
+
return if @out_file
|
48
|
+
|
49
|
+
case @out_path
|
50
|
+
when '-'
|
51
|
+
@out_file = $stdout
|
52
|
+
@out_path = nil
|
53
|
+
when nil
|
54
|
+
@out_path = "#{spec}-config-schema.json"
|
55
|
+
end
|
56
|
+
|
57
|
+
@out_file ||= File.open(@out_path, 'w') # rubocop:disable Naming/MemoizedInstanceVariableName
|
58
|
+
end
|
59
|
+
|
60
|
+
def report_duration(lowercase: false)
|
61
|
+
started = Time.now
|
28
62
|
yield
|
29
63
|
ensure
|
30
64
|
finished = Time.now
|
31
|
-
|
65
|
+
message = "Complete in #{(finished - started).round 1}s"
|
66
|
+
message.downcase! if lowercase
|
67
|
+
handle_event Event.new(message: message)
|
32
68
|
end
|
33
69
|
|
34
70
|
def handle_event(event)
|
35
71
|
case event.type
|
36
72
|
when :request
|
37
|
-
|
73
|
+
@log_file << '.'
|
38
74
|
@line_dirty = true
|
39
75
|
else
|
40
|
-
|
76
|
+
@log_file.puts '' if @line_dirty
|
41
77
|
@line_dirty = false
|
42
|
-
|
78
|
+
@log_file.puts event.message.to_s
|
43
79
|
end
|
44
80
|
end
|
45
81
|
|
46
82
|
def fail(msg)
|
47
|
-
|
83
|
+
@log_file.puts msg.to_s
|
48
84
|
exit 1
|
49
85
|
end
|
50
86
|
|
51
|
-
def
|
52
|
-
@
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
def cache
|
60
|
-
@cache ||= Cache.new(cache_dir, &method(:handle_event))
|
61
|
-
end
|
62
|
-
|
63
|
-
def cache_dir
|
64
|
-
@cache_dir ||= Pathname(Dir.home) + '.rubocop-schema-cache'
|
87
|
+
def document_loader
|
88
|
+
@document_loader ||=
|
89
|
+
DocumentLoader.new(
|
90
|
+
CachedHTTPClient.new(
|
91
|
+
@home_dir + '.rubocop-schema-cache',
|
92
|
+
&method(:handle_event)
|
93
|
+
)
|
94
|
+
)
|
65
95
|
end
|
66
96
|
end
|
67
97
|
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module RuboCop
|
2
|
+
module Schema
|
3
|
+
class CopInfoMerger
|
4
|
+
# @param [CopInfo] old
|
5
|
+
# @param [CopInfo] new
|
6
|
+
# @return [CopInfo]
|
7
|
+
def self.merge(old, new)
|
8
|
+
new(old, new).merged
|
9
|
+
end
|
10
|
+
|
11
|
+
# @return [CopInfo]
|
12
|
+
attr_reader :merged
|
13
|
+
|
14
|
+
def initialize(old, new)
|
15
|
+
@merged = old.dup
|
16
|
+
@new = new
|
17
|
+
merge
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def merge
|
23
|
+
@merged.supports_autocorrect = @new.supports_autocorrect if @merged.supports_autocorrect.nil?
|
24
|
+
@merged.enabled_by_default = @new.enabled_by_default if @merged.enabled_by_default.nil?
|
25
|
+
@merged.attributes = merge_attribute_sets(@merged.attributes, @new.attributes)
|
26
|
+
@merged.description ||= @new.description
|
27
|
+
end
|
28
|
+
|
29
|
+
# @param [Array<Attribute>] old
|
30
|
+
# @param [Array<Attribute>] new
|
31
|
+
# @return [Array<Attribute>]
|
32
|
+
def merge_attribute_sets(old, new)
|
33
|
+
return old || new unless old && new
|
34
|
+
|
35
|
+
merged = old.map { |attr| [attr.name, attr] }.to_h
|
36
|
+
new.each do |attr|
|
37
|
+
merged[attr.name] = merged.key?(attr.name) ? merge_attributes(merged[attr.name], attr) : attr
|
38
|
+
end
|
39
|
+
|
40
|
+
merged.values
|
41
|
+
end
|
42
|
+
|
43
|
+
# @param [Attribute] old
|
44
|
+
# @param [Attribute] new
|
45
|
+
# @return [Attribute]
|
46
|
+
def merge_attributes(old, new)
|
47
|
+
old.dup.tap do |merged|
|
48
|
+
merged.type ||= new.type
|
49
|
+
merged.default ||= new.default
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -1,40 +1,83 @@
|
|
1
|
+
require 'rubocop/schema/helpers'
|
2
|
+
|
1
3
|
module RuboCop
|
2
4
|
module Schema
|
3
5
|
class CopSchema
|
4
|
-
|
5
|
-
|
6
|
+
include Helpers
|
7
|
+
|
8
|
+
KNOWN_TYPES = {
|
9
|
+
'boolean' => 'boolean',
|
10
|
+
'integer' => 'integer',
|
11
|
+
'array' => 'array',
|
12
|
+
'string' => 'string',
|
13
|
+
'float' => 'number'
|
14
|
+
}.freeze
|
6
15
|
|
7
|
-
# @param [Class<RuboCop::Cop::Base>] cop
|
8
16
|
# @param [CopInfo] info
|
9
|
-
def initialize(
|
10
|
-
|
17
|
+
def initialize(info)
|
18
|
+
@info = info.dup.freeze
|
19
|
+
@json = template('cop_schema')
|
20
|
+
generate
|
21
|
+
end
|
11
22
|
|
12
|
-
|
13
|
-
@
|
23
|
+
def as_json
|
24
|
+
@json
|
14
25
|
end
|
15
26
|
|
16
|
-
|
27
|
+
alias to_h as_json
|
17
28
|
|
18
|
-
def
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
29
|
+
def freeze
|
30
|
+
@json.freeze
|
31
|
+
super
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# @return Hash
|
37
|
+
attr_reader :json
|
38
|
+
|
39
|
+
# @return CopInfo
|
40
|
+
attr_reader :info
|
41
|
+
|
42
|
+
def props
|
43
|
+
json['properties']
|
44
|
+
end
|
45
|
+
|
46
|
+
def generate
|
47
|
+
json['description'] = info.description unless info.description.nil?
|
48
|
+
assign_default_attributes
|
49
|
+
info.attributes&.each do |attr|
|
50
|
+
prop = props[attr.name] ||= {}
|
51
|
+
assign_attribute_type prop, attr
|
52
|
+
assign_attribute_description prop, attr
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def assign_default_attributes
|
57
|
+
props['AutoCorrect'] = boolean if info.supports_autocorrect
|
58
|
+
props['Enabled']['description'] = "Default: #{info.enabled_by_default}" if info.enabled_by_default
|
59
|
+
end
|
60
|
+
|
61
|
+
# @param [Hash] prop
|
62
|
+
# @param [Attribute] attr
|
63
|
+
def assign_attribute_type(prop, attr)
|
64
|
+
if KNOWN_TYPES.key? attr.type&.downcase
|
65
|
+
prop['type'] ||= KNOWN_TYPES[attr.type.downcase] unless prop.key? '$ref'
|
66
|
+
elsif attr.type
|
67
|
+
prop['enum'] = attr.type.split(/\s*,\s*/)
|
36
68
|
end
|
37
69
|
end
|
70
|
+
|
71
|
+
# @param [Hash] prop
|
72
|
+
# @param [Attribute] attr
|
73
|
+
def assign_attribute_description(prop, attr)
|
74
|
+
prop['description'] = format_default(attr.default) unless attr.default.nil?
|
75
|
+
end
|
76
|
+
|
77
|
+
def format_default(default)
|
78
|
+
default = default.join(', ') if default.is_a? Array
|
79
|
+
"Default: #{default}"
|
80
|
+
end
|
38
81
|
end
|
39
82
|
end
|
40
83
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'rubocop/schema/value_objects'
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Schema
|
5
|
+
class DefaultsRipper
|
6
|
+
EXCLUDE_ATTRIBUTES = Set.new(%w[Description VersionAdded VersionChanged StyleGuide]).freeze
|
7
|
+
|
8
|
+
TYPE_MAP = {
|
9
|
+
integer: [Integer],
|
10
|
+
number: [Float],
|
11
|
+
boolean: [TrueClass, FalseClass],
|
12
|
+
string: [String],
|
13
|
+
array: [Array]
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
# @return [Array<CopInfo>]
|
17
|
+
attr_reader :cops
|
18
|
+
|
19
|
+
# @param [Hash] defaults
|
20
|
+
def initialize(defaults)
|
21
|
+
@cops = defaults.map do |cop_name, attributes|
|
22
|
+
CopInfo.new(
|
23
|
+
name: cop_name,
|
24
|
+
description: attributes['Description'],
|
25
|
+
enabled_by_default: attributes['Enabled'] == true,
|
26
|
+
attributes: transform_attributes(attributes)
|
27
|
+
)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def transform_attributes(hash)
|
34
|
+
hash.map do |name, default|
|
35
|
+
next if EXCLUDE_ATTRIBUTES.include? name
|
36
|
+
|
37
|
+
Attribute.new(
|
38
|
+
name: name,
|
39
|
+
type: TYPE_MAP.find { |_, v| v.any? { |c| default.is_a? c } }&.first&.to_s,
|
40
|
+
default: default
|
41
|
+
)
|
42
|
+
end.compact
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module RuboCop
|
2
|
+
module Schema
|
3
|
+
class DocumentLoader
|
4
|
+
DOCS_URL_TEMPLATE =
|
5
|
+
-'https://raw.githubusercontent.com/rubocop/%s/v%s/docs/modules/ROOT/pages/cops%s.adoc'
|
6
|
+
DEFAULTS_URL_TEMPLATE =
|
7
|
+
-'https://raw.githubusercontent.com/rubocop/%s/v%s/config/default.yml'
|
8
|
+
|
9
|
+
# @param [CachedHTTPClient] http_client
|
10
|
+
def initialize(http_client)
|
11
|
+
@http_client = http_client
|
12
|
+
@docs = {}
|
13
|
+
@defaults = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
# @param [Spec] spec
|
17
|
+
def defaults(spec)
|
18
|
+
@defaults[spec] ||=
|
19
|
+
YAML.safe_load @http_client.get(url_for_defaults(spec)), [Regexp, Symbol]
|
20
|
+
end
|
21
|
+
|
22
|
+
# @param [Spec] spec
|
23
|
+
# @param [String] department
|
24
|
+
# @return [Asciidoctor::Document]
|
25
|
+
def doc(spec, department = nil)
|
26
|
+
@docs[[spec, department]] ||=
|
27
|
+
Asciidoctor.load @http_client.get url_for_doc(spec, department)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def url_for_doc(spec, department)
|
33
|
+
format DOCS_URL_TEMPLATE, spec.name, spec.version, department && "_#{department.to_s.downcase}"
|
34
|
+
end
|
35
|
+
|
36
|
+
def url_for_defaults(spec)
|
37
|
+
format DEFAULTS_URL_TEMPLATE, spec.name, spec.version
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'rubocop/schema/value_objects'
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Schema
|
5
|
+
class ExtensionSpec
|
6
|
+
KNOWN_CLASSES = Set.new(
|
7
|
+
%w[
|
8
|
+
RuboCop
|
9
|
+
RuboCop::Rake
|
10
|
+
RuboCop::RSpec
|
11
|
+
RuboCop::Minitest
|
12
|
+
RuboCop::Performance
|
13
|
+
RuboCop::Rails
|
14
|
+
]
|
15
|
+
).freeze
|
16
|
+
|
17
|
+
KNOWN_GEMS = Set.new(['rubocop', *KNOWN_CLASSES.map { |e| e.sub('::', '-').downcase }]).freeze
|
18
|
+
|
19
|
+
def self.internal
|
20
|
+
@internal ||= new(KNOWN_CLASSES.map do |klass_name|
|
21
|
+
next unless Object.const_defined? klass_name
|
22
|
+
|
23
|
+
klass = Object.const_get(klass_name)
|
24
|
+
Spec.new(
|
25
|
+
name: klass.name.sub('::', '-').downcase,
|
26
|
+
version: (defined?(klass::VERSION) ? klass::VERSION : klass::Version::STRING)
|
27
|
+
)
|
28
|
+
end.compact)
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.from_lockfile(lockfile)
|
32
|
+
new(Bundler::LockfileParser.new(lockfile.to_s).sources.flat_map do |source|
|
33
|
+
source.specs.map do |stub|
|
34
|
+
next unless KNOWN_GEMS.include? stub.name
|
35
|
+
|
36
|
+
Spec.new(
|
37
|
+
name: stub.name,
|
38
|
+
version: stub.version.to_s
|
39
|
+
)
|
40
|
+
end.compact
|
41
|
+
end)
|
42
|
+
end
|
43
|
+
|
44
|
+
attr_reader :specs
|
45
|
+
|
46
|
+
def initialize(specs)
|
47
|
+
@specs = specs.dup.sort_by(&:name).freeze
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_s
|
51
|
+
@specs.join '-'
|
52
|
+
end
|
53
|
+
|
54
|
+
def empty?
|
55
|
+
@specs.empty?
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|