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 +4 -4
- data/CHANGELOG.md +21 -0
- data/README.md +13 -4
- data/config/default.yml +4 -1
- data/docs/checks/form_action.md +6 -0
- data/docs/checks/form_authenticity_token.md +22 -3
- data/docs/checks/include_in_render.md +62 -0
- data/lib/platformos_check/checks/convert_include_to_render.rb +41 -2
- data/lib/platformos_check/checks/form_action.rb +3 -1
- data/lib/platformos_check/checks/form_authenticity_token.rb +20 -0
- data/lib/platformos_check/checks/img_lazy_loading.rb +6 -2
- data/lib/platformos_check/checks/include_in_render.rb +45 -0
- data/lib/platformos_check/checks/invalid_args.rb +4 -1
- data/lib/platformos_check/checks/undefined_object.rb +55 -26
- data/lib/platformos_check/checks/unreachable_code.rb +9 -9
- data/lib/platformos_check/checks/unused_assign.rb +33 -24
- data/lib/platformos_check/cli.rb +1 -1
- data/lib/platformos_check/graphql_file.rb +4 -0
- data/lib/platformos_check/tags/graphql.rb +3 -0
- data/lib/platformos_check/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e41eee4e2aa717be9663df4efbc1acf79bc8b571784808b80177f9298fdad9e9
|
4
|
+
data.tar.gz: 6bed8f9e1e3e22dbd02cee00a15d5e9ce1787f7826ee09e3e461fcf0161c681d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/docs/checks/form_action.md
CHANGED
@@ -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
|
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
|
-
|
13
|
-
|
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]
|
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,
|
43
|
-
|
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
|
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]
|
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)]
|
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]
|
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]
|
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]
|
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]
|
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 +
|
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 +
|
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,
|
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
|
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
|
-
|
193
|
-
check_object(partial_info, all_global_objects + partial_variables, node,
|
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
|
-
|
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?(
|
98
|
-
return false if
|
97
|
+
def include_node_contains_flow_command?(node)
|
98
|
+
return false if node.nil?
|
99
99
|
|
100
|
-
|
101
|
-
if INCLUDE_FLOW_COMMAND.include?(
|
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
|
104
|
-
include_node_contains_flow_command?(
|
105
|
-
elsif
|
106
|
-
evaluate_include(
|
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
|
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
|
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
|
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
|
-
|
81
|
-
|
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
|
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
|
data/lib/platformos_check/cli.rb
CHANGED
@@ -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
|
|
@@ -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
|
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.
|
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:
|
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
|