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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +13 -0
  3. data/README.md +20 -25
  4. data/assets/templates/cop_schema.yml +1 -3
  5. data/assets/templates/schema.yml +4 -0
  6. data/exe/rubocop-schema-gen +4 -2
  7. data/lib/rubocop/schema.rb +0 -4
  8. data/lib/rubocop/schema/ascii_doc/base.rb +52 -0
  9. data/lib/rubocop/schema/ascii_doc/cop.rb +93 -0
  10. data/lib/rubocop/schema/ascii_doc/department.rb +21 -0
  11. data/lib/rubocop/schema/ascii_doc/index.rb +20 -0
  12. data/lib/rubocop/schema/ascii_doc/stringifier.rb +49 -0
  13. data/lib/rubocop/schema/{cache.rb → cached_http_client.rb} +9 -12
  14. data/lib/rubocop/schema/cli.rb +94 -26
  15. data/lib/rubocop/schema/cop_info_merger.rb +54 -0
  16. data/lib/rubocop/schema/cop_schema.rb +69 -26
  17. data/lib/rubocop/schema/defaults_ripper.rb +48 -0
  18. data/lib/rubocop/schema/diff.rb +67 -0
  19. data/lib/rubocop/schema/document_loader.rb +55 -0
  20. data/lib/rubocop/schema/extension_spec.rb +67 -0
  21. data/lib/rubocop/schema/generator.rb +91 -0
  22. data/lib/rubocop/schema/helpers.rb +62 -0
  23. data/lib/rubocop/schema/repo.rb +91 -0
  24. data/lib/rubocop/schema/value_objects.rb +29 -3
  25. data/lib/rubocop/schema/version.rb +1 -1
  26. metadata +20 -36
  27. data/.gitignore +0 -19
  28. data/.rspec +0 -3
  29. data/.rubocop.yml +0 -38
  30. data/.ruby-version +0 -1
  31. data/.travis.yml +0 -6
  32. data/CODE_OF_CONDUCT.md +0 -74
  33. data/Gemfile +0 -10
  34. data/LICENSE.txt +0 -21
  35. data/Rakefile +0 -6
  36. data/bin/console +0 -14
  37. data/bin/setup +0 -8
  38. data/lib/rubocop/schema/lockfile_inspector.rb +0 -51
  39. data/lib/rubocop/schema/scraper.rb +0 -183
  40. data/lib/rubocop/schema/templates.rb +0 -8
  41. data/rubocop-schema.gemspec +0 -31
  42. 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
- # @return [Class<RuboCop::Cop::Base>]
5
- attr_reader :cop
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(cop, info)
10
- raise ArgumentError unless cop.is_a?(Class) && cop < Cop::Base
17
+ def initialize(info)
18
+ @info = info.dup.freeze
19
+ @json = template('cop_schema')
20
+ generate
21
+ end
11
22
 
12
- @cop = cop
13
- @info = info
23
+ def as_json
24
+ @json
14
25
  end
15
26
 
16
- KNOWN_TYPES = Set.new(%w[boolean integer array string]).freeze
27
+ alias to_h as_json
17
28
 
18
- def as_json
19
- Schema.template('cop_schema').tap do |json|
20
- json['$comment'] = cop.documentation_url
21
- json['properties'] = props = json.fetch('properties').dup
22
-
23
- # AutoCorrect
24
- props['AutoCorrect'] = { 'type' => 'boolean' } if cop.support_autocorrect?
25
-
26
- if @info
27
- json['description'] = @info.description
28
- @info.attributes.each do |attr|
29
- next if attr.name.blank?
30
-
31
- props[attr.name] = prop = {}
32
- prop['description'] = "Default: #{attr.default}" unless attr.default.blank?
33
- prop['type'] = attr.type if KNOWN_TYPES.include? attr.type
34
- end
35
- end
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