platformos-check 0.4.8 → 0.4.9

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 579d15b95d0e00e65eb562778f0870bc28698bfe2f6c97ca66e7ca81a18b855c
4
- data.tar.gz: 2f6afe88f067cccc169d62a9744399b5b11294537dd8c7653ca9d5ed9165f1f3
3
+ metadata.gz: e41eee4e2aa717be9663df4efbc1acf79bc8b571784808b80177f9298fdad9e9
4
+ data.tar.gz: 6bed8f9e1e3e22dbd02cee00a15d5e9ce1787f7826ee09e3e461fcf0161c681d
5
5
  SHA512:
6
- metadata.gz: 2fa86abf31229208eaff48e7f95b212977682bd560234d99d2fa9b3162a33ca7139b8faad2960152951295b056ef41b834e9faa756b0627f02a14034f6d4e468
7
- data.tar.gz: 18fa38e2613faf4e31dfdf0a432e4fbe3a8ced0b18f8f194698bf73606affd770cf2589dadb852c26c19f5ec31146535e95e863d7c31ab9c8977feb67fa99a07
6
+ metadata.gz: 71a9feab2a3d5e31a2436652036bf1669c65424f24cfa550feedb49bd43e03264f2feb51b0367c84a79805c3cda0d24ed3ccccdd27d2b63a09dd15f3e26a94b4
7
+ data.tar.gz: 56961261716dae13fce53dadb2c6e03d8c3f268195748e81783cfe4dd7162c18ffb05063530975939ad2805f2a2e33d5d63aab1da51de65f3c98e17e1e2df2b4
data/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
1
+ v0.4.9 / 2024-01-10
2
+ ==================
3
+
4
+ * Skip FormAuthenticityToken check for GET forms
5
+ * Skip FormAuthenticityToken for action which is not relative path
6
+ * Fix FormAction to not report offenses on valid scenarios
7
+ * UnusedAssign will not automatically remove assign if it might change the business logic (which is a scenario when filters modifying objects are used)
8
+ * UnusedAssign will automatically rename result of background tag if variable not used
9
+ * Fix reporting UndefinedObject's missing argument offenses when the same partial is used multiple times (previously offenses where displayed only for the last render)
10
+ * Add autocorrector for UndefinedObject's missing argument error (explicitly provide null)
11
+ * Add autocorrector for ImgLazyLoading
12
+ * ConvertIncludeToRender will not report offense as autocorrect
13
+ * Improve inline GraphQL syntax check to raise error if result variable not provided
14
+ * Add autocorrector for UndefinedObject (Unused Argument offense) (FIXME: for N unused arguments in the same line it needs to be invoked N times)
15
+ * Add autocorrector for InvalidArgs - remove duplicates arguments
16
+ * Do not report ConvertIncludeToRender offenses for valid use cases (using `break` and using variable as a template name)
17
+ * Add IncludeInRender check
18
+ * Improve autocorrector for UndefinedObject's missing argument error - if variable is defined, it will be passed instead of hardcoding null
19
+ * Re-enable autocorrector for ConvertIncludeToRender
20
+ * Make UndefinedObject more clever - it will report undefined object if variable is used before declaration
21
+
1
22
  v0.4.8 / 2023-12-20
2
23
  ==================
3
24
 
data/README.md CHANGED
@@ -11,7 +11,7 @@ PlatformOS Check currently checks for the following:
11
11
  ✅ Liquid syntax errors
12
12
  ✅ JSON syntax errors
13
13
  ✅ Missing partials and graphqls
14
- ✅ Unused `{% assign ... %}`
14
+ ✅ Unused variables (via `{% assign var = ... %}`, {% function var = ... %} etc.)
15
15
  ✅ Unused partials
16
16
  ✅ Template length
17
17
  ✅ Deprecated tags
@@ -23,14 +23,14 @@ PlatformOS Check currently checks for the following:
23
23
  ✅ Deprecated filters
24
24
  ✅ Missing `platformos-check-enable` comment
25
25
  ✅ Invalid arguments provided to `{% graphql %}` tags
26
+ ✅ Missing `authenticity_token` in `<form>`
27
+ ✅ Unreachable code
26
28
 
27
29
  As well as checks that prevent easy to spot performance problems:
28
30
 
31
+ ✅ [GraphQL in for loop](/docs/checks/graphql_in_for_loop.md)
29
32
  ✅ Use of [parser-blocking](/docs/checks/parser_blocking_javascript.md) JavaScript
30
- ✅ [Use of non-platformOS domains for assets](/docs/checks/remote_asset.md)
31
33
  ✅ [Missing width and height attributes on `img` tags](/docs/checks/img_width_and_height.md)
32
- ✅ [Too much JavaScript](/docs/checks/asset_size_javascript.md)
33
- ✅ [Too much CSS](/docs/checks/asset_size_css.md)
34
34
 
35
35
  For detailed descriptions and configuration options, [take a look at the complete list.](/docs/checks/)
36
36
 
@@ -52,12 +52,21 @@ With more to come! Suggestions welcome ([create an issue](https://github.com/Pla
52
52
  ### Install ruby and platform-check gem
53
53
 
54
54
  1. Download the latest version of Ruby - https://www.ruby-lang.org/en/documentation/installation/
55
+
56
+ Verify that you've installed at least version 3.2:
57
+
58
+ `ruby -v`
59
+
60
+ ⚠️ **Note:** You might need to restart the terminal after installing.
61
+ ⚠️ **Note:*** Please make sure you install ruby for your user, not the root
62
+
55
63
  2. Install platformos-check gem
56
64
 
57
65
  `gem install platformos-check`
58
66
 
59
67
  You can verify the installation was successful by invoking `platformos-check --version`. If you chose this method, use `platformos-check-language-server` as a path to your language server instead of `/Users/<username/platformos-check-language-server`
60
68
 
69
+ ⚠️ **Note:*** Please make sure you install the gem for your user, not the root - i.e. without `sudo`
61
70
 
62
71
  ### Using Docker
63
72
 
data/config/default.yml CHANGED
@@ -15,6 +15,10 @@ ConvertIncludeToRender:
15
15
  enabled: true
16
16
  ignore: []
17
17
 
18
+ IncludeInRender:
19
+ enabled: true
20
+ ignore: []
21
+
18
22
  LiquidTag:
19
23
  enabled: true
20
24
  ignore: []
@@ -69,7 +73,6 @@ ValidYaml:
69
73
  UndefinedObject:
70
74
  enabled: true
71
75
  ignore: []
72
- config_type: :default
73
76
 
74
77
  DeprecatedFilter:
75
78
  enabled: true
@@ -30,6 +30,12 @@ This check is aimed at ensuring you have not forgotten to start the path with /.
30
30
  </form>
31
31
  ```
32
32
 
33
+ ```liquid
34
+ <form action="https://example.com/external">
35
+ ...
36
+ </form>
37
+ ```
38
+
33
39
  ## Check Options
34
40
 
35
41
  The default configuration for this check is the following:
@@ -3,7 +3,7 @@
3
3
  In platformOS all POST/PATCH/PUT/DELETE requests are protected from [CSRF Attacks][csrf-attack] through [authenticity_token][page-csrf]
4
4
  Form action defines the endpoint to which browser will make a request after submitting it.
5
5
 
6
- As a general rule you should include hidden input `<input type="hidden" name="authenticity_token" value="{{ context.authenticity_token }}">` in every form. Missing it will result in session invalidation and any logged in user will be automatically logged out.
6
+ As a general rule you should include hidden input `<input type="hidden" name="authenticity_token" value="{{ context.authenticity_token }}">` in every form. Missing it will result in session invalidation and the logged in user will be automatically logged out.
7
7
 
8
8
  ## Check Details
9
9
 
@@ -12,18 +12,37 @@ This check is aimed at ensuring you have not forgotten to include authenticity_t
12
12
  :-1: Examples of **incorrect** code for this check:
13
13
 
14
14
  ```liquid
15
- <form action="dummy/create">
15
+ <form action="/dummy/create" method="post">
16
16
  </form>
17
17
  ```
18
18
 
19
19
  :+1: Examples of **correct** code for this check:
20
20
 
21
+ With token:
21
22
  ```liquid
22
- <form action="/dummy/create">
23
+ <form action="/dummy/create" method="post">
23
24
  <input type="hidden" name="authenticity_token" value="{{ context.authenticity_token }}">
24
25
  </form>
25
26
  ```
26
27
 
28
+ For GET request:
29
+ ```liquid
30
+ <form action="/dummy/create">
31
+ </form>
32
+ ```
33
+
34
+ For external request:
35
+ ```liquid
36
+ <form action="https://example.com/dummy/create" method="post">
37
+ </form>
38
+ ```
39
+
40
+ For parameterized request:
41
+ ```liquid
42
+ <form action="{{ context.constants.MY_REQUEST_URL }}" method="post">
43
+ </form>
44
+ ```
45
+
27
46
  ## Check Options
28
47
 
29
48
  The default configuration for this check is the following:
@@ -0,0 +1,62 @@
1
+ # Reports usage of `include` tag inside `render` (`IncludeInRender`)
2
+
3
+ Runtime error is used when `include` tag is used inside `render` tag.
4
+
5
+ ## Check Details
6
+
7
+ This check is aimed at eliminating the use of `include` tags `render` tag.
8
+
9
+ :-1: Examples of **incorrect** code for this check:
10
+
11
+ ```liquid
12
+ {% liquid
13
+ # app/views/pages/index.liquid
14
+ render 'foo'
15
+ %}
16
+ ```liquid
17
+ {% liquid
18
+ # app/views/partials/foo.liquid
19
+ include 'bar'
20
+ %}
21
+ ```
22
+
23
+ :+1: Examples of **correct** code for this check:
24
+
25
+ ```liquid
26
+ {% liquid
27
+ # app/views/pages/index.liquid
28
+ render 'foo'
29
+ %}
30
+ ```liquid
31
+ {% liquid
32
+ # app/views/partials/foo.liquid
33
+ render 'bar'
34
+ %}
35
+ ```
36
+
37
+ ## Check Options
38
+
39
+ The default configuration for this check is the following:
40
+
41
+ ```yaml
42
+ IncludeInRender:
43
+ enabled: true
44
+ ```
45
+
46
+ ## When Not To Use It
47
+
48
+ It is discouraged to disable this rule.
49
+
50
+ ## Version
51
+
52
+ This check has been introduced in PlatformOS Check 0.4.9.
53
+
54
+ ## Resources
55
+
56
+ - [Deprecated Tags Reference][deprecated]
57
+ - [Rule Source][codesource]
58
+ - [Documentation Source][docsource]
59
+
60
+ [deprecated]: https://documentation.platformos.com/api-reference/liquid/include
61
+ [codesource]: /lib/platformos_check/checks/convert_include_to_render.rb
62
+ [docsource]: /docs/checks/convert_include_to_render.md
@@ -3,15 +3,54 @@
3
3
  module PlatformosCheck
4
4
  # Recommends replacing `include` for `render`
5
5
  class ConvertIncludeToRender < LiquidCheck
6
+ RENDER_INCOMPATIBLE_TAGS = %w[break include].freeze
7
+
6
8
  severity :suggestion
7
9
  category :liquid
8
10
  doc docs_url(__FILE__)
9
11
 
12
+ def initialize
13
+ @processed_files = {}
14
+ end
15
+
10
16
  def on_include(node)
17
+ return if allowed_usecase?(node)
18
+
11
19
  add_offense("`include` is deprecated - convert it to `render`", node:) do |corrector|
12
- # We need to fix #445 and pass the variables from the context or don't replace at all.
13
- # corrector.replace(node, "render \'#{node.value.template_name_expr}\' ")
20
+ match = node.markup.match(/(?<include>include\s*)/)
21
+ corrector.replace(node, node.markup.sub(match[:include], 'render '), node.start_index...node.end_index)
22
+ end
23
+ end
24
+
25
+ protected
26
+
27
+ def allowed_usecase?(node)
28
+ return true if name_is_variable?(node)
29
+ return true if include_node_contains_render_incompatible_tag?(root_node_from_include(node.value.template_name_expr))
30
+
31
+ false
32
+ end
33
+
34
+ def name_is_variable?(node)
35
+ !node.value.template_name_expr.is_a?(String)
36
+ end
37
+
38
+ def include_node_contains_render_incompatible_tag?(node)
39
+ return false if node.nil?
40
+
41
+ node.nodelist.any? do |n|
42
+ if RENDER_INCOMPATIBLE_TAGS.include?(n.respond_to?(:tag_name) && n.tag_name)
43
+ true
44
+ elsif n.respond_to?(:nodelist) && n.nodelist
45
+ include_node_contains_render_incompatible_tag?(n)
46
+ else
47
+ false
48
+ end
14
49
  end
15
50
  end
51
+
52
+ def root_node_from_include(path)
53
+ @platformos_app.grouped_files[PlatformosCheck::PartialFile][path]&.parse&.root
54
+ end
16
55
  end
17
56
  end
@@ -6,11 +6,13 @@ module PlatformosCheck
6
6
  categories :html
7
7
  doc docs_url(__FILE__)
8
8
 
9
+ VALID_ACTION_START = ['/', '{%', '{{', '#', 'http'].freeze
10
+
9
11
  def on_form(node)
10
12
  action = node.attributes["action"]&.strip
11
13
  return if action.nil?
12
14
  return if action.empty?
13
- return if action.start_with?('/', '{{')
15
+ return if action.start_with?(*VALID_ACTION_START)
14
16
 
15
17
  add_offense("Use action=\"/#{action}\" (start with /) to ensure the form can be submitted multiple times in case of validation errors", node:)
16
18
  end
@@ -9,6 +9,9 @@ module PlatformosCheck
9
9
  AUTHENTICITY_TOKEN_VALUE = /\A\s*{{\s*context\.authenticity_token\s*}}\s*\z/
10
10
 
11
11
  def on_form(node)
12
+ return if method_is_get(node.attributes['method'])
13
+ return unless action_is_relative_url(node.attributes['action'])
14
+
12
15
  authenticity_toke_inputs = node.children.select { |c| c.name == 'input' && c.attributes['name'] == 'authenticity_token' && c.attributes['value']&.match?(AUTHENTICITY_TOKEN_VALUE) }
13
16
  return if authenticity_toke_inputs.size == 1
14
17
  return add_offense('Duplicated authenticity_token inputs', node:) if authenticity_toke_inputs.size > 1
@@ -17,5 +20,22 @@ module PlatformosCheck
17
20
  corrector.insert_after(node, "\n<input type=\"hidden\" name=\"authenticity_token\" value=\"{{ context.authenticity_token }}\">")
18
21
  end
19
22
  end
23
+
24
+ protected
25
+
26
+ def method_is_get(method)
27
+ return true if method.nil?
28
+
29
+ method = method.downcase.strip
30
+ return true if method == ''
31
+
32
+ method == 'get'
33
+ end
34
+
35
+ def action_is_relative_url(action)
36
+ return true if action.nil?
37
+
38
+ action.lstrip[0] == '/'
39
+ end
20
40
  end
21
41
  end
@@ -6,13 +6,17 @@ module PlatformosCheck
6
6
  categories :html, :performance
7
7
  doc docs_url(__FILE__)
8
8
 
9
- ACCEPTED_LOADING_VALUES = %w[lazy eager]
9
+ ACCEPTED_LOADING_VALUES = Set.new(%w[lazy eager]).freeze
10
+ LOADING_DEFAULT_ATTRIBUTE = ' loading="eager"'
10
11
 
11
12
  def on_img(node)
12
13
  loading = node.attributes["loading"]&.downcase
13
14
  return if ACCEPTED_LOADING_VALUES.include?(loading)
14
15
 
15
- add_offense("Use loading=\"eager\" for images visible in the viewport on load and loading=\"lazy\" for others", node:)
16
+ add_offense("Use loading=\"eager\" for images visible in the viewport on load and loading=\"lazy\" for others", node:) do |corrector|
17
+ start_pos = node.start_index + node.markup.index('>')
18
+ corrector.insert_after(node, LOADING_DEFAULT_ATTRIBUTE, start_pos...start_pos)
19
+ end
16
20
  end
17
21
  end
18
22
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlatformosCheck
4
+ # Recommends replacing `include` for `render`
5
+ class IncludeInRender < LiquidCheck
6
+ severity :error
7
+ category :liquid
8
+ doc docs_url(__FILE__)
9
+
10
+ def initialize
11
+ @processed_files = {}
12
+ end
13
+
14
+ def on_render(node)
15
+ path = node.value.template_name_expr
16
+ return unless include_tag_in_render?(root_node_for_render(path))
17
+
18
+ add_offense("`render` context does not allow to use `include`, either remove all includes from `#{app_file_for_path(path).relative_path}` or change `render` to `include`", node:)
19
+ end
20
+
21
+ protected
22
+
23
+ def include_tag_in_render?(node)
24
+ return false if node.nil?
25
+
26
+ node.nodelist.any? do |n|
27
+ if n.respond_to?(:tag_name) && n.tag_name == 'include'
28
+ true
29
+ elsif n.respond_to?(:nodelist) && n.nodelist
30
+ include_tag_in_render?(n)
31
+ else
32
+ false
33
+ end
34
+ end
35
+ end
36
+
37
+ def root_node_for_render(path)
38
+ app_file_for_path(path)&.parse&.root
39
+ end
40
+
41
+ def app_file_for_path(path)
42
+ @platformos_app.grouped_files[PlatformosCheck::PartialFile][path]
43
+ end
44
+ end
45
+ end
@@ -42,7 +42,10 @@ module PlatformosCheck
42
42
 
43
43
  def add_duplicated_key_offense(node)
44
44
  node.value.duplicated_attrs.each do |duplicated_arg|
45
- add_offense("Duplicated argument `#{duplicated_arg}`", node:)
45
+ add_offense("Duplicated argument `#{duplicated_arg}`", node:) do |corrector|
46
+ match = node.markup.match(/(?<attribute>,?\s*#{duplicated_arg}\s*:\s*#{Liquid::QuotedFragment})\s*/)
47
+ corrector.replace(node, node.markup.sub(match[:attribute], ''), node.start_index...node.end_index)
48
+ end
46
49
  end
47
50
  end
48
51
  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.sort_by(&:line_number).first
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
 
@@ -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
@@ -5,6 +5,7 @@ module PlatformosCheck
5
5
  class Graphql < Base
6
6
  QUERY_NAME_SYNTAX = /(#{Liquid::VariableSignature}+)\s*=\s*(.*)\s*/om
7
7
  INLINE_SYNTAX = /(#{Liquid::QuotedFragment}+)(\s*(#{Liquid::QuotedFragment}+))?/o
8
+ INLINE_SYNTAX_WITHOUT_RESULT_VARIABLE = /\A([\w\-\.\[\]])+\s*:\s*/om
8
9
  CLOSE_TAG_SYNTAX = /\A(.*)(?-mix:\{%-?)\s*(\w+)\s*(.*)?(?-mix:%\})\z/m # based on Liquid::Raw::FullTokenPossiblyInvalid
9
10
 
10
11
  attr_reader :to, :from, :inline_query, :value_expr, :partial_name, :attributes_expr, :attributes
@@ -28,6 +29,8 @@ module PlatformosCheck
28
29
  @partial_name = value_expr
29
30
  @from = Liquid::Variable.new(after_assign_markup.join('|'), options)
30
31
  elsif INLINE_SYNTAX.match?(markup)
32
+ raise Liquid::SyntaxError, 'Invalid syntax for inline graphql tag - missing result name. Valid syntax: graphql result, arg1: var1, ...' if markup.match?(INLINE_SYNTAX_WITHOUT_RESULT_VARIABLE)
33
+
31
34
  @inline_query = true
32
35
  parse_markup(tag_name, markup)
33
36
  @attributes = attributes_expr.keys
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PlatformosCheck
4
- VERSION = "0.4.8"
4
+ VERSION = "0.4.9"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: platformos-check
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.8
4
+ version: 0.4.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Piotr Bliszczyk
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: exe
12
12
  cert_chain: []
13
- date: 2023-12-29 00:00:00.000000000 Z
13
+ date: 2024-01-10 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: graphql
@@ -123,6 +123,7 @@ files:
123
123
  - docs/checks/html_parsing_error.md
124
124
  - docs/checks/img_lazy_loading.md
125
125
  - docs/checks/img_width_and_height.md
126
+ - docs/checks/include_in_render.md
126
127
  - docs/checks/invalid_args.md
127
128
  - docs/checks/liquid_tag.md
128
129
  - docs/checks/missing_enable_comment.md
@@ -169,6 +170,7 @@ files:
169
170
  - lib/platformos_check/checks/html_parsing_error.rb
170
171
  - lib/platformos_check/checks/img_lazy_loading.rb
171
172
  - lib/platformos_check/checks/img_width_and_height.rb
173
+ - lib/platformos_check/checks/include_in_render.rb
172
174
  - lib/platformos_check/checks/invalid_args.rb
173
175
  - lib/platformos_check/checks/liquid_tag.rb
174
176
  - lib/platformos_check/checks/missing_enable_comment.rb