rubocop-schema-gen 0.1.0 → 0.1.1
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 +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
|