rubocop-schema-gen 0.1.0 → 0.1.5
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/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
|