platformos-check 0.4.8 → 0.4.10

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