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.
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