platformos-check 0.4.7 → 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: aebbd738b1045c874460b7f1de2cf2774297f36136fce816521dda29ce371652
4
- data.tar.gz: c348e3afc68f2a664fe1ad313d41497ad7c977f8de72f3b82df9dda297926c38
3
+ metadata.gz: e41eee4e2aa717be9663df4efbc1acf79bc8b571784808b80177f9298fdad9e9
4
+ data.tar.gz: 6bed8f9e1e3e22dbd02cee00a15d5e9ce1787f7826ee09e3e461fcf0161c681d
5
5
  SHA512:
6
- metadata.gz: 368d39315a05ab34eff3797012af56775ca9a5ff4d8209ed0c44aa2916f97fe5740afa859bbfbbdf34409aa40967efe4236b2394eafcdecfc313c97f62a4dba7
7
- data.tar.gz: f912bda1de7e0be84e887ab2f19cde9072e03468ea52265f910e20ff5a7317b7f7f4e6fe83aef690350f21543ffbd495e01c72eddca5c393acfc5500055d6939
6
+ metadata.gz: 71a9feab2a3d5e31a2436652036bf1669c65424f24cfa550feedb49bd43e03264f2feb51b0367c84a79805c3cda0d24ed3ccccdd27d2b63a09dd15f3e26a94b4
7
+ data.tar.gz: 56961261716dae13fce53dadb2c6e03d8c3f268195748e81783cfe4dd7162c18ffb05063530975939ad2805f2a2e33d5d63aab1da51de65f3c98e17e1e2df2b4
data/CHANGELOG.md CHANGED
@@ -1,4 +1,30 @@
1
- v0.4.7 / 2023-12-22
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
+
22
+ v0.4.8 / 2023-12-20
23
+ ==================
24
+
25
+ * Add GraphqlInForLoop check
26
+
27
+ v0.4.7 / 2023-12-27
2
28
  ==================
3
29
 
4
30
  * Add UnreachableCode check
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
@@ -99,6 +102,10 @@ FormAuthenticityToken:
99
102
  enabled: true
100
103
  ignore: []
101
104
 
105
+ GraphqlInForLoop:
106
+ enabled: true
107
+ ignore: []
108
+
102
109
  HtmlParsingError:
103
110
  enabled: true
104
111
  ignore: []
@@ -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,56 @@
1
+ # GraphQL in for loop (`GraphqlInForLoop`)
2
+
3
+ This check is aimed towards identifying performance problems before they arise. Invoking GraphQL queries/mutations inside the `for loop` can lead to performance issues such as increased database load, higher latency, and inefficient use of network resources. It is particularly problematic when dealing with large datasets or when the number of entities grows.
4
+
5
+ Invoking a GraphQL query within a `for loop` might also be a sign of a so called N+1 problem. The N+1 pattern often arises when dealing with relationships between entities. In an attempt to retrieve related data, developers may inadvertently end up executing a large number of queries, resulting in a significant performance overhead.
6
+
7
+ To address this problem, developers can use techniques like eager loading, which involves fetching the related data in a single query instead of issuing separate queries for each entity. This helps reduce the number of database round-trips and improves overall system performance. In platformOS the most common technique to fix the N+1 issue is by [using related_records][relalted_records].
8
+
9
+ ## Check Details
10
+
11
+ This check is aimed towards identifying GraphQL queries invoked within a for loop.
12
+
13
+ :-1: Examples of **incorrect** code for this check:
14
+
15
+ ```liquid
16
+ {% assign arr = 'a,b,c' | split: ','}
17
+ {% for el in arr %}
18
+ {% graphql g = 'my/graphql', el: el %}
19
+ {% print el %}
20
+ {% endfor %}
21
+
22
+ ```
23
+
24
+ :+1: Examples of **correct** code for this check:
25
+
26
+ ```liquid
27
+ {% assign arr = 'a,b,c' | split: ','}
28
+ {% graphql g = 'my/graphql', arr: arr %}
29
+ ```
30
+
31
+ ## Check Options
32
+
33
+ The default configuration for this check is the following:
34
+
35
+ ```yaml
36
+ GraphqlInForLoop:
37
+ enabled: true
38
+ ```
39
+
40
+ ## When Not To Use It
41
+
42
+ In the perfect world, there should be no cases where disabling this rule is needed - platformOS most likely already has a way to solve a problem without using GraphQL query / mutation in the for loop.
43
+
44
+ ## Version
45
+
46
+ This check has been introduced in PlatformOS Check 0.4.9.
47
+
48
+ ## Resources
49
+
50
+ - [Rule Source][codesource]
51
+ - [Documentation Source][docsource]
52
+ - [platformOS - loading related records][related_records]
53
+
54
+ [codesource]: /lib/platformos_check/checks/graphql_in_for_loop.rb
55
+ [docsource]: /docs/checks/graphql_in_for_loop.md
56
+ [related_records]: https://documentation.platformos.com/developer-guide/records/loading-related-records
@@ -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
@@ -2,13 +2,15 @@
2
2
 
3
3
  module PlatformosCheck
4
4
  class Analyzer
5
- def initialize(platformos_app, checks = Check.all.map(&:new), auto_correct = false)
5
+ def initialize(platformos_app, checks = Check.all.map(&:new), auto_correct = false, store_warnings = false)
6
6
  @platformos_app = platformos_app
7
7
  @auto_correct = auto_correct
8
+ @store_warnings = store_warnings
8
9
 
9
10
  @liquid_checks = Checks.new
10
11
  @yaml_checks = Checks.new
11
12
  @html_checks = Checks.new
13
+ @warnings = {}
12
14
 
13
15
  checks.each do |check|
14
16
  check.platformos_app = @platformos_app
@@ -30,6 +32,8 @@ module PlatformosCheck
30
32
  @html_checks.flat_map(&:offenses)
31
33
  end
32
34
 
35
+ attr_reader :warnings
36
+
33
37
  def yaml_file_count
34
38
  @yaml_file_count ||= @platformos_app.yaml.size
35
39
  end
@@ -54,6 +58,7 @@ module PlatformosCheck
54
58
  yield(liquid_file.relative_path.to_s, i, total_file_count) if block_given?
55
59
  liquid_visitor.visit_liquid_file(liquid_file)
56
60
  html_visitor.visit_liquid_file(liquid_file)
61
+ @warnings[liquid_file.path] = liquid_file.warnings if @store_warnings && !liquid_file.warnings&.empty?
57
62
  end
58
63
  end
59
64
 
@@ -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
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlatformosCheck
4
+ class GraphqlInForLoop < LiquidCheck
5
+ severity :suggestion
6
+ categories :liquid, :performance
7
+ doc docs_url(__FILE__)
8
+
9
+ PARTIAL_TAG = %i[render include]
10
+ OFFENSE_MSG = "Do not invoke GraphQL in a for loop"
11
+
12
+ class PartialInfo
13
+ attr_reader :node, :app_file
14
+
15
+ def initialize(node:, app_file:)
16
+ @node = node
17
+ @app_file = app_file
18
+ end
19
+ end
20
+
21
+ def self.single_file(**_args)
22
+ true
23
+ end
24
+
25
+ def initialize
26
+ @partials = []
27
+ @all_partials = Set.new
28
+ end
29
+
30
+ def on_for(_node)
31
+ @in_for = true
32
+ end
33
+
34
+ def on_graphql(node)
35
+ add_graphql_offense(node:, graphql_node: node) if should_report?
36
+ end
37
+
38
+ def after_for(_node)
39
+ @in_for = false
40
+ end
41
+
42
+ def on_background(_node)
43
+ @in_background = true
44
+ end
45
+
46
+ def after_background(_node)
47
+ @in_background = false
48
+ end
49
+
50
+ def on_include(node)
51
+ return unless should_report?
52
+
53
+ add_partial(path: node.value.template_name_expr, node:)
54
+ end
55
+
56
+ def on_render(node)
57
+ return unless should_report?
58
+
59
+ add_partial(path: node.value.template_name_expr, node:)
60
+ end
61
+
62
+ def on_function(node)
63
+ return unless should_report?
64
+
65
+ add_partial(path: node.value.from, node:)
66
+ end
67
+
68
+ def on_end
69
+ while (partial_info = @partials.shift)
70
+ report_offense_on_graphql(LiquidNode.new(partial_info.app_file.parse.root, nil, partial_info.app_file), offense_node: partial_info.node)
71
+ end
72
+ end
73
+
74
+ protected
75
+
76
+ def add_partial(path:, node:)
77
+ return unless path.is_a?(String)
78
+ return if @all_partials.include?(path)
79
+ return if @platformos_app.grouped_files[PlatformosCheck::PartialFile][path].nil?
80
+
81
+ @all_partials << path
82
+ @partials << PartialInfo.new(node:, app_file: @platformos_app.grouped_files[PlatformosCheck::PartialFile][path])
83
+ end
84
+
85
+ def should_report?
86
+ @in_for && !@in_background
87
+ end
88
+
89
+ def add_graphql_offense(node:, graphql_node:)
90
+ return add_offense(OFFENSE_MSG, node:) unless graphql_node.value.partial_name
91
+
92
+ partial_name = graphql_node.value.partial_name.is_a?(String) ? graphql_node.value.partial_name : "variable: #{graphql_node.value.partial_name.name}"
93
+ add_offense("#{OFFENSE_MSG} (#{partial_name})", node:)
94
+ end
95
+
96
+ def report_offense_on_graphql(node, offense_node:)
97
+ if node.type_name == :graphql
98
+ add_graphql_offense(node: offense_node, graphql_node: node)
99
+ elsif PARTIAL_TAG.include?(node.type_name)
100
+ add_partial(path: node.value.template_name_expr, node: offense_node)
101
+ elsif node.type_name == :function
102
+ add_partial(path: node.value.from, node: offense_node)
103
+ elsif node.children && !node.children.empty?
104
+ node.children.each { |c| report_offense_on_graphql(c, offense_node:) }
105
+ end
106
+ end
107
+ end
108
+ 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
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PlatformosCheck
4
- # Recommends using {% liquid ... %} if 5 or more consecutive {% ... %} are found.
5
4
  class UnreachableCode < LiquidCheck
6
5
  severity :error
7
6
  category :liquid
@@ -9,7 +8,7 @@ module PlatformosCheck
9
8
 
10
9
  FLOW_COMMAND = %i[break continue return]
11
10
  CONDITION_TYPES = Set.new(%i[condition else_condition])
12
- INCLUDE_FLOW_COMMAND = %w[break]
11
+ INCLUDE_FLOW_COMMAND = %w[break].freeze
13
12
 
14
13
  def on_document(node)
15
14
  @processed_files = {}
@@ -95,16 +94,16 @@ module PlatformosCheck
95
94
  @processed_files[path]
96
95
  end
97
96
 
98
- def include_node_contains_flow_command?(root)
99
- return false if root.nil?
97
+ def include_node_contains_flow_command?(node)
98
+ return false if node.nil?
100
99
 
101
- root.nodelist.any? do |node|
102
- 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)
103
102
  true
104
- elsif node.respond_to?(:nodelist) && node.nodelist
105
- include_node_contains_flow_command?(node)
106
- elsif node.respond_to?(:tag_name) && node.tag_name == 'include' && node.template_name_expr.is_a?(String)
107
- 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)
108
107
  else
109
108
  false
110
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
@@ -68,7 +68,7 @@ module PlatformosCheck
68
68
  end
69
69
 
70
70
  def warnings
71
- @ast.warnings
71
+ parse&.warnings
72
72
  end
73
73
 
74
74
  def root
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlatformosCheck
4
+ module Tags
5
+ class Context < Base
6
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
7
+ def children
8
+ @node.attributes_expr.values
9
+ end
10
+ end
11
+ end
12
+ end
13
+ 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
@@ -53,6 +53,8 @@ module PlatformosCheck
53
53
  register_tag('background', Background)
54
54
  register_tag('content_for', ContentFor)
55
55
  register_tag('session', Session)
56
+ register_tag('context', Context)
57
+ register_tag('context_rc', Context)
56
58
  register_tag('sign_in', SignIn)
57
59
  register_tag('yield', Yield)
58
60
  register_tag('graphql', Graphql)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PlatformosCheck
4
- VERSION = "0.4.7"
4
+ VERSION = "0.4.9"
5
5
  end
@@ -47,6 +47,7 @@ require_relative "platformos_check/tags/base"
47
47
  require_relative "platformos_check/tags/base_block"
48
48
  require_relative "platformos_check/tags/background"
49
49
  require_relative "platformos_check/tags/cache"
50
+ require_relative "platformos_check/tags/context"
50
51
  require_relative "platformos_check/tags/export"
51
52
  require_relative "platformos_check/tags/form"
52
53
  require_relative "platformos_check/tags/function"
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.7
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-27 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
@@ -119,9 +119,11 @@ files:
119
119
  - docs/checks/deprecated_filter.md
120
120
  - docs/checks/form_action.md
121
121
  - docs/checks/form_authenticity_token.md
122
+ - docs/checks/graphql_in_for_loop.md
122
123
  - docs/checks/html_parsing_error.md
123
124
  - docs/checks/img_lazy_loading.md
124
125
  - docs/checks/img_width_and_height.md
126
+ - docs/checks/include_in_render.md
125
127
  - docs/checks/invalid_args.md
126
128
  - docs/checks/liquid_tag.md
127
129
  - docs/checks/missing_enable_comment.md
@@ -164,9 +166,11 @@ files:
164
166
  - lib/platformos_check/checks/deprecated_filter.rb
165
167
  - lib/platformos_check/checks/form_action.rb
166
168
  - lib/platformos_check/checks/form_authenticity_token.rb
169
+ - lib/platformos_check/checks/graphql_in_for_loop.rb
167
170
  - lib/platformos_check/checks/html_parsing_error.rb
168
171
  - lib/platformos_check/checks/img_lazy_loading.rb
169
172
  - lib/platformos_check/checks/img_width_and_height.rb
173
+ - lib/platformos_check/checks/include_in_render.rb
170
174
  - lib/platformos_check/checks/invalid_args.rb
171
175
  - lib/platformos_check/checks/liquid_tag.rb
172
176
  - lib/platformos_check/checks/missing_enable_comment.rb
@@ -316,6 +320,7 @@ files:
316
320
  - lib/platformos_check/tags/base_block.rb
317
321
  - lib/platformos_check/tags/base_tag_methods.rb
318
322
  - lib/platformos_check/tags/cache.rb
323
+ - lib/platformos_check/tags/context.rb
319
324
  - lib/platformos_check/tags/export.rb
320
325
  - lib/platformos_check/tags/form.rb
321
326
  - lib/platformos_check/tags/function.rb