theme-check 1.10.3 → 1.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +29 -0
  3. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  4. data/.github/workflows/cla.yml +22 -0
  5. data/.github/workflows/theme-check.yml +1 -1
  6. data/.gitignore +3 -0
  7. data/CHANGELOG.md +48 -0
  8. data/CONTRIBUTING.md +82 -0
  9. data/README.md +11 -8
  10. data/Rakefile +7 -0
  11. data/TROUBLESHOOTING.md +65 -0
  12. data/config/default.yml +4 -0
  13. data/data/shopify_liquid/built_in_liquid_objects.json +60 -0
  14. data/data/shopify_liquid/documentation/filters.json +5528 -0
  15. data/data/shopify_liquid/documentation/latest.json +1 -0
  16. data/data/shopify_liquid/documentation/objects.json +19272 -0
  17. data/data/shopify_liquid/documentation/tags.json +1252 -0
  18. data/data/shopify_liquid/filters.yml +18 -0
  19. data/dev.yml +1 -1
  20. data/docs/checks/asset_preload.md +60 -0
  21. data/docs/checks/asset_size_javascript.md +2 -2
  22. data/docs/checks/missing_enable_comment.md +3 -3
  23. data/docs/checks/nested_snippet.md +8 -8
  24. data/docs/checks/translation_key_exists.md +4 -4
  25. data/docs/checks/valid_html_translation.md +1 -1
  26. data/lib/theme_check/analyzer.rb +18 -3
  27. data/lib/theme_check/check.rb +6 -1
  28. data/lib/theme_check/checks/asset_preload.rb +20 -0
  29. data/lib/theme_check/checks/deprecated_filter.rb +29 -5
  30. data/lib/theme_check/checks/missing_enable_comment.rb +4 -0
  31. data/lib/theme_check/checks/missing_required_template_files.rb +5 -1
  32. data/lib/theme_check/checks/missing_template.rb +5 -1
  33. data/lib/theme_check/checks/undefined_object.rb +4 -0
  34. data/lib/theme_check/checks/unused_assign.rb +6 -1
  35. data/lib/theme_check/checks/unused_snippet.rb +50 -2
  36. data/lib/theme_check/config.rb +2 -2
  37. data/lib/theme_check/disabled_checks.rb +11 -4
  38. data/lib/theme_check/file_system_storage.rb +2 -0
  39. data/lib/theme_check/in_memory_storage.rb +1 -1
  40. data/lib/theme_check/language_server/bridge.rb +31 -6
  41. data/lib/theme_check/language_server/completion_context.rb +52 -0
  42. data/lib/theme_check/language_server/completion_engine.rb +15 -21
  43. data/lib/theme_check/language_server/completion_provider.rb +16 -1
  44. data/lib/theme_check/language_server/completion_providers/assignments_completion_provider.rb +36 -0
  45. data/lib/theme_check/language_server/completion_providers/filter_completion_provider.rb +49 -6
  46. data/lib/theme_check/language_server/completion_providers/object_attribute_completion_provider.rb +47 -0
  47. data/lib/theme_check/language_server/completion_providers/object_completion_provider.rb +10 -7
  48. data/lib/theme_check/language_server/completion_providers/render_snippet_completion_provider.rb +5 -1
  49. data/lib/theme_check/language_server/completion_providers/tag_completion_provider.rb +8 -1
  50. data/lib/theme_check/language_server/diagnostics_engine.rb +80 -34
  51. data/lib/theme_check/language_server/diagnostics_manager.rb +27 -6
  52. data/lib/theme_check/language_server/execute_command_providers/run_checks_execute_command_provider.rb +7 -6
  53. data/lib/theme_check/language_server/handler.rb +93 -9
  54. data/lib/theme_check/language_server/protocol.rb +9 -0
  55. data/lib/theme_check/language_server/server.rb +42 -14
  56. data/lib/theme_check/language_server/type_helper.rb +22 -0
  57. data/lib/theme_check/language_server/variable_lookup_finder/assignments_finder/node_handler.rb +63 -0
  58. data/lib/theme_check/language_server/variable_lookup_finder/assignments_finder/scope.rb +57 -0
  59. data/lib/theme_check/language_server/variable_lookup_finder/assignments_finder/scope_visitor.rb +42 -0
  60. data/lib/theme_check/language_server/variable_lookup_finder/assignments_finder.rb +76 -0
  61. data/lib/theme_check/language_server/variable_lookup_finder/constants.rb +43 -0
  62. data/lib/theme_check/language_server/variable_lookup_finder/liquid_fixer.rb +103 -0
  63. data/lib/theme_check/language_server/variable_lookup_finder/potential_lookup.rb +10 -0
  64. data/lib/theme_check/language_server/variable_lookup_finder/tolerant_parser.rb +94 -0
  65. data/lib/theme_check/language_server/variable_lookup_finder.rb +60 -100
  66. data/lib/theme_check/language_server/variable_lookup_traverser.rb +70 -0
  67. data/lib/theme_check/language_server/versioned_in_memory_storage.rb +17 -2
  68. data/lib/theme_check/language_server.rb +12 -0
  69. data/lib/theme_check/liquid_file.rb +22 -1
  70. data/lib/theme_check/liquid_node.rb +33 -1
  71. data/lib/theme_check/liquid_visitor.rb +1 -1
  72. data/lib/theme_check/remote_asset_file.rb +13 -7
  73. data/lib/theme_check/schema_helper.rb +1 -1
  74. data/lib/theme_check/shopify_liquid/documentation/markdown_template.rb +51 -0
  75. data/lib/theme_check/shopify_liquid/documentation.rb +44 -0
  76. data/lib/theme_check/shopify_liquid/filter.rb +4 -0
  77. data/lib/theme_check/shopify_liquid/object.rb +4 -0
  78. data/lib/theme_check/shopify_liquid/source_index/base_entry.rb +60 -0
  79. data/lib/theme_check/shopify_liquid/source_index/base_state.rb +23 -0
  80. data/lib/theme_check/shopify_liquid/source_index/filter_entry.rb +18 -0
  81. data/lib/theme_check/shopify_liquid/source_index/filter_state.rb +11 -0
  82. data/lib/theme_check/shopify_liquid/source_index/object_entry.rb +14 -0
  83. data/lib/theme_check/shopify_liquid/source_index/object_state.rb +11 -0
  84. data/lib/theme_check/shopify_liquid/source_index/parameter_entry.rb +21 -0
  85. data/lib/theme_check/shopify_liquid/source_index/property_entry.rb +9 -0
  86. data/lib/theme_check/shopify_liquid/source_index/return_type_entry.rb +37 -0
  87. data/lib/theme_check/shopify_liquid/source_index/tag_entry.rb +20 -0
  88. data/lib/theme_check/shopify_liquid/source_index/tag_state.rb +11 -0
  89. data/lib/theme_check/shopify_liquid/source_index.rb +56 -0
  90. data/lib/theme_check/shopify_liquid/source_manager.rb +111 -0
  91. data/lib/theme_check/shopify_liquid/tag.rb +4 -0
  92. data/lib/theme_check/shopify_liquid.rb +17 -1
  93. data/lib/theme_check/tags.rb +2 -1
  94. data/lib/theme_check/version.rb +1 -1
  95. data/shipit.rubygems.yml +3 -0
  96. data/theme-check.gemspec +5 -3
  97. metadata +45 -6
  98. data/.github/probots.yml +0 -3
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "timeout"
4
+
3
5
  # This class exists as a bridge (or boundary) between our handlers and the outside world.
4
6
  #
5
7
  # It is concerned with all the Language Server Protocol constructs. i.e.
@@ -29,6 +31,9 @@ module ThemeCheck
29
31
 
30
32
  # Whether the client supports work done progress notifications
31
33
  @supports_work_done_progress = false
34
+
35
+ @work_done_progress_mutex = Mutex.new
36
+ @work_done_progress_token = 1
32
37
  end
33
38
 
34
39
  def log(message)
@@ -78,12 +83,17 @@ module ThemeCheck
78
83
 
79
84
  # https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#responseError
80
85
  def send_internal_error(id, e)
86
+ # For a reason I can't comprehend, sometimes
87
+ # e.full_message _hangs_ and brings your CPU to 100%.
88
+ # It's wrapped in here because it prints anyway...
89
+ # This shit is weird, yo.
90
+ Timeout.timeout(1) do
91
+ $stderr.puts e.full_message
92
+ end
93
+ ensure
81
94
  send_response(id, nil, {
82
95
  code: ErrorCodes::INTERNAL_ERROR,
83
- message: <<~EOS,
84
- #{e.class}: #{e.message}
85
- #{e.backtrace.join("\n ")}
86
- EOS
96
+ message: "A theme-check-language-server has occured, inspect OUTPUT logs for details.",
87
97
  })
88
98
  end
89
99
 
@@ -103,15 +113,28 @@ module ThemeCheck
103
113
  @supports_work_done_progress
104
114
  end
105
115
 
106
- def send_create_work_done_progress_request(token)
107
- return unless supports_work_done_progress?
116
+ def send_create_work_done_progress_request
117
+ # This isn't necessary, but it kind of is to make it obvious
118
+ # that this variable is not thread safe. Don't try to refactor
119
+ # this with @work_done_progress_token because you're going to
120
+ # have a hard time.
121
+ token = @work_done_progress_mutex.synchronize do
122
+ @work_done_progress_token += 1
123
+ end
124
+
125
+ return token unless supports_work_done_progress?
126
+
127
+ # We're going to wait for a response here...
108
128
  send_request("window/workDoneProgress/create", {
109
129
  token: token,
110
130
  })
131
+
132
+ token
111
133
  end
112
134
 
113
135
  def send_work_done_progress_begin(token, title)
114
136
  return unless supports_work_done_progress?
137
+
115
138
  send_progress(token, {
116
139
  kind: 'begin',
117
140
  title: title,
@@ -122,6 +145,7 @@ module ThemeCheck
122
145
 
123
146
  def send_work_done_progress_report(token, message, percentage)
124
147
  return unless supports_work_done_progress?
148
+
125
149
  send_progress(token, {
126
150
  kind: 'report',
127
151
  message: message,
@@ -132,6 +156,7 @@ module ThemeCheck
132
156
 
133
157
  def send_work_done_progress_end(token, message)
134
158
  return unless supports_work_done_progress?
159
+
135
160
  send_progress(token, {
136
161
  kind: 'end',
137
162
  message: message,
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ class CompletionContext
6
+ include PositionHelper
7
+
8
+ attr_reader :storage, :relative_path, :line, :col
9
+
10
+ def initialize(storage, relative_path, line, col)
11
+ @storage = storage
12
+ @relative_path = relative_path
13
+ @line = line
14
+ @col = col
15
+ end
16
+
17
+ def buffer
18
+ @buffer ||= storage.read(relative_path)
19
+ end
20
+
21
+ def buffer_until_previous_row
22
+ @buffer_without_current_row ||= buffer[0..absolute_cursor].lines[0...-1].join
23
+ end
24
+
25
+ def absolute_cursor
26
+ @absolute_cursor ||= from_row_column_to_index(buffer, line, col)
27
+ end
28
+
29
+ def cursor
30
+ @cursor ||= absolute_cursor - token&.start || 0
31
+ end
32
+
33
+ def content
34
+ @content ||= token&.content
35
+ end
36
+
37
+ def token
38
+ @token ||= Tokens.new(buffer).find do |t|
39
+ # Here we include the next character and exclude the first
40
+ # one becase when we want to autocomplete inside a token
41
+ # and at most 1 outside it since the cursor could be placed
42
+ # at the end of the token.
43
+ t.start < absolute_cursor && absolute_cursor <= t.end
44
+ end
45
+ end
46
+
47
+ def clone_and_overwrite(col:)
48
+ CompletionContext.new(storage, relative_path, line, col)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -3,35 +3,29 @@
3
3
  module ThemeCheck
4
4
  module LanguageServer
5
5
  class CompletionEngine
6
- include PositionHelper
7
-
8
- def initialize(storage)
6
+ def initialize(storage, bridge = nil)
9
7
  @storage = storage
8
+ @bridge = bridge
10
9
  @providers = CompletionProvider.all.map { |x| x.new(storage) }
11
10
  end
12
11
 
13
12
  def completions(relative_path, line, col)
14
- buffer = @storage.read(relative_path)
15
- cursor = from_row_column_to_index(buffer, line, col)
16
- token = find_token(buffer, cursor)
17
- return [] if token.nil?
13
+ context = context(relative_path, line, col)
14
+
15
+ @providers
16
+ .flat_map { |provider| provider.completions(context) }
17
+ .uniq { |completion_item| completion_item[:label] }
18
+ rescue StandardError => error
19
+ @bridge || raise(error)
20
+
21
+ message = error.message
22
+ backtrace = error.backtrace.join("\n")
18
23
 
19
- @providers.flat_map do |p|
20
- p.completions(
21
- token.content,
22
- cursor - token.start
23
- )
24
- end
24
+ @bridge.log("[completion error] error: #{message}\n#{backtrace}")
25
25
  end
26
26
 
27
- def find_token(buffer, cursor)
28
- Tokens.new(buffer).find do |token|
29
- # Here we include the next character and exclude the first
30
- # one becase when we want to autocomplete inside a token
31
- # and at most 1 outside it since the cursor could be placed
32
- # at the end of the token.
33
- token.start < cursor && cursor <= token.end
34
- end
27
+ def context(relative_path, line, col)
28
+ CompletionContext.new(@storage, relative_path, line, col)
35
29
  end
36
30
  end
37
31
  end
@@ -6,6 +6,10 @@ module ThemeCheck
6
6
  include CompletionHelper
7
7
  include RegexHelpers
8
8
 
9
+ attr_reader :storage
10
+
11
+ CurrentToken = Struct.new(:content, :cursor, :absolute_cursor, :buffer)
12
+
9
13
  class << self
10
14
  def all
11
15
  @all ||= []
@@ -20,9 +24,20 @@ module ThemeCheck
20
24
  @storage = storage
21
25
  end
22
26
 
23
- def completions(content, cursor)
27
+ def completions(relative_path, line, col)
24
28
  raise NotImplementedError
25
29
  end
30
+
31
+ def doc_hash(content)
32
+ return {} if content.nil? || content.empty?
33
+
34
+ {
35
+ documentation: {
36
+ kind: MarkupKinds::MARKDOWN,
37
+ value: content,
38
+ },
39
+ }
40
+ end
26
41
  end
27
42
  end
28
43
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ class AssignmentsCompletionProvider < CompletionProvider
6
+ def completions(context)
7
+ content = context.buffer_until_previous_row
8
+
9
+ return [] if content.nil?
10
+ return [] unless (variable_lookup = VariableLookupFinder.lookup(context))
11
+ return [] unless variable_lookup.lookups.empty?
12
+ return [] if context.content[context.cursor - 1] == "."
13
+
14
+ finder = VariableLookupFinder::AssignmentsFinder.new(content)
15
+ finder.find!
16
+
17
+ finder.assignments.map do |label, potential_lookup|
18
+ object, _property = VariableLookupTraverser.lookup_object_and_property(potential_lookup)
19
+ object_to_completion(label, object.name)
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def object_to_completion(label, object)
26
+ content = ShopifyLiquid::Documentation.object_doc(object)
27
+
28
+ {
29
+ label: label,
30
+ kind: CompletionItemKinds::VARIABLE,
31
+ **doc_hash(content),
32
+ }
33
+ end
34
+ end
35
+ end
36
+ end
@@ -4,11 +4,19 @@ module ThemeCheck
4
4
  module LanguageServer
5
5
  class FilterCompletionProvider < CompletionProvider
6
6
  NAMED_FILTER = /#{Liquid::FilterSeparator}\s*(\w+)/o
7
+ FILTER_SEPARATOR_INCLUDING_SPACES = /\s*#{Liquid::FilterSeparator}/
8
+ INPUT_TYPE_VARIABLE = 'variable'
7
9
 
8
- def completions(content, cursor)
10
+ def completions(context)
11
+ content = context.content
12
+ cursor = context.cursor
13
+
14
+ return [] if content.nil?
9
15
  return [] unless can_complete?(content, cursor)
10
- available_labels
11
- .select { |w| w.start_with?(partial(content, cursor)) }
16
+
17
+ context = context_with_cursor_before_potential_filter_separator(context)
18
+ available_filters_for(determine_input_type(context))
19
+ .select { |w| w.name.start_with?(partial(content, cursor)) }
12
20
  .map { |filter| filter_to_completion(filter) }
13
21
  end
14
22
 
@@ -21,12 +29,41 @@ module ThemeCheck
21
29
 
22
30
  private
23
31
 
24
- def available_labels
25
- @labels ||= ShopifyLiquid::Filter.labels - ShopifyLiquid::DeprecatedFilter.labels
32
+ def context_with_cursor_before_potential_filter_separator(context)
33
+ content = context.content
34
+ diff = content.index(FILTER_SEPARATOR_INCLUDING_SPACES) - context.cursor
35
+
36
+ return context unless content.scan(FILTER_SEPARATOR_INCLUDING_SPACES).size == 1
37
+
38
+ context.clone_and_overwrite(col: context.col + diff)
39
+ end
40
+
41
+ def determine_input_type(context)
42
+ variable_lookup = VariableLookupFinder.lookup(context)
43
+
44
+ if variable_lookup
45
+ object, property = VariableLookupTraverser.lookup_object_and_property(variable_lookup)
46
+ return property.return_type if property
47
+ return object.name if object
48
+ end
49
+ end
50
+
51
+ def available_filters_for(input_type)
52
+ filters = ShopifyLiquid::SourceIndex.filters
53
+ .select { |filter| input_type.nil? || filter.input_type == input_type }
54
+ return all_labels if filters.empty?
55
+ return filters if input_type == INPUT_TYPE_VARIABLE
56
+
57
+ filters + available_filters_for(INPUT_TYPE_VARIABLE)
58
+ end
59
+
60
+ def all_labels
61
+ available_filters_for(nil)
26
62
  end
27
63
 
28
64
  def cursor_on_filter?(content, cursor)
29
65
  return false unless content.match?(NAMED_FILTER)
66
+
30
67
  matches(content, NAMED_FILTER).any? do |match|
31
68
  match.begin(1) <= cursor && cursor < match.end(1) + 1 # including next character
32
69
  end
@@ -34,17 +71,23 @@ module ThemeCheck
34
71
 
35
72
  def partial(content, cursor)
36
73
  return '' unless content.match?(NAMED_FILTER)
74
+
37
75
  partial_match = matches(content, NAMED_FILTER).find do |match|
38
76
  match.begin(1) <= cursor && cursor < match.end(1) + 1 # including next character
39
77
  end
40
78
  return '' if partial_match.nil?
79
+
41
80
  partial_match[1]
42
81
  end
43
82
 
44
83
  def filter_to_completion(filter)
84
+ content = ShopifyLiquid::Documentation.render_doc(filter)
85
+
45
86
  {
46
- label: filter,
87
+ label: filter.name,
47
88
  kind: CompletionItemKinds::FUNCTION,
89
+ tags: filter.deprecated? ? [CompletionItemTag::DEPRECATED] : [],
90
+ **doc_hash(content),
48
91
  }
49
92
  end
50
93
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ThemeCheck
4
+ module LanguageServer
5
+ class ObjectAttributeCompletionProvider < CompletionProvider
6
+ def completions(context)
7
+ content = context.content
8
+ cursor = context.cursor
9
+
10
+ return [] if content.nil?
11
+ return [] unless (variable_lookup = VariableLookupFinder.lookup(context))
12
+ return [] if content[cursor - 1] == "." && content[cursor - 2] == "."
13
+
14
+ # Navigate through lookups until the last valid [object, property] level
15
+ object, property = VariableLookupTraverser.lookup_object_and_property(variable_lookup)
16
+
17
+ # If the last lookup level is incomplete/invalid, use the partial term
18
+ # to filter object properties.
19
+ partial = partial_property_name(property, variable_lookup)
20
+
21
+ return [] unless object
22
+
23
+ object
24
+ .properties
25
+ .select { |prop| partial.nil? || prop.name.start_with?(partial) }
26
+ .map { |prop| property_to_completion(prop) }
27
+ end
28
+
29
+ private
30
+
31
+ def partial_property_name(property, variable_lookup)
32
+ last_property = variable_lookup.lookups.last
33
+ last_property if last_property != property&.name
34
+ end
35
+
36
+ def property_to_completion(prop)
37
+ content = ShopifyLiquid::Documentation.render_doc(prop)
38
+
39
+ {
40
+ label: prop.name,
41
+ kind: CompletionItemKinds::PROPERTY,
42
+ **doc_hash(content),
43
+ }
44
+ end
45
+ end
46
+ end
47
+ end
@@ -3,19 +3,19 @@
3
3
  module ThemeCheck
4
4
  module LanguageServer
5
5
  class ObjectCompletionProvider < CompletionProvider
6
- def completions(content, cursor)
7
- return [] unless (variable_lookup = variable_lookup_at_cursor(content, cursor))
6
+ def completions(context)
7
+ content = context.content
8
+
9
+ return [] if content.nil?
10
+ return [] unless (variable_lookup = VariableLookupFinder.lookup(context))
8
11
  return [] unless variable_lookup.lookups.empty?
9
- return [] if content[cursor - 1] == "."
12
+ return [] if content[context.cursor - 1] == "."
13
+
10
14
  ShopifyLiquid::Object.labels
11
15
  .select { |w| w.start_with?(partial(variable_lookup)) }
12
16
  .map { |object| object_to_completion(object) }
13
17
  end
14
18
 
15
- def variable_lookup_at_cursor(content, cursor)
16
- VariableLookupFinder.lookup(content, cursor)
17
- end
18
-
19
19
  def partial(variable_lookup)
20
20
  variable_lookup.name || ''
21
21
  end
@@ -23,9 +23,12 @@ module ThemeCheck
23
23
  private
24
24
 
25
25
  def object_to_completion(object)
26
+ content = ShopifyLiquid::Documentation.object_doc(object)
27
+
26
28
  {
27
29
  label: object,
28
30
  kind: CompletionItemKinds::VARIABLE,
31
+ **doc_hash(content),
29
32
  }
30
33
  end
31
34
  end
@@ -3,7 +3,11 @@
3
3
  module ThemeCheck
4
4
  module LanguageServer
5
5
  class RenderSnippetCompletionProvider < CompletionProvider
6
- def completions(content, cursor)
6
+ def completions(context)
7
+ content = context.content
8
+ cursor = context.cursor
9
+
10
+ return [] if content.nil?
7
11
  return [] unless cursor_on_quoted_argument?(content, cursor)
8
12
  partial = snippet(content) || ''
9
13
  snippets
@@ -3,7 +3,11 @@
3
3
  module ThemeCheck
4
4
  module LanguageServer
5
5
  class TagCompletionProvider < CompletionProvider
6
- def completions(content, cursor)
6
+ def completions(context)
7
+ content = context.content
8
+ cursor = context.cursor
9
+
10
+ return [] if content.nil?
7
11
  return [] unless can_complete?(content, cursor)
8
12
  partial = first_word(content) || ''
9
13
  labels = ShopifyLiquid::Tag.labels
@@ -23,9 +27,12 @@ module ThemeCheck
23
27
  private
24
28
 
25
29
  def tag_to_completion(tag)
30
+ content = ShopifyLiquid::Documentation.tag_doc(tag)
31
+
26
32
  {
27
33
  label: tag,
28
34
  kind: CompletionItemKinds::KEYWORD,
35
+ **doc_hash(content),
29
36
  }
30
37
  end
31
38
  end
@@ -12,57 +12,103 @@ module ThemeCheck
12
12
  @diagnostics_manager = diagnostics_manager
13
13
  @storage = storage
14
14
  @bridge = bridge
15
- @token = 0
16
15
  end
17
16
 
18
17
  def first_run?
19
18
  @diagnostics_manager.first_run?
20
19
  end
21
20
 
22
- def analyze_and_send_offenses(absolute_path, config, force: false, only_single_file: false)
21
+ def analyze_and_send_offenses(absolute_path_or_paths, config, force: false, only_single_file: false)
23
22
  return unless @diagnostics_lock.try_lock
24
- @token += 1
25
- @bridge.send_create_work_done_progress_request(@token)
23
+
26
24
  theme = ThemeCheck::Theme.new(storage)
27
25
  analyzer = ThemeCheck::Analyzer.new(theme, config.enabled_checks)
28
26
 
29
- if (!only_single_file && @diagnostics_manager.first_run?) || force
30
- @bridge.send_work_done_progress_begin(@token, "Full theme check")
31
- @bridge.log("Checking #{storage.root}")
32
- offenses = nil
33
- time = Benchmark.measure do
34
- offenses = analyzer.analyze_theme do |path, i, total|
35
- @bridge.send_work_done_progress_report(@token, "#{i}/#{total} #{path}", (i.to_f / total * 100.0).to_i)
36
- end
37
- end
38
- end_message = "Found #{offenses.size} offenses in #{format("%0.2f", time.real)}s"
39
- @bridge.send_work_done_progress_end(@token, end_message)
40
- @bridge.log(end_message)
41
- send_diagnostics(offenses)
27
+ if !only_single_file && (@diagnostics_manager.first_run? || force)
28
+ run_full_theme_check(analyzer)
42
29
  else
43
- # Analyze selected files
44
- relative_path = Pathname.new(storage.relative_path(absolute_path))
45
- file = theme[relative_path]
46
- # Skip if not a theme file
47
- if file
48
- @bridge.send_work_done_progress_begin(@token, "Partial theme check")
49
- offenses = nil
50
- time = Benchmark.measure do
51
- offenses = analyzer.analyze_files([file], only_single_file: only_single_file) do |path, i, total|
52
- @bridge.send_work_done_progress_report(@token, "#{i}/#{total} #{path}", (i.to_f / total * 100.0).to_i)
53
- end
54
- end
55
- end_message = "Found #{offenses.size} new offenses in #{format("%0.2f", time.real)}s"
56
- @bridge.send_work_done_progress_end(@token, end_message)
57
- @bridge.log(end_message)
58
- send_diagnostics(offenses, [relative_path], only_single_file: only_single_file)
59
- end
30
+ run_partial_theme_check(absolute_path_or_paths, theme, analyzer, only_single_file)
31
+ end
32
+
33
+ @diagnostics_lock.unlock
34
+ end
35
+
36
+ def clear_diagnostics(relative_paths)
37
+ return unless @diagnostics_lock.try_lock
38
+
39
+ as_array(relative_paths).each do |relative_path|
40
+ send_clearing_diagnostics(relative_path)
60
41
  end
42
+
61
43
  @diagnostics_lock.unlock
62
44
  end
63
45
 
64
46
  private
65
47
 
48
+ def run_full_theme_check(analyzer)
49
+ raise 'Unsafe operation' unless @diagnostics_lock.owned?
50
+
51
+ token = @bridge.send_create_work_done_progress_request
52
+ @bridge.send_work_done_progress_begin(token, "Full theme check")
53
+ @bridge.log("Checking #{storage.root}")
54
+ offenses = nil
55
+ time = Benchmark.measure do
56
+ offenses = analyzer.analyze_theme do |path, i, total|
57
+ @bridge.send_work_done_progress_report(token, "#{i}/#{total} #{path}", (i.to_f / total * 100.0).to_i)
58
+ end
59
+ end
60
+ end_message = "Found #{offenses.size} offenses in #{format("%0.2f", time.real)}s"
61
+ @bridge.send_work_done_progress_end(token, end_message)
62
+ @bridge.log(end_message)
63
+ send_diagnostics(offenses)
64
+ end
65
+
66
+ def run_partial_theme_check(absolute_path_or_paths, theme, analyzer, only_single_file)
67
+ raise 'Unsafe operation' unless @diagnostics_lock.owned?
68
+
69
+ relative_paths = as_array(absolute_path_or_paths).map do |absolute_path|
70
+ Pathname.new(storage.relative_path(absolute_path))
71
+ end
72
+
73
+ theme_files = relative_paths
74
+ .map { |relative_path| theme[relative_path] }
75
+ .reject(&:nil?)
76
+
77
+ deleted_relative_paths = relative_paths - theme_files.map(&:relative_path)
78
+ deleted_relative_paths
79
+ .each { |p| send_clearing_diagnostics(p) }
80
+
81
+ token = @bridge.send_create_work_done_progress_request
82
+ @bridge.send_work_done_progress_begin(token, "Partial theme check")
83
+ offenses = nil
84
+ time = Benchmark.measure do
85
+ offenses = analyzer.analyze_files(theme_files, only_single_file: only_single_file) do |path, i, total|
86
+ @bridge.send_work_done_progress_report(token, "#{i}/#{total} #{path}", (i.to_f / total * 100.0).to_i)
87
+ end
88
+ end
89
+ end_message = "Found #{offenses.size} new offenses in #{format("%0.2f", time.real)}s"
90
+ @bridge.send_work_done_progress_end(token, end_message)
91
+ @bridge.log(end_message)
92
+ send_diagnostics(offenses, theme_files.map(&:relative_path), only_single_file: only_single_file)
93
+ end
94
+
95
+ def send_clearing_diagnostics(relative_path)
96
+ raise 'Unsafe operation' unless @diagnostics_lock.owned?
97
+
98
+ relative_path = Pathname.new(relative_path) unless relative_path.is_a?(Pathname)
99
+ @diagnostics_manager.clear_diagnostics(relative_path)
100
+ send_diagnostic(relative_path, DiagnosticsManager::NO_DIAGNOSTICS)
101
+ end
102
+
103
+ def as_array(maybe_array)
104
+ case maybe_array
105
+ when Array
106
+ maybe_array
107
+ else
108
+ [maybe_array]
109
+ end
110
+ end
111
+
66
112
  def send_diagnostics(offenses, analyzed_files = nil, only_single_file: false)
67
113
  @diagnostics_manager.build_diagnostics(
68
114
  offenses,