rubocop-sketchup 0.2.1 → 0.3.0

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.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RuboCop
2
4
  # Ripped directly from rubocop-rspec.
3
5
  module SketchUp
@@ -38,7 +38,7 @@ module RuboCop
38
38
  end
39
39
 
40
40
  def top_level?
41
- parts.last == 'Object'
41
+ %w[Kernel Object].include?(parts.first)
42
42
  end
43
43
 
44
44
  end
@@ -23,9 +23,12 @@ module RuboCop
23
23
  def check_class_or_module(node)
24
24
  name = node.defined_module_name
25
25
  parent = Namespace.new(node.parent_module_name)
26
+ namespace = parent.join(name)
26
27
  # Don't want to process anything that aren't top level namespaces.
27
28
  return unless parent.top_level?
28
- check_namespace(node, parent.join(name))
29
+ # Don't check excluded namespaces.
30
+ return if exempted?(namespace)
31
+ check_namespace(node, namespace)
29
32
  end
30
33
 
31
34
  # Class variables are normally frowned upon since they leak through all
@@ -58,6 +61,17 @@ module RuboCop
58
61
  format('Use a single root namespace. (Found `%s`; Previously found `%s`)', namespace, @@namespace)
59
62
  end
60
63
 
64
+ def exempted?(namespace)
65
+ namespace_exceptions.include?(namespace.first)
66
+ end
67
+
68
+ def namespace_exceptions
69
+ exceptions = cop_config['Exceptions'] || []
70
+ return exceptions if exceptions.is_a?(Array)
71
+
72
+ raise 'exceptions needs to be an array of strings!'
73
+ end
74
+
61
75
  end
62
76
  end
63
77
  end
@@ -9,6 +9,7 @@ module RuboCop
9
9
 
10
10
  include SketchUp::NoCommentDisable
11
11
  include SketchUp::ExtensionProject
12
+ include RangeHelp
12
13
 
13
14
  IGNORED_DIRECTORIES = %w[
14
15
  __MACOSX
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module SketchupRequirements
6
+ class GlobalInclude < Cop
7
+
8
+ include SketchUp::NoCommentDisable
9
+ include SketchUp
10
+
11
+ MSG = 'Do not include into global namespace.'.freeze
12
+
13
+ def_node_matcher :is_include?, <<-PATTERN
14
+ (send nil? :include ...)
15
+ PATTERN
16
+
17
+ def on_send(node)
18
+ return unless global_include?(node)
19
+ add_offense(node, severity: :error)
20
+ end
21
+
22
+ private
23
+
24
+ def global_include?(node)
25
+ is_include?(node) && global_namespace?(node)
26
+ end
27
+
28
+ def global_namespace?(node)
29
+ ['Kernel', 'Object'].include?(node.parent_module_name)
30
+ end
31
+
32
+ end
33
+ end
34
+ end
35
+ end
@@ -16,7 +16,7 @@ module RuboCop
16
16
 
17
17
  # Reference: http://rubocop.readthedocs.io/en/latest/node_pattern/
18
18
  def_node_matcher :require_filename, <<-PATTERN
19
- (:send
19
+ (send
20
20
  {(const nil? :Sketchup) nil?} :require
21
21
  (str $_))
22
22
  PATTERN
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module SketchupRequirements
6
+ class ObserversStartOperation < Cop
7
+
8
+ include SketchUp::NoCommentDisable
9
+ include RangeHelp
10
+
11
+ MSG = 'Observers should create transparent operations.'.freeze
12
+
13
+ def_node_search :start_operation, <<-PATTERN
14
+ (send
15
+ _ :start_operation
16
+ ...)
17
+ PATTERN
18
+
19
+ OBSERVER_METHODS = %i[
20
+ onActivateModel
21
+ onNewModel
22
+ onOpenModel
23
+ onQuit
24
+ onUnloadExtension
25
+
26
+ onComponentInstanceAdded
27
+ onComponentInstanceRemoved
28
+
29
+ onComponentAdded
30
+ onComponentPropertiesChanged
31
+ onComponentRemoved
32
+ onComponentTypeChanged
33
+
34
+ onTextChanged
35
+
36
+ onActiveSectionPlaneChanged
37
+ onElementAdded
38
+ onElementModified
39
+ onElementRemoved
40
+ onEraseEntities
41
+
42
+ onChangeEntity
43
+ onEraseEntity
44
+
45
+ onClose
46
+ onOpen
47
+
48
+ onCurrentLayerChanged
49
+ onLayerAdded
50
+ onLayerChanged
51
+ onLayerRemoved
52
+ onRemoveAllLayers
53
+
54
+ onMaterialAdd
55
+ onMaterialChange
56
+ onMaterialRefChange
57
+ onMaterialRemove
58
+ onMaterialSetCurrent
59
+ onMaterialUndoRedo
60
+
61
+ onActivePathChanged
62
+ onAfterComponentSaveAs
63
+ onBeforeComponentSaveAs
64
+ onDeleteModel
65
+ onEraseAll
66
+ onExplode
67
+ onPidChanged
68
+ onPlaceComponent
69
+ onPostSaveModel
70
+ onPreSaveModel
71
+ onSaveModel
72
+ onTransactionAbort
73
+ onTransactionCommit
74
+ onTransactionEmpty
75
+ onTransactionRedo
76
+ onTransactionStart
77
+ onTransactionUndo
78
+
79
+ onOptionsProviderChanged
80
+
81
+ onContentsModified
82
+ onElementAdded
83
+ onElementRemoved
84
+
85
+ onRenderingOptionsChanged
86
+
87
+ onSelectionAdded
88
+ onSelectionBulkChange
89
+ onSelectionCleared
90
+ onSelectionRemoved
91
+
92
+ onShadowInfoChanged
93
+
94
+ onActiveToolChanged
95
+ onToolStateChanged
96
+
97
+ onViewChanged
98
+ ]
99
+
100
+ def on_def(node)
101
+ return unless observer_event?(node)
102
+ operations = start_operation(node)
103
+ operations.each { |operation|
104
+ _name, _disable_ui, _next_tr, transparent = operation.arguments
105
+ next unless transparent.nil? || transparent.falsey_literal?
106
+ location = operation_location(operation)
107
+ add_offense(operation, location: location, severity: :error)
108
+ }
109
+ end
110
+
111
+ private
112
+
113
+ def range(node)
114
+ range_between(node.begin_pos, node.end_pos)
115
+ end
116
+
117
+ def operation_location(node)
118
+ # Highlight the fourth argument if it's used. Fall back to the method
119
+ # name.
120
+ transparent_argument = node.arguments[3]
121
+ if transparent_argument
122
+ range(transparent_argument.loc.expression)
123
+ else
124
+ :selector
125
+ end
126
+ end
127
+
128
+ def observer_event?(node)
129
+ OBSERVER_METHODS.include?(node.method_name)
130
+ end
131
+
132
+ end
133
+ end
134
+ end
135
+ end
@@ -12,7 +12,7 @@ module RuboCop
12
12
  MSG = 'Always register extensions to load by default.'.freeze
13
13
 
14
14
  def_node_search :sketchup_register_extension, <<-PATTERN
15
- (:send
15
+ (send
16
16
  (const nil? :Sketchup) :register_extension
17
17
  $...)
18
18
  PATTERN
@@ -9,6 +9,7 @@ module RuboCop
9
9
 
10
10
  include SketchUp::NoCommentDisable
11
11
  include SketchUp::ExtensionProject
12
+ include RangeHelp
12
13
 
13
14
  MSG = 'Create and register one SketchupExtension instance per extension.'.freeze
14
15
  MSG_CREATE_ONE = 'Create only SketchupExtension instance per extension.'.freeze
@@ -18,17 +19,14 @@ module RuboCop
18
19
 
19
20
  # Reference: http://rubocop.readthedocs.io/en/latest/node_pattern/
20
21
  def_node_search :sketchup_extension_new, <<-PATTERN
21
- ({:lvasgn :ivasgn :cvasgn :gvasgn :casgn} ...
22
- (:send
23
- (:const nil? :SketchupExtension) :new
24
- _
25
- _))
22
+ (send
23
+ (const nil? :SketchupExtension) :new _ _)
26
24
  PATTERN
27
25
 
28
26
  def_node_search :sketchup_register_extension, <<-PATTERN
29
- (:send
27
+ (send
30
28
  (const nil? :Sketchup) :register_extension
31
- {({:lvar :ivar :cvar :gvar} $_)(const nil? $_)}
29
+ {({lvar ivar cvar gvar} $_)(const nil? $_)}
32
30
  _)
33
31
  PATTERN
34
32
 
@@ -41,6 +39,13 @@ module RuboCop
41
39
 
42
40
  # Look for SketchupExtension.new.
43
41
  extension_nodes = sketchup_extension_new(source_node).to_a
42
+
43
+ # Threat instances not assigned to anything as non-existing.
44
+ extension_nodes.select! { |node|
45
+ node.parent && node.parent.assignment?
46
+ }
47
+
48
+ # There should not be multiple instances.
44
49
  if extension_nodes.size > 1
45
50
  add_offense(nil,
46
51
  location: range,
@@ -48,6 +53,8 @@ module RuboCop
48
53
  severity: :error)
49
54
  return
50
55
  end
56
+
57
+ # There should be exactly one.
51
58
  extension_node = extension_nodes.first
52
59
  if extension_node.nil?
53
60
  add_offense(nil,
@@ -58,24 +65,25 @@ module RuboCop
58
65
  end
59
66
 
60
67
  # Find the name of the value SketchupExtension.new was assigned to.
61
- if extension_node.casgn_type?
62
- extension_var = extension_node.to_a[1]
68
+ assignment_node = extension_node.parent
69
+ if assignment_node.casgn_type?
70
+ extension_var = assignment_node.to_a[1]
63
71
  else
64
- extension_var = extension_node.to_a[0]
72
+ extension_var = assignment_node.to_a[0]
65
73
  end
66
74
 
67
75
  # Look for Sketchup.register and make sure it register the extension
68
76
  # object detected earlier.
69
77
  registered_vars = sketchup_register_extension(source_node).to_a
70
- # TODO: The offences here should probably highlight the line where
71
- # Sketchup.register_extension is.
78
+
79
+ # Make sure there is only one call to `register_extension`.
72
80
  if registered_vars.size > 1
73
- add_offense(nil,
74
- location: range,
81
+ add_offense(registered_vars[1],
75
82
  message: MSG_REGISTER_ONE,
76
83
  severity: :error)
77
84
  return
78
85
  end
86
+
79
87
  registered_var = sketchup_register_extension(source_node).first
80
88
  unless registered_var == extension_var
81
89
  msg = MSG_REGISTER_MISSING % extension_var.to_s
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module SketchUp
5
+ class SketchUpVersion
6
+
7
+ include Comparable
8
+
9
+ attr_reader :version, :maintenance
10
+
11
+ class InvalidVersion < StandardError; end
12
+
13
+ def initialize(version)
14
+ @version, @maintenance = parse_version(version)
15
+ end
16
+
17
+ def <=>(other)
18
+ if version == other.version
19
+ maintenance <=> other.maintenance
20
+ else
21
+ version <=> other.version
22
+ end
23
+ end
24
+
25
+ def to_s
26
+ string_version = version < 2013 ? version.to_f : version.to_i
27
+ if maintenance > 0
28
+ "SketchUp #{string_version} M#{maintenance}"
29
+ else
30
+ "SketchUp #{string_version}"
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ VERSION_NUMBER_REGEX = /^(?:SketchUp )?([0-9.]+)(?: M(\d+))?$/
37
+
38
+ # This list is compiled from the list of versions reported by YARD when
39
+ # running the `versions` template;
40
+ #
41
+ # yardoc -t versions -f text
42
+ #
43
+ # TODO(thomthom): Push the version template to the API stubs repository.
44
+ VALID_VERSIONS = [
45
+ [2018, 0],
46
+ [2017, 0],
47
+ [2016, 1],
48
+ [2016, 0],
49
+ [2015, 0],
50
+ [2014, 0],
51
+ [2013, 0],
52
+ [8.0, 2],
53
+ [8.0, 1],
54
+ [8.0, 0],
55
+ [7.1, 1],
56
+ [7.1, 0],
57
+ [7.0, 1],
58
+ [7.0, 0],
59
+ [6.0, 0],
60
+ ]
61
+
62
+ def parse_version(version)
63
+ v = 0
64
+ m = 0
65
+ if version.is_a?(String)
66
+ # Treat all LayOut versions as SketchUp versions for now.
67
+ normalised_version = version.gsub('LayOut', 'SketchUp')
68
+ result = normalised_version.match(VERSION_NUMBER_REGEX)
69
+ if result
70
+ v = result.captures[0].to_f
71
+ m = (result.captures[1] || '0').to_i
72
+ end
73
+ elsif version.is_a?(Numeric)
74
+ v, m = [version, 0]
75
+ end
76
+ version_parts = [v, m]
77
+ unless VALID_VERSIONS.include?(version_parts)
78
+ raise InvalidVersion, "#{version} is not a valid SketchUp version"
79
+ end
80
+ version_parts
81
+ end
82
+
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module SketchupSuggestions
6
+ class Compatibility < Cop
7
+
8
+ include SketchUp::Config
9
+ include SketchUp::Features
10
+
11
+ MSG = "Incompatible feature with target SketchUp version".freeze
12
+
13
+ def on_def(node)
14
+ return unless observer_method?(node)
15
+ feature_name = "##{node.method_name}"
16
+ check_feature(node, :method, feature_name)
17
+ end
18
+
19
+ def on_send(node)
20
+ if module_method?(node)
21
+ feature_name = "#{node.receiver.const_name}.#{node.method_name}"
22
+ check_feature(node, :method, feature_name)
23
+ else
24
+ # Instance methods are harder. It's difficult to infer the type of
25
+ # the receiver. If we only check the method name in isolation we
26
+ # will get too many false positives with method names matching
27
+ # methods in Ruby itself and other older features.
28
+ # We try to match names that are unlikely to cause much noise.
29
+ return unless checkable_instance_method?(node)
30
+ feature_name = "##{node.method_name}"
31
+ check_feature(node, :method, feature_name)
32
+ end
33
+ end
34
+
35
+ def on_const(node)
36
+ feature_name = node.const_name
37
+ [:class, :module, :constant].each { |type|
38
+ check_feature(node, type, feature_name)
39
+ }
40
+ end
41
+
42
+ private
43
+
44
+ def check_feature(node, type, feature_name)
45
+ return unless sketchup_target_version?
46
+ full_feature_name = feature_name
47
+ FEATURES.each { |feature_set|
48
+ feature_version = SketchUp::SketchUpVersion.new(feature_set[:version])
49
+ next unless feature_version > sketchup_target_version
50
+ objects = feature_set[:types][type] || []
51
+ if type == :method && instance_method?(feature_name)
52
+ # Instance methods are simply matching the method name since it's
53
+ # very difficult to determine the type of the receiver.
54
+ full_feature_name = objects.find { |object| object.end_with?(feature_name) }
55
+ next unless full_feature_name
56
+ else
57
+ next unless objects.include?(feature_name)
58
+ end
59
+ report(node, full_feature_name, feature_version, type)
60
+ }
61
+ end
62
+
63
+ def report(node, feature_name, feature_version, feature_type)
64
+ message = "The #{feature_type} `#{feature_name}` was added in "\
65
+ "#{feature_version} which is incompatible with target "\
66
+ "#{sketchup_target_version}."
67
+ add_offense(node, message: message)
68
+ end
69
+
70
+ def module_method?(node)
71
+ node.receiver && node.receiver.const_type?
72
+ end
73
+
74
+ def instance_method?(feature_name)
75
+ feature_name.start_with?('#')
76
+ end
77
+
78
+ def checkable_instance_method?(node)
79
+ INSTANCE_METHODS.include?(node.method_name)
80
+ end
81
+
82
+ def observer_method?(node)
83
+ OBSERVER_METHODS.include?(node.method_name)
84
+ end
85
+
86
+ end
87
+ end
88
+ end
89
+ end