rubocop-sketchup 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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