platformos-check 0.4.8 → 0.4.10

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +27 -0
  3. data/README.md +28 -19
  4. data/config/default.yml +13 -1
  5. data/docs/checks/form_action.md +6 -0
  6. data/docs/checks/form_authenticity_token.md +22 -3
  7. data/docs/checks/include_in_render.md +62 -0
  8. data/docs/checks/translation_files_match.md +70 -0
  9. data/docs/checks/translation_key_exists.md +44 -0
  10. data/docs/platformos-check.jpg +0 -0
  11. data/lib/platformos_check/app.rb +13 -0
  12. data/lib/platformos_check/app_file.rb +10 -3
  13. data/lib/platformos_check/checks/convert_include_to_render.rb +41 -2
  14. data/lib/platformos_check/checks/form_action.rb +3 -1
  15. data/lib/platformos_check/checks/form_authenticity_token.rb +20 -0
  16. data/lib/platformos_check/checks/img_lazy_loading.rb +6 -2
  17. data/lib/platformos_check/checks/include_in_render.rb +45 -0
  18. data/lib/platformos_check/checks/invalid_args.rb +4 -1
  19. data/lib/platformos_check/checks/translation_files_match.rb +83 -0
  20. data/lib/platformos_check/checks/translation_key_exists.rb +48 -0
  21. data/lib/platformos_check/checks/undefined_object.rb +55 -26
  22. data/lib/platformos_check/checks/unreachable_code.rb +9 -9
  23. data/lib/platformos_check/checks/unused_assign.rb +33 -24
  24. data/lib/platformos_check/cli.rb +1 -1
  25. data/lib/platformos_check/ext/hash.rb +19 -0
  26. data/lib/platformos_check/graphql_file.rb +4 -0
  27. data/lib/platformos_check/language_server/constants.rb +18 -2
  28. data/lib/platformos_check/language_server/document_link_provider.rb +67 -10
  29. data/lib/platformos_check/language_server/document_link_providers/localize_document_link_provider.rb +38 -0
  30. data/lib/platformos_check/language_server/document_link_providers/theme_render_document_link_provider.rb +2 -1
  31. data/lib/platformos_check/language_server/document_link_providers/translation_document_link_provider.rb +36 -0
  32. data/lib/platformos_check/tags/graphql.rb +3 -0
  33. data/lib/platformos_check/translation_file.rb +40 -0
  34. data/lib/platformos_check/version.rb +1 -1
  35. data/lib/platformos_check/yaml_file.rb +7 -2
  36. data/lib/platformos_check.rb +1 -0
  37. metadata +13 -4
  38. data/docs/preview.png +0 -0
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlatformosCheck
4
+ class TranslationFilesMatch < YamlCheck
5
+ severity :error
6
+ category :translation
7
+ doc docs_url(__FILE__)
8
+
9
+ PLURALIZATION_KEYS = Set.new(%w[zero one two few many other])
10
+
11
+ def on_file(file)
12
+ return unless file.translation?
13
+ return if file.parse_error
14
+ return add_offense_wrong_language_in_file(file) if file.language != file.language_from_path
15
+ return check_if_file_exists_for_all_other_languages(file) if file.language == @platformos_app.default_language
16
+
17
+ default_language_file = @platformos_app.grouped_files[PlatformosCheck::TranslationFile][file.name.sub(file.language, @platformos_app.default_language)]
18
+
19
+ return add_offense_missing_file(file) if default_language_file.nil?
20
+
21
+ add_offense_different_structure(file, default_language_file) unless same_structure?(default_language_file.content[@platformos_app.default_language], file.content[file.language])
22
+ end
23
+
24
+ protected
25
+
26
+ def add_offense_wrong_language_in_file(file)
27
+ add_offense("Mismatch detected - file inside #{file.language_from_path} directory defines translations for `#{file.language}`", app_file: file) do |_corrector|
28
+ file.update_contents(file.content[file.language_from_path] = file.content.delete(file.content[file.language]))
29
+ file.write
30
+ end
31
+ end
32
+
33
+ def add_offense_missing_file(file)
34
+ add_offense("Mismatch detected - missing `#{file.relative_path.to_s.sub(file.language, @platformos_app.default_language)}` to define translations the default language", app_file: file)
35
+ end
36
+
37
+ def check_if_file_exists_for_all_other_languages(file)
38
+ @platformos_app.translations_hash.each_key do |lang|
39
+ next if lang == @platformos_app.default_language
40
+
41
+ language_file = @platformos_app.grouped_files[PlatformosCheck::TranslationFile][file.name.sub(file.language, lang)]
42
+ add_offense_missing_translation_file(file, lang) if language_file.nil?
43
+ end
44
+ end
45
+
46
+ def add_offense_missing_translation_file(file, lang)
47
+ missing_file_path = file.relative_path.to_s.sub(file.language, lang)
48
+ add_offense("Mismatch detected - missing `#{missing_file_path}` file to define translations for `#{lang}`", app_file: file) do |corrector|
49
+ missing_file_content = file.content.clone
50
+ missing_file_content[lang] = missing_file_content.delete(file.language)
51
+ corrector.create_file(@platformos_app.storage, missing_file_path, YAML.dump(missing_file_content))
52
+ end
53
+ end
54
+
55
+ def same_structure?(hash1, hash2)
56
+ if !hash1.is_a?(Hash) && !hash2.is_a?(Hash)
57
+ true
58
+ elsif (hash1.is_a?(Hash) && !hash2.is_a?(Hash)) || (!hash1.is_a?(Hash) && hash2.is_a?(Hash))
59
+ false
60
+ elsif pluralization?(hash1) && pluralization?(hash2)
61
+ true
62
+ elsif hash1.keys.map(&:to_s).sort != hash2.keys.map(&:to_s).sort
63
+ false
64
+ else
65
+ hash1.keys.all? { |key| same_structure?(hash1[key], hash2[key]) }
66
+ end
67
+ end
68
+
69
+ def add_offense_different_structure(file, default_language_file)
70
+ add_offense("Mismatch detected - structure differs from the default language file #{default_language_file.relative_path}", app_file: file) do |_corrector|
71
+ file.content[file.language].transform_values! { |v| v.nil? ? {} : v }
72
+ file.content[file.language] = default_language_file.content[default_language_file.language].deep_merge(file.content[file.language])
73
+ file.write
74
+ end
75
+ end
76
+
77
+ def pluralization?(hash)
78
+ hash.all? do |key, value|
79
+ PLURALIZATION_KEYS.include?(key) && !value.is_a?(Hash)
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlatformosCheck
4
+ # Recommends using {% liquid ... %} if 5 or more consecutive {% ... %} are found.
5
+ class TranslationKeyExists < LiquidCheck
6
+ severity :error
7
+ categories :translation, :liquid
8
+ doc docs_url(__FILE__)
9
+
10
+ def on_variable(node)
11
+ return unless node.value.name.is_a?(String)
12
+ return unless node.filters.size == 1
13
+
14
+ translation_filter = node.filters.detect { |f| TranslationFile::TRANSLATION_FILTERS.include?(f[0]) }
15
+ return unless translation_filter
16
+ return unless translation_filter
17
+
18
+ filter_attributes = translation_filter[2] || {}
19
+
20
+ return unless filter_attributes['default'].nil?
21
+ return if !filter_attributes['scope'].nil? && !filter_attributes['scope'].is_a?(String)
22
+
23
+ lang = filter_attributes['language'].is_a?(String) ? filter_attributes['language'] : @platformos_app.default_language
24
+ translation_components = node.value.name.split('.')
25
+
26
+ translation_components = filter_attributes['scope'].split('.') + translation_components if filter_attributes['scope']
27
+
28
+ return add_translation_offense(node:, lang:) if @platformos_app.translations_hash.empty?
29
+
30
+ hash = @platformos_app.translations_hash[lang] || {}
31
+ index = 0
32
+ while translation_components[index]
33
+ hash = hash[translation_components[index]]
34
+ if hash.nil?
35
+ add_translation_offense(node:, lang:)
36
+ break
37
+ end
38
+ index += 1
39
+ end
40
+ end
41
+
42
+ protected
43
+
44
+ def add_translation_offense(node:, lang:)
45
+ add_offense("Translation `#{lang}.#{node.value.name}` does not exists", node:)
46
+ end
47
+ end
48
+ end
@@ -6,6 +6,9 @@ module PlatformosCheck
6
6
  doc docs_url(__FILE__)
7
7
  severity :error
8
8
 
9
+ NOTIFICATION_GLOBAL_OBJECTS = %w[data response form].freeze
10
+ FORM_GLOBAL_OBJECTS = %w[form form_builder].freeze
11
+
9
12
  class TemplateInfo
10
13
  def initialize(app_file: nil)
11
14
  @all_variable_lookups = {}
@@ -19,7 +22,8 @@ module PlatformosCheck
19
22
  attr_reader :all_assigns, :all_captures, :all_forloops, :app_file, :all_renders
20
23
 
21
24
  def add_render(name:, node:)
22
- @all_renders[name] = node
25
+ @all_renders[name] ||= []
26
+ @all_renders[name] << node
23
27
  end
24
28
 
25
29
  def add_variable_lookup(name:, node:)
@@ -39,8 +43,10 @@ module PlatformosCheck
39
43
  end
40
44
 
41
45
  def each_partial
42
- @all_renders.each do |(name, info)|
43
- yield [name, info]
46
+ @all_renders.each do |(name, nodes)|
47
+ nodes.each do |node|
48
+ yield [name, node]
49
+ end
44
50
  end
45
51
  end
46
52
 
@@ -56,14 +62,17 @@ module PlatformosCheck
56
62
  yield [key, info]
57
63
  end
58
64
  end
65
+
66
+ def first_declaration(name)
67
+ [all_assigns[name], all_captures[name]].compact.min_by(&:line_number)
68
+ end
59
69
  end
60
70
 
61
71
  def self.single_file(**_args)
62
72
  true
63
73
  end
64
74
 
65
- def initialize(config_type: :default)
66
- @config_type = config_type
75
+ def initialize
67
76
  @files = {}
68
77
  end
69
78
 
@@ -72,15 +81,15 @@ module PlatformosCheck
72
81
  end
73
82
 
74
83
  def on_assign(node)
75
- @files[node.app_file.name].all_assigns[node.value.to] = node
84
+ @files[node.app_file.name].all_assigns[node.value.to] ||= node
76
85
  end
77
86
 
78
87
  def on_capture(node)
79
- @files[node.app_file.name].all_captures[node.value.instance_variable_get(:@to)] = node
88
+ @files[node.app_file.name].all_captures[node.value.instance_variable_get(:@to)] ||= node
80
89
  end
81
90
 
82
91
  def on_parse_json(node)
83
- @files[node.app_file.name].all_captures[node.value.to] = node
92
+ @files[node.app_file.name].all_captures[node.value.to] ||= node
84
93
  end
85
94
 
86
95
  def on_for(node)
@@ -102,7 +111,7 @@ module PlatformosCheck
102
111
  end
103
112
 
104
113
  def on_function(node)
105
- @files[node.app_file.name].all_assigns[node.value.to] = node
114
+ @files[node.app_file.name].all_assigns[node.value.to] ||= node
106
115
 
107
116
  return unless node.value.from.is_a?(String)
108
117
 
@@ -113,13 +122,13 @@ module PlatformosCheck
113
122
  end
114
123
 
115
124
  def on_graphql(node)
116
- @files[node.app_file.name].all_assigns[node.value.to] = node
125
+ @files[node.app_file.name].all_assigns[node.value.to] ||= node
117
126
  end
118
127
 
119
128
  def on_background(node)
120
129
  return unless node.value.partial_syntax
121
130
 
122
- @files[node.app_file.name].all_assigns[node.value.to] = node
131
+ @files[node.app_file.name].all_assigns[node.value.to] ||= node
123
132
 
124
133
  return unless node.value.partial_name.is_a?(String)
125
134
 
@@ -155,10 +164,10 @@ module PlatformosCheck
155
164
  each_template do |(_name, info)|
156
165
  if info.app_file.notification?
157
166
  # NOTE: `data` comes from graphql for notifications
158
- check_object(info, all_global_objects + %w[data response form])
167
+ check_object(info, all_global_objects + NOTIFICATION_GLOBAL_OBJECTS)
159
168
  elsif info.app_file.form?
160
169
  # NOTE: `data` comes from graphql for notifications
161
- check_object(info, all_global_objects + %w[form form_builder])
170
+ check_object(info, all_global_objects + FORM_GLOBAL_OBJECTS)
162
171
  else
163
172
  check_object(info, all_global_objects)
164
173
  end
@@ -167,42 +176,37 @@ module PlatformosCheck
167
176
 
168
177
  private
169
178
 
170
- attr_reader :config_type
171
-
172
179
  def each_template
173
180
  @files.each do |(name, info)|
174
181
  yield [name, info]
175
182
  end
176
183
  end
177
184
 
178
- def check_object(info, all_global_objects, render_node = nil, visited_partials = Set.new, level = 0)
185
+ def check_object(info, all_global_objects, render_node = nil, level = 0)
179
186
  return if level > 1
180
187
 
181
188
  check_undefined(info, all_global_objects, render_node) unless info.app_file.partial? && render_node.nil? # ||
182
189
 
183
190
  info.each_partial do |(partial_name, node)|
184
- next if visited_partials.include?(partial_name)
191
+ next unless @files[partial_name] # NOTE: undefined partial
185
192
 
186
193
  partial_info = @files[partial_name]
187
-
188
- next unless partial_info # NOTE: undefined partial
189
-
190
194
  partial_variables = node.value.attributes.keys +
191
195
  [node.value.instance_variable_get(:@alias_name)]
192
- visited_partials << partial_name
193
- check_object(partial_info, all_global_objects + partial_variables, node, visited_partials, level + 1)
196
+
197
+ check_object(partial_info, all_global_objects + partial_variables, node, level + 1)
194
198
  end
195
199
  end
196
200
 
197
201
  def check_undefined(info, all_global_objects, render_node)
198
- all_variables = info.all_variables
199
202
  potentially_unused_variables = render_node.value.attributes.keys if render_node
203
+ missing_arguments = []
200
204
  info.each_variable_lookup(!!render_node) do |(key, node)|
201
205
  name, line_number = key
202
206
 
203
207
  potentially_unused_variables&.delete(name)
204
208
 
205
- next if all_variables.include?(name)
209
+ next if info.all_variables.include?(name) && variable_declared_before_used?(name, info, line_number)
206
210
  next if all_global_objects.include?(name)
207
211
 
208
212
  node = node.parent
@@ -211,7 +215,7 @@ module PlatformosCheck
211
215
  next if node.variable? && node.filters.any? { |(filter_name)| filter_name == "default" }
212
216
 
213
217
  if render_node
214
- add_offense("Missing argument `#{name}`", node: render_node)
218
+ missing_arguments << name
215
219
  elsif !info.app_file.partial?
216
220
  add_offense("Undefined object `#{name}`", node:, line_number:)
217
221
  end
@@ -219,8 +223,33 @@ module PlatformosCheck
219
223
 
220
224
  potentially_unused_variables -= render_node.value.internal_attributes if render_node && render_node.value.respond_to?(:internal_attributes)
221
225
  potentially_unused_variables&.each do |name|
222
- add_offense("Unused argument `#{name}`", node: render_node)
226
+ add_offense("Unused argument `#{name}`", node: render_node) do |corrector|
227
+ match = render_node.markup.match(/(?<attribute>,?\s*#{name}\s*:\s*#{Liquid::QuotedFragment})\s*/)
228
+
229
+ corrector.replace(render_node, render_node.markup.sub(match[:attribute], ''), render_node.start_index...render_node.end_index)
230
+ end
223
231
  end
232
+
233
+ return if missing_arguments.empty?
234
+
235
+ add_offense("Missing arguments: #{missing_arguments.map { |name| "`#{name}`" }.join(', ')}", node: render_node) do |corrector|
236
+ new_attributes = ''
237
+ missing_arguments.each do |name|
238
+ new_attributes += ", #{name}: "
239
+ new_attributes += @files[render_node.app_file.name].all_assigns.key?(name) ? name : 'null'
240
+ end
241
+
242
+ start_pos = render_node.end_index
243
+ start_pos -= 1 while start_pos > 0 && render_node.source[start_pos - 1] == ' '
244
+ corrector.replace(render_node, new_attributes, start_pos...start_pos)
245
+ end
246
+ end
247
+
248
+ def variable_declared_before_used?(name, info, line_number_when_used)
249
+ declaration = info.first_declaration(name)
250
+ return true if declaration.nil?
251
+
252
+ declaration.line_number <= line_number_when_used
224
253
  end
225
254
  end
226
255
  end
@@ -8,7 +8,7 @@ module PlatformosCheck
8
8
 
9
9
  FLOW_COMMAND = %i[break continue return]
10
10
  CONDITION_TYPES = Set.new(%i[condition else_condition])
11
- INCLUDE_FLOW_COMMAND = %w[break]
11
+ INCLUDE_FLOW_COMMAND = %w[break].freeze
12
12
 
13
13
  def on_document(node)
14
14
  @processed_files = {}
@@ -94,16 +94,16 @@ module PlatformosCheck
94
94
  @processed_files[path]
95
95
  end
96
96
 
97
- def include_node_contains_flow_command?(root)
98
- return false if root.nil?
97
+ def include_node_contains_flow_command?(node)
98
+ return false if node.nil?
99
99
 
100
- root.nodelist.any? do |node|
101
- if INCLUDE_FLOW_COMMAND.include?(node.respond_to?(:tag_name) && node.tag_name)
100
+ node.nodelist.any? do |n|
101
+ if INCLUDE_FLOW_COMMAND.include?(n.respond_to?(:tag_name) && n.tag_name)
102
102
  true
103
- elsif node.respond_to?(:nodelist) && node.nodelist
104
- include_node_contains_flow_command?(node)
105
- elsif node.respond_to?(:tag_name) && node.tag_name == 'include' && node.template_name_expr.is_a?(String)
106
- evaluate_include(node.template_name_expr)
103
+ elsif n.respond_to?(:nodelist) && n.nodelist
104
+ include_node_contains_flow_command?(n)
105
+ elsif n.respond_to?(:tag_name) && n.tag_name == 'include' && n.template_name_expr.is_a?(String)
106
+ evaluate_include(n.template_name_expr)
107
107
  else
108
108
  false
109
109
  end
@@ -7,6 +7,13 @@ module PlatformosCheck
7
7
  category :liquid
8
8
  doc docs_url(__FILE__)
9
9
 
10
+ TAGS_FOR_AUTO_VARIABLE_PREPEND = Set.new(%i[graphql function background]).freeze
11
+ FILTERS_THAT_MODIFY_OBJECT = Set.new(%w[array_add add_to_array
12
+ prepend_to_array array_prepend
13
+ assign_to_hash_key hash_add_key add_hash_key
14
+ remove_hash_key hash_delete_key delete_hash_key]).freeze
15
+ PREPEND_CHARACTER = '_'
16
+
10
17
  class TemplateInfo < Struct.new(:used_assigns, :assign_nodes, :includes)
11
18
  def collect_used_assigns(templates, visited = Set.new)
12
19
  collected = used_assigns
@@ -34,7 +41,8 @@ module PlatformosCheck
34
41
  end
35
42
 
36
43
  def on_assign(node)
37
- return if ignore_underscored?(node)
44
+ return if ignore_prepended?(node)
45
+ return if node.value.from.filters.any? { |filter_name, *_arguments| FILTERS_THAT_MODIFY_OBJECT.include?(filter_name) }
38
46
 
39
47
  @templates[node.app_file.name].assign_nodes[node.value.to] = node
40
48
  end
@@ -44,13 +52,21 @@ module PlatformosCheck
44
52
  end
45
53
 
46
54
  def on_function(node)
47
- return if ignore_underscored?(node)
55
+ return if ignore_prepended?(node)
48
56
 
49
57
  @templates[node.app_file.name].assign_nodes[node.value.to] = node
50
58
  end
51
59
 
52
60
  def on_graphql(node)
53
- return if ignore_underscored?(node)
61
+ return if node.value.to.nil?
62
+ return if ignore_prepended?(node)
63
+
64
+ @templates[node.app_file.name].assign_nodes[node.value.to] = node
65
+ end
66
+
67
+ def on_background(node)
68
+ return if node.value.to.nil?
69
+ return if ignore_prepended?(node)
54
70
 
55
71
  @templates[node.app_file.name].assign_nodes[node.value.to] = node
56
72
  end
@@ -77,25 +93,8 @@ module PlatformosCheck
77
93
  next if used.include?(name)
78
94
 
79
95
  add_offense("`#{name}` is never used", node:) do |corrector|
80
- case node.type_name
81
- when :graphql
82
- offset = node.markup.match(/^graphql\s+/)[0].size
83
-
84
- corrector.insert_before(
85
- node,
86
- '_',
87
- (node.start_index + offset)...(node.start_index + offset)
88
- )
89
- when :function
90
- offset = node.markup.match(/^function\s+/)[0].size
91
-
92
- corrector.insert_before(
93
- node,
94
- '_',
95
- (node.start_index + offset)...(node.start_index + offset)
96
- )
97
- when :parse_json
98
- # noop
96
+ if TAGS_FOR_AUTO_VARIABLE_PREPEND.include?(node.type_name)
97
+ prepend_variable(node, corrector)
99
98
  else
100
99
  corrector.remove(node)
101
100
  end
@@ -106,8 +105,18 @@ module PlatformosCheck
106
105
 
107
106
  private
108
107
 
109
- def ignore_underscored?(node)
110
- node.value.to.start_with?('_')
108
+ def ignore_prepended?(node)
109
+ node.value.to.start_with?(PREPEND_CHARACTER)
110
+ end
111
+
112
+ def prepend_variable(node, corrector)
113
+ offset = node.markup.match(/^#{node.type_name}\s+/)[0].size
114
+
115
+ corrector.insert_before(
116
+ node,
117
+ PREPEND_CHARACTER,
118
+ (node.start_index + offset)...(node.start_index + offset)
119
+ )
111
120
  end
112
121
  end
113
122
  end
@@ -188,7 +188,7 @@ module PlatformosCheck
188
188
  def check(out_stream = STDOUT)
189
189
  update_docs
190
190
 
191
- warn "Checking #{@config.root} ..."
191
+ warn "Checking #{@config.root}:"
192
192
  storage = PlatformosCheck::FileSystemStorage.new(@config.root, ignored_patterns: @config.ignored_patterns)
193
193
  raise Abort, "No platformos_app files found." if storage.platformos_app.all.empty?
194
194
 
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Hash
4
+ def deep_merge!(other_hash, &block)
5
+ merge!(other_hash) do |key, this_val, other_val|
6
+ if this_val.is_a?(Hash) && other_val.is_a?(Hash)
7
+ this_val.deep_merge(other_val, &block)
8
+ elsif block
9
+ yield(key, this_val, other_val)
10
+ else
11
+ other_val
12
+ end
13
+ end
14
+ end
15
+
16
+ def deep_merge(other_hash, &)
17
+ dup.deep_merge!(other_hash, &)
18
+ end
19
+ end
@@ -71,6 +71,10 @@ module PlatformosCheck
71
71
  end
72
72
  end
73
73
 
74
+ def optional_arguments
75
+ @optional_arguments ||= defined_arguments - required_arguments
76
+ end
77
+
74
78
  def defined_arguments
75
79
  @defined_arguments ||= variables.map(&:name)
76
80
  end
@@ -32,13 +32,29 @@ module PlatformosCheck
32
32
  PARTIAL_GRAPHQL = partial_tag_with_result('graphql')
33
33
  PARTIAL_BACKGROUND = partial_tag_with_result('background')
34
34
 
35
+ TAGS_FOR_FILTERS = 'echo|print|log|hash_assign|assign'
36
+ TRANSLATION_FILTERS_NAMES = 'translate|t_escape|translate_escape|t[^\\w]'
37
+ OPTIONAL_SCOPE_ARGUMENT = %((:?([\\w:'"\\s]*)\\s*(scope:\\s*['"](?<scope>[^'"]*)['"]))?)
38
+
39
+ LOCALIZE_FILTERS_NAMES = ''
40
+
35
41
  ASSET_INCLUDE = /
36
42
  \{\{-?\s*'(?<partial>[^']*)'\s*\|\s*asset_url|
37
43
  \{\{-?\s*"(?<partial>[^"]*)"\s*\|\s*asset_url|
38
44
 
39
45
  # in liquid tags the whole line is white space until the asset partial
40
- ^\s*(?:echo|assign[^=]*=)\s*'(?<partial>[^']*)'\s*\|\s*asset_url|
41
- ^\s*(?:echo|assign[^=]*=)\s*"(?<partial>[^"]*)"\s*\|\s*asset_url
46
+ ^\s*(?:#{TAGS_FOR_FILTERS}[^=]*=)\s*'(?<partial>[^']*)'\s*\|\s*asset_url|
47
+ ^\s*(?:#{TAGS_FOR_FILTERS}[^=]*=)\s*"(?<partial>[^"]*)"\s*\|\s*asset_url
48
+ /mix
49
+
50
+ TRANSLATION_FILTER = /
51
+ '(?<key>[^']*)'\s*\|\s*(#{TRANSLATION_FILTERS_NAMES})#{OPTIONAL_SCOPE_ARGUMENT}|
52
+ "(?<key>[^"]*)"\s*\|\s*(#{TRANSLATION_FILTERS_NAMES})#{OPTIONAL_SCOPE_ARGUMENT}
53
+ /mix
54
+
55
+ LOCALIZE_FILTER = /
56
+ [\s\w'"-:.]+\|\s*(localize|l):\s*'(?<key>[^']*)'|
57
+ [\s\w'"-:.]+\|\s*(localize|l):\s*"(?<key>[^"]*)"
42
58
  /mix
43
59
  end
44
60
  end
@@ -7,6 +7,16 @@ module PlatformosCheck
7
7
  include PositionHelper
8
8
  include URIHelper
9
9
 
10
+ class DefaultTranslationFile
11
+ def initialize(default_language)
12
+ @default_language = default_language
13
+ end
14
+
15
+ def relative_path
16
+ Pathname.new(@default_language, "#{@default_language}.yml")
17
+ end
18
+ end
19
+
10
20
  class << self
11
21
  attr_accessor :partial_regexp, :app_file_type, :default_dir, :default_extension
12
22
 
@@ -41,18 +51,12 @@ module PlatformosCheck
41
51
 
42
52
  def document_links(buffer, platformos_app)
43
53
  matches(buffer, partial_regexp).map do |match|
44
- start_row, start_column = from_index_to_row_column(
45
- buffer,
46
- match.begin(:partial)
47
- )
54
+ start_row, start_column = start_coordinates(buffer, match)
48
55
 
49
- end_row, end_column = from_index_to_row_column(
50
- buffer,
51
- match.end(:partial)
52
- )
56
+ end_row, end_column = end_coordinates(buffer, match)
53
57
 
54
58
  {
55
- target: file_link(match[:partial], platformos_app),
59
+ target: file_link(match, platformos_app),
56
60
  range: {
57
61
  start: {
58
62
  line: start_row,
@@ -67,13 +71,66 @@ module PlatformosCheck
67
71
  end
68
72
  end
69
73
 
70
- def file_link(partial, platformos_app)
74
+ def start_coordinates(buffer, match)
75
+ from_index_to_row_column(
76
+ buffer,
77
+ match.begin(:partial)
78
+ )
79
+ end
80
+
81
+ def end_coordinates(buffer, match)
82
+ from_index_to_row_column(
83
+ buffer,
84
+ match.end(:partial)
85
+ )
86
+ end
87
+
88
+ def file_link(match, platformos_app)
89
+ partial = match[:partial]
71
90
  relative_path = platformos_app.send(app_file_type).detect { |f| f.name == partial }&.relative_path
72
91
  relative_path ||= default_relative_path(partial)
73
92
 
74
93
  file_uri(@storage.path(relative_path))
75
94
  end
76
95
 
96
+ def translation_file_link(match, platformos_app)
97
+ @current_best_fit = platformos_app.translations.first || DefaultTranslationFile.new(platformos_app.default_language)
98
+ @current_best_fit_level = 0
99
+ array_of_translation_components = translation_components_for_match(match)
100
+ platformos_app.translations.each do |translation_file|
101
+ array_of_translation_components.each do |translation_components|
102
+ exact_match_level = translation_components.size
103
+ component_result = translation_file.content[platformos_app.default_language]
104
+ next if component_result.nil?
105
+
106
+ i = 0
107
+ while i < exact_match_level
108
+ component_result = yaml(component_result, translation_components[i])
109
+
110
+ break if component_result.nil?
111
+
112
+ i += 1
113
+ if i > @current_best_fit_level
114
+ @current_best_fit = translation_file
115
+ @current_best_fit_level = i
116
+ end
117
+
118
+ break unless component_result.is_a?(Hash)
119
+ end
120
+ end
121
+ end
122
+
123
+ file_uri(@storage.path(@current_best_fit&.relative_path))
124
+ end
125
+
126
+ def translation_components_for_match(match)
127
+ raise NotImplementedError
128
+ end
129
+
130
+ def yaml(component_result, component)
131
+ component_result[component]
132
+ end
133
+
77
134
  def default_relative_path(partial)
78
135
  return Pathname.new("app/#{default_dir}/#{partial}#{default_extension}") unless partial.start_with?('modules/')
79
136
 
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlatformosCheck
4
+ module LanguageServer
5
+ class LocalizeDocumentLinkProvider < DocumentLinkProvider
6
+ @partial_regexp = LOCALIZE_FILTER
7
+ @app_file_type = :translations
8
+ @default_dir = 'translations'
9
+ @default_extension = '.yml'
10
+
11
+ def file_link(match, platformos_app)
12
+ translation_file_link(match, platformos_app)
13
+ end
14
+
15
+ def translation_components_for_match(match)
16
+ key = match[:key].split('.')
17
+ [
18
+ %w[time formats] + key,
19
+ %w[date formats] + key
20
+ ]
21
+ end
22
+
23
+ def start_coordinates(buffer, match)
24
+ from_index_to_row_column(
25
+ buffer,
26
+ match.begin(:key)
27
+ )
28
+ end
29
+
30
+ def end_coordinates(buffer, match)
31
+ from_index_to_row_column(
32
+ buffer,
33
+ match.end(:key)
34
+ )
35
+ end
36
+ end
37
+ end
38
+ end