rubocop-schema-gen 0.1.0 → 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- 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/exe/rubocop-schema-gen +4 -2
- data/lib/rubocop/schema.rb +0 -4
- data/lib/rubocop/schema/ascii_doc/base.rb +52 -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} +9 -12
- data/lib/rubocop/schema/cli.rb +94 -26
- 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 +48 -0
- data/lib/rubocop/schema/diff.rb +67 -0
- data/lib/rubocop/schema/document_loader.rb +55 -0
- data/lib/rubocop/schema/extension_spec.rb +67 -0
- data/lib/rubocop/schema/generator.rb +91 -0
- data/lib/rubocop/schema/helpers.rb +62 -0
- data/lib/rubocop/schema/repo.rb +91 -0
- data/lib/rubocop/schema/value_objects.rb +29 -3
- data/lib/rubocop/schema/version.rb +1 -1
- metadata +20 -36
- data/.gitignore +0 -19
- data/.rspec +0 -3
- data/.rubocop.yml +0 -38
- data/.ruby-version +0 -1
- data/.travis.yml +0 -6
- data/CODE_OF_CONDUCT.md +0 -74
- data/Gemfile +0 -10
- data/LICENSE.txt +0 -21
- data/Rakefile +0 -6
- data/bin/console +0 -14
- data/bin/setup +0 -8
- 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
- data/rubocop-schema.gemspec +0 -31
- data/rubocop-schema.json +0 -21110
@@ -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,48 @@
|
|
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
|
+
next unless attributes.is_a? Hash
|
23
|
+
|
24
|
+
CopInfo.new(
|
25
|
+
name: cop_name,
|
26
|
+
description: attributes['Description'],
|
27
|
+
enabled_by_default: attributes['Enabled'] == true,
|
28
|
+
attributes: transform_attributes(attributes)
|
29
|
+
)
|
30
|
+
end.compact
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def transform_attributes(hash)
|
36
|
+
hash.map do |name, default|
|
37
|
+
next if EXCLUDE_ATTRIBUTES.include? name
|
38
|
+
|
39
|
+
Attribute.new(
|
40
|
+
name: name,
|
41
|
+
type: TYPE_MAP.find { |_, v| v.any? { |c| default.is_a? c } }&.first&.to_s,
|
42
|
+
default: default
|
43
|
+
)
|
44
|
+
end.compact
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'rubocop/schema/helpers'
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Schema
|
5
|
+
class Diff
|
6
|
+
include Helpers
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def instance
|
10
|
+
@instance ||= new
|
11
|
+
end
|
12
|
+
|
13
|
+
def diff(old, new)
|
14
|
+
instance.diff old, new
|
15
|
+
end
|
16
|
+
|
17
|
+
def apply(old, diff)
|
18
|
+
instance.apply old, diff
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def diff(old, new)
|
23
|
+
return diff_hashes old, new if old.is_a?(Hash) && new.is_a?(Hash)
|
24
|
+
|
25
|
+
new
|
26
|
+
end
|
27
|
+
|
28
|
+
def apply(old, diff)
|
29
|
+
return apply_hash(old, diff) if old.is_a?(Hash) && diff.is_a?(Hash)
|
30
|
+
|
31
|
+
diff
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def diff_hashes(old, new)
|
37
|
+
(old.keys - new.keys).map { |k| [k, nil] }.to_h.tap do |result|
|
38
|
+
new.each do |k, v|
|
39
|
+
if old.key? k
|
40
|
+
result[k] = diff(old[k], v) unless old[k] == v
|
41
|
+
else
|
42
|
+
result[k] = v
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def apply_hash(old, diff)
|
49
|
+
deep_dup(old).tap do |result|
|
50
|
+
diff.each do |k, v|
|
51
|
+
apply_hash_pair result, k, v
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def apply_hash_pair(hash, key, value)
|
57
|
+
if value.nil?
|
58
|
+
hash.delete key
|
59
|
+
elsif hash.key? key
|
60
|
+
hash[key] = apply(hash[key], value)
|
61
|
+
else
|
62
|
+
hash[key] = value
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'asciidoctor'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module RuboCop
|
5
|
+
module Schema
|
6
|
+
class DocumentLoader
|
7
|
+
DOCS_URL_TEMPLATE =
|
8
|
+
-'https://raw.githubusercontent.com/rubocop/%s/%s/docs/modules/ROOT/pages/cops%s.adoc'
|
9
|
+
DEFAULTS_URL_TEMPLATE =
|
10
|
+
-'https://raw.githubusercontent.com/rubocop/%s/%s/config/default.yml'
|
11
|
+
|
12
|
+
CORRECTIONS = {
|
13
|
+
'rubocop' => {
|
14
|
+
# Fixes a typo that causes Asciidoctor to crash
|
15
|
+
'1.10.0' => '174bda389c2c23cffb17e9d6128f5e6bdbc0e8a0'
|
16
|
+
}
|
17
|
+
}.freeze
|
18
|
+
|
19
|
+
# @param [CachedHTTPClient] http_client
|
20
|
+
def initialize(http_client)
|
21
|
+
@http_client = http_client
|
22
|
+
@docs = {}
|
23
|
+
@defaults = {}
|
24
|
+
end
|
25
|
+
|
26
|
+
# @param [Spec] spec
|
27
|
+
def defaults(spec)
|
28
|
+
@defaults[spec] ||=
|
29
|
+
YAML.safe_load @http_client.get(url_for_defaults(spec)), [Regexp, Symbol]
|
30
|
+
end
|
31
|
+
|
32
|
+
# @param [Spec] spec
|
33
|
+
# @param [String] department
|
34
|
+
# @return [Asciidoctor::Document]
|
35
|
+
def doc(spec, department = nil)
|
36
|
+
@docs[[spec, department]] ||=
|
37
|
+
Asciidoctor.load @http_client.get url_for_doc(spec, department)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def url_for_doc(spec, department)
|
43
|
+
format DOCS_URL_TEMPLATE, spec.name, correct_version(spec), department && "_#{department.to_s.downcase}"
|
44
|
+
end
|
45
|
+
|
46
|
+
def url_for_defaults(spec)
|
47
|
+
format DEFAULTS_URL_TEMPLATE, spec.name, correct_version(spec)
|
48
|
+
end
|
49
|
+
|
50
|
+
def correct_version(spec)
|
51
|
+
CORRECTIONS.dig(spec.name, spec.version) || "v#{spec.version}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,67 @@
|
|
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
|
+
# @param [Pathname] lockfile
|
32
|
+
def self.from_lockfile(lockfile)
|
33
|
+
new(lockfile.readlines.map do |line|
|
34
|
+
next unless line =~ /\A\s+(rubocop(?:-\w+)?) \((\d+(?:\.\d+)+)\)\s*\z/
|
35
|
+
next unless KNOWN_GEMS.include? $1
|
36
|
+
|
37
|
+
Spec.new(name: $1, version: $2)
|
38
|
+
end.compact)
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.from_string(string)
|
42
|
+
new(string.split('-').each_slice(2).map do |(name, version)|
|
43
|
+
name = "rubocop-#{name}" unless name == 'rubocop'
|
44
|
+
|
45
|
+
raise ArgumentError, "Unknown gem '#{name}'" unless KNOWN_GEMS.include? name
|
46
|
+
raise ArgumentError, "Invalid version '#{version}'" unless version&.match? /\A\d+(?:\.\d+)+\z/
|
47
|
+
|
48
|
+
Spec.new(name: name, version: version)
|
49
|
+
end)
|
50
|
+
end
|
51
|
+
|
52
|
+
attr_reader :specs
|
53
|
+
|
54
|
+
def initialize(specs)
|
55
|
+
@specs = specs.dup.sort_by(&:name).freeze
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_s
|
59
|
+
@specs.join '-'
|
60
|
+
end
|
61
|
+
|
62
|
+
def empty?
|
63
|
+
@specs.empty?
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'rubocop/schema/value_objects'
|
2
|
+
require 'rubocop/schema/cop_schema'
|
3
|
+
require 'rubocop/schema/helpers'
|
4
|
+
require 'rubocop/schema/ascii_doc/index'
|
5
|
+
require 'rubocop/schema/ascii_doc/department'
|
6
|
+
require 'rubocop/schema/document_loader'
|
7
|
+
require 'rubocop/schema/defaults_ripper'
|
8
|
+
require 'rubocop/schema/cop_info_merger'
|
9
|
+
|
10
|
+
module RuboCop
|
11
|
+
module Schema
|
12
|
+
class Generator
|
13
|
+
include Helpers
|
14
|
+
|
15
|
+
# @return Hash
|
16
|
+
attr_reader :schema
|
17
|
+
|
18
|
+
# @param [Array<Spec>] specs
|
19
|
+
# @param [DocumentLoader] document_loader
|
20
|
+
def initialize(specs, document_loader)
|
21
|
+
@specs = specs
|
22
|
+
@loader = document_loader
|
23
|
+
@schema = template('schema')
|
24
|
+
@props = @schema.fetch('properties')
|
25
|
+
generate
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def generate
|
31
|
+
@specs.each &method(:generate_spec)
|
32
|
+
@props.delete 'AllCops' unless @specs.any? { |s| s.name == 'rubocop' }
|
33
|
+
end
|
34
|
+
|
35
|
+
def generate_spec(spec)
|
36
|
+
info_map = read_docs(spec)
|
37
|
+
read_defaults(spec).each do |name, cop_info|
|
38
|
+
info_map[name] = info_map.key?(name) ? CopInfoMerger.merge(info_map[name], cop_info) : cop_info
|
39
|
+
end
|
40
|
+
apply_cop_info info_map
|
41
|
+
end
|
42
|
+
|
43
|
+
def read_docs(spec)
|
44
|
+
{}.tap do |info_map|
|
45
|
+
AsciiDoc::Index.new(@loader.doc(spec)).department_names.each do |department|
|
46
|
+
info_map[department] = department_info(spec, department)
|
47
|
+
|
48
|
+
AsciiDoc::Department.new(@loader.doc(spec, department)).cops.each do |cop_info|
|
49
|
+
info_map[cop_info.name] = CopInfo.new(**cop_info.to_h)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def read_defaults(spec)
|
56
|
+
defaults = @loader.defaults(spec) or
|
57
|
+
return {}
|
58
|
+
|
59
|
+
DefaultsRipper.new(defaults).cops.map { |cop_info| [cop_info.name, cop_info] }.to_h
|
60
|
+
end
|
61
|
+
|
62
|
+
def apply_cop_info(info)
|
63
|
+
info.each do |cop_name, cop_info|
|
64
|
+
schema = CopSchema.new(cop_info).as_json
|
65
|
+
@props[cop_name] = @props.key?(cop_name) ? merge_schemas(@props[cop_name], schema) : schema
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# @param [Hash] old
|
70
|
+
# @param [Hash] new
|
71
|
+
def merge_schemas(old, new)
|
72
|
+
deep_merge(old, new) do |merged|
|
73
|
+
merged.delete 'type' if merged.key? '$ref'
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# @param [Spec] spec
|
78
|
+
# @param [String] department
|
79
|
+
# @return [CopInfo]
|
80
|
+
def department_info(spec, department)
|
81
|
+
description = "'#{department}' department"
|
82
|
+
description << " (#{spec.short_name} extension)" if spec.short_name
|
83
|
+
|
84
|
+
CopInfo.new(
|
85
|
+
name: department,
|
86
|
+
description: description
|
87
|
+
)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|