theme-check 1.0.0 → 1.1.0

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: 94b918e53777cfb70c65dacd9d9bfbdb6f4463597c8f9c81969e1baef89df7a1
4
- data.tar.gz: 7e45af9a3dfeab1eafdff5533df68a9e0d67007d94661a2c974c0c2fece61095
3
+ metadata.gz: adc4aaf3260408a9b52d9afaa59662308f173a5d5dcd0f3663cbcc68da1d7f39
4
+ data.tar.gz: 50cd7e0d42fda99ad3816a846c25868c38b92ea521e0be759c515548dd82f70b
5
5
  SHA512:
6
- metadata.gz: ea1b75ca11a66aefe5eef421f577d53716cb180799863185d36fad21a9033b5cf52579df3e593e41867513da410adc7d16de723f35b3a5435e50098b7af2a348
7
- data.tar.gz: ceab091f6bf7317d899cb5459601b7dfb5294e6e4be962a421b33bad33c74fd730001dea90687f81f754d57e5cdedc5b39d0efb5d95d6c016ed965f7e4e86a9f
6
+ metadata.gz: 7ffad3310e441edd678143de233584a09167ffff1e09b96109cef417d360d797821dc9ef3491af4564608cd3b35aa59be41ebea6bb76030e1544d53bc9db8d02
7
+ data.tar.gz: 30b6d8f3b58b95c833a78bc30ceea6cfcfa35d563e3d184b6aee51817965be87b352c2dbad5ff67a74f2a64479fe1945c11db738c1bfb6e64f610ae15b862cb3
data/CHANGELOG.md CHANGED
@@ -1,4 +1,17 @@
1
1
 
2
+ v1.1.0 / 2021-07-06
3
+ ==================
4
+
5
+ * Add `--fail-level` CLI flag to configure exit code
6
+ * Refactor all theme file classes to inherit from `ThemeFile`
7
+ * Fix `undefined method liquid?` error when scanning from LSP
8
+ * Adding asset document links
9
+ * Allow initializing theme app extension configuration files
10
+ * Allow disabling registering mock Liquid tags w/ `ThemeCheck::Tags.register_tags = false`
11
+ * Support Theme App Extensions
12
+ * Add checks for theme app extension block JS/CSS
13
+ * Disable Liquid::C when parsing Liquid templates
14
+
2
15
  v1.0.0 / 2021-06-28
3
16
  ==================
4
17
 
data/README.md CHANGED
@@ -100,6 +100,8 @@ TemplateLength:
100
100
  # Or ignore certain paths
101
101
  ignore:
102
102
  - snippets/icon-*
103
+ # Or change the severity (error|suggestion|style)
104
+ severity: suggestion
103
105
 
104
106
  # Enable a custom check
105
107
  MyCustomCheck
@@ -141,3 +143,40 @@ Disable checks for the _entire document_ by placing the comment on the first lin
141
143
 
142
144
  {%assign x = 1%}
143
145
  ```
146
+
147
+ ## Exit Code and `--fail-level`
148
+
149
+ Use the `--fail-level` (default: `error`) flag to configure the exit code of theme-check. Useful in CI scenarios.
150
+
151
+ Example:
152
+
153
+ ```
154
+ # Make CI fail on styles warnings, suggestions, and errors
155
+ theme-check --fail-level style path_to_theme
156
+
157
+ # Make CI fail on suggestions, and errors
158
+ theme-check --fail-level suggestion path_to_theme
159
+
160
+ # Make CI fail on errors
161
+ theme-check path_to_theme
162
+ ```
163
+
164
+ There are three fail levels:
165
+
166
+ - `error`
167
+ - `suggestion`
168
+ - `style`
169
+
170
+ Exit code meanings:
171
+
172
+ - 0: Success!
173
+ - 1: Your code doesn't pass the checks
174
+ - 2: There's a bug in theme-check
175
+
176
+ If you would like to change the severity of a check, you can do so with the `severity` attribute. Example:
177
+
178
+ ```yaml
179
+ DeprecateLazysizes:
180
+ enabled: true
181
+ severity: error
182
+ ```
data/config/default.yml CHANGED
@@ -1,7 +1,13 @@
1
1
  root: .
2
2
 
3
+ extends: :nothing
4
+
3
5
  require: []
4
6
 
7
+ include_categories: []
8
+
9
+ exclude_categories: []
10
+
5
11
  ignore:
6
12
  - node_modules/*
7
13
 
@@ -161,3 +167,13 @@ ImgLazyLoading:
161
167
  HtmlParsingError:
162
168
  enabled: true
163
169
  ignore: []
170
+
171
+ AssetSizeAppBlockJavaScript:
172
+ enabled: false
173
+ ignore: []
174
+ threshold_in_bytes: 10_000
175
+
176
+ AssetSizeAppBlockCSS:
177
+ enabled: false
178
+ ignore: []
179
+ threshold_in_bytes: 100_000
@@ -0,0 +1,11 @@
1
+ root: .
2
+
3
+ require: []
4
+
5
+ include_categories: []
6
+
7
+ exclude_categories: []
8
+
9
+ ignore:
10
+ - node_modules/*
11
+
@@ -0,0 +1,153 @@
1
+ root: .
2
+
3
+ extends: :nothing
4
+
5
+ require: []
6
+
7
+ include_categories: []
8
+
9
+ exclude_categories: []
10
+
11
+ ignore:
12
+ - node_modules/*
13
+
14
+ ConvertIncludeToRender:
15
+ enabled: true
16
+ ignore: []
17
+
18
+ LiquidTag:
19
+ enabled: true
20
+ ignore: []
21
+ min_consecutive_statements: 4
22
+
23
+ MissingTemplate:
24
+ enabled: true
25
+ ignore: []
26
+
27
+ NestedSnippet:
28
+ enabled: true
29
+ ignore: []
30
+ max_nesting_level: 3
31
+
32
+ RequiredLayoutThemeObject:
33
+ enabled: true
34
+ ignore: []
35
+
36
+ SpaceInsideBraces:
37
+ enabled: true
38
+ ignore: []
39
+
40
+ SyntaxError:
41
+ enabled: true
42
+ ignore: []
43
+
44
+ TemplateLength:
45
+ enabled: true
46
+ ignore: []
47
+ max_length: 200
48
+ # Exclude content of {% schema %} in line count
49
+ exclude_schema: true
50
+
51
+ UnknownFilter:
52
+ enabled: true
53
+ ignore: []
54
+
55
+ UnusedAssign:
56
+ enabled: true
57
+ ignore: []
58
+
59
+ UnusedSnippet:
60
+ enabled: true
61
+ ignore: []
62
+
63
+ MatchingSchemaTranslations:
64
+ enabled: true
65
+ ignore: []
66
+
67
+ MatchingTranslations:
68
+ enabled: true
69
+ ignore: []
70
+
71
+ TranslationKeyExists:
72
+ enabled: true
73
+ ignore: []
74
+
75
+ ValidHTMLTranslation:
76
+ enabled: true
77
+ ignore: []
78
+
79
+ ValidJson:
80
+ enabled: true
81
+ ignore: []
82
+
83
+ ValidSchema:
84
+ enabled: true
85
+ ignore: []
86
+
87
+ UndefinedObject:
88
+ enabled: true
89
+ ignore: []
90
+ exclude_snippets: true
91
+
92
+ RequiredDirectories:
93
+ enabled: false
94
+ ignore: []
95
+
96
+ DeprecatedFilter:
97
+ enabled: true
98
+ ignore: []
99
+
100
+ MissingEnableComment:
101
+ enabled: true
102
+ ignore: []
103
+
104
+ ParserBlockingJavaScript:
105
+ enabled: true
106
+ ignore: []
107
+
108
+ ParserBlockingScriptTag:
109
+ enabled: true
110
+
111
+ AssetSizeJavaScript:
112
+ enabled: true
113
+ ignore: []
114
+ threshold_in_bytes: 10_000
115
+
116
+ AssetSizeCSS:
117
+ enabled: true
118
+ ignore: []
119
+ threshold_in_bytes: 100_000
120
+
121
+ ImgWidthAndHeight:
122
+ enabled: true
123
+ ignore: []
124
+
125
+ RemoteAsset:
126
+ enabled: true
127
+ ignore: []
128
+
129
+ AssetUrlFilters:
130
+ enabled: true
131
+ ignore: []
132
+
133
+ ContentForHeaderModification:
134
+ enabled: true
135
+ ignore: []
136
+
137
+ ImgLazyLoading:
138
+ enabled: true
139
+ ignore: []
140
+
141
+ HtmlParsingError:
142
+ enabled: true
143
+ ignore: []
144
+
145
+ AssetSizeAppBlockJavaScript:
146
+ enabled: true
147
+ ignore: []
148
+ threshold_in_bytes: 10_000
149
+
150
+ AssetSizeAppBlockCSS:
151
+ enabled: true
152
+ ignore: []
153
+ threshold_in_bytes: 100_000
@@ -0,0 +1,52 @@
1
+ # Prevent Large CSS bundles (`AssetSizeAppBlockCSS`)
2
+
3
+ This rule exists to prevent large CSS bundles from being included via Theme App Extensions (for speed).
4
+
5
+ ## Check Details
6
+
7
+ This rule disallows the use of too much CSS in themes, as configured by `threshold_in_bytes`.
8
+
9
+ :-1: Examples of **incorrect** code for this check:
10
+ ```liquid
11
+ <!-- Here, assets/app.css is **greater** than `threshold_in_bytes` compressed. -->
12
+ {% schema %}
13
+ {
14
+ ...
15
+ "stylesheet": "app.css"
16
+ }
17
+ {% endschema %}
18
+ ```
19
+
20
+ ## Check Options
21
+
22
+ The default configuration is the following:
23
+
24
+ ```yaml
25
+ AssetSizeAppBlockCSS:
26
+ enabled: false
27
+ threshold_in_bytes: 100_000
28
+ ```
29
+
30
+ ### `threshold_in_bytes`
31
+
32
+ The `threshold_in_bytes` option (default: `100_000`) determines the maximum allowed compressed size in bytes that a single CSS file can take.
33
+
34
+ This includes theme and remote stylesheets.
35
+
36
+ ## When Not To Use It
37
+
38
+ This rule should not be disabled locally since the check will be enforced when
39
+ promoting new versions of the extension.
40
+
41
+ ## Version
42
+
43
+ This check has been introduced in 1.1.0
44
+
45
+ ## Resources
46
+
47
+ - [The Performance Inequality Gap](https://infrequently.org/2021/03/the-performance-inequality-gap/)
48
+ - [Rule Source][codesource]
49
+ - [Documentation Source][docsource]
50
+
51
+ [codesource]: /lib/theme_check/checks/asset_size_app_block_css.rb
52
+ [docsource]: /docs/checks/asset_size_app_block_css.md
@@ -0,0 +1,57 @@
1
+ # Prevent Abuse on Server Rendered App Blocks (`AssetSizeAppBlockJavaScript`)
2
+
3
+ For server rendered app blocks, it is an anti-pattern to execute large JavaScript bundles on every page load
4
+
5
+ This doesn't mean they don't have a reason to exist. For instance, chat widgets are mini applications embedded inside web pages. Designing such an app with server rendered updates would be absurd. However, if only 10% of the users interact with the chat widget, the other 90% should not have to execute the entire bundle on every page load.
6
+
7
+ The natural solution to this problem is to implement the chat widget using the [Import on Interaction Pattern][ioip].
8
+
9
+ ## Check Details
10
+
11
+ This rule disallows the use of block JavaScript files and external scripts to have a compressed size greater than a configured `threshold_in_bytes`.
12
+
13
+ :-1: Examples of **incorrect** code for this check:
14
+ ```liquid
15
+ <!-- Here assets/chat-widget.js is more than 10KB gzipped. -->
16
+ {% schema %}
17
+ {
18
+ ...
19
+ "javascript": "chat-widget.js"
20
+ }
21
+ {% endschema %}
22
+ ```
23
+
24
+ ## Check Options
25
+
26
+ The default configuration is the following:
27
+
28
+ ```yaml
29
+ AssetSizeAppBlockJavaScript:
30
+ enabled: true
31
+ threshold_in_bytes: 10000
32
+ ```
33
+
34
+ ### `threshold_in_bytes`
35
+
36
+ The `threshold_in_bytes` option (default: `10000`) determines the maximum allowed compressed size in bytes that a single JavaScript file can take.
37
+
38
+ This includes theme and remote scripts.
39
+
40
+ ## When Not To Use It
41
+
42
+ This rule should not be disabled locally since the check will be enforced when
43
+ promoting new versions of the extension.
44
+
45
+ ## Version
46
+
47
+ This check has been introduced in 1.1.0
48
+
49
+ ## Resources
50
+
51
+ - [The Import On Interaction Pattern][ioip]
52
+ - [Rule Source][codesource]
53
+ - [Documentation Source][docsource]
54
+
55
+ [ioip]: https://addyosmani.com/blog/import-on-interaction/
56
+ [codesource]: /lib/theme_check/checks/asset_size_app_block_javascript.rb
57
+ [docsource]: /docs/checks/asset_size_app_block_javascript.md
data/lib/theme_check.rb CHANGED
@@ -4,6 +4,7 @@ require "liquid"
4
4
  require_relative "theme_check/version"
5
5
  require_relative "theme_check/bug"
6
6
  require_relative "theme_check/exceptions"
7
+ require_relative "theme_check/theme_file"
7
8
  require_relative "theme_check/analyzer"
8
9
  require_relative "theme_check/check"
9
10
  require_relative "theme_check/checks_tracking"
@@ -45,3 +46,17 @@ Dir[__dir__ + "/theme_check/checks/*.rb"].each { |file| require file }
45
46
  # UTF-8 is the default internal and external encoding, like in Rails & Shopify.
46
47
  Encoding.default_external = Encoding::UTF_8
47
48
  Encoding.default_internal = Encoding::UTF_8
49
+
50
+ module ThemeCheck
51
+ def self.with_liquid_c_disabled
52
+ if defined?(Liquid::C)
53
+ was_enabled = Liquid::C.enabled
54
+ Liquid::C.enabled = false if was_enabled
55
+ end
56
+ yield
57
+ ensure
58
+ if defined?(Liquid::C) && was_enabled
59
+ Liquid::C.enabled = true
60
+ end
61
+ end
62
+ end
@@ -34,9 +34,11 @@ module ThemeCheck
34
34
 
35
35
  liquid_visitor = Visitor.new(@liquid_checks, @disabled_checks)
36
36
  html_visitor = HtmlVisitor.new(@html_checks)
37
- @theme.liquid.each do |template|
38
- liquid_visitor.visit_template(template)
39
- html_visitor.visit_template(template)
37
+ ThemeCheck.with_liquid_c_disabled do
38
+ @theme.liquid.each do |template|
39
+ liquid_visitor.visit_template(template)
40
+ html_visitor.visit_template(template)
41
+ end
40
42
  end
41
43
 
42
44
  @theme.json.each { |json_file| @json_checks.call(:on_file, json_file) }
@@ -47,24 +49,26 @@ module ThemeCheck
47
49
  def analyze_files(files)
48
50
  reset
49
51
 
50
- # Call all checks that run on the whole theme
51
- liquid_visitor = Visitor.new(@liquid_checks.whole_theme, @disabled_checks)
52
- html_visitor = HtmlVisitor.new(@html_checks.whole_theme)
53
- @theme.liquid.each do |template|
54
- liquid_visitor.visit_template(template)
55
- html_visitor.visit_template(template)
56
- end
57
- @theme.json.each { |json_file| @json_checks.whole_theme.call(:on_file, json_file) }
58
-
59
- # Call checks that run on a single files, only on specified file
60
- liquid_visitor = Visitor.new(@liquid_checks.single_file, @disabled_checks)
61
- html_visitor = HtmlVisitor.new(@html_checks.single_file)
62
- files.each do |file|
63
- if file.liquid?
64
- liquid_visitor.visit_template(file)
65
- html_visitor.visit_template(file)
66
- elsif file.json?
67
- @json_checks.single_file.call(:on_file, file)
52
+ ThemeCheck.with_liquid_c_disabled do
53
+ # Call all checks that run on the whole theme
54
+ liquid_visitor = Visitor.new(@liquid_checks.whole_theme, @disabled_checks)
55
+ html_visitor = HtmlVisitor.new(@html_checks.whole_theme)
56
+ @theme.liquid.each do |template|
57
+ liquid_visitor.visit_template(template)
58
+ html_visitor.visit_template(template)
59
+ end
60
+ @theme.json.each { |json_file| @json_checks.whole_theme.call(:on_file, json_file) }
61
+
62
+ # Call checks that run on a single files, only on specified file
63
+ liquid_visitor = Visitor.new(@liquid_checks.single_file, @disabled_checks)
64
+ html_visitor = HtmlVisitor.new(@html_checks.single_file)
65
+ files.each do |file|
66
+ if file.liquid?
67
+ liquid_visitor.visit_template(file)
68
+ html_visitor.visit_template(file)
69
+ elsif file.json?
70
+ @json_checks.single_file.call(:on_file, file)
71
+ end
68
72
  end
69
73
  end
70
74
 
@@ -1,27 +1,15 @@
1
1
  # frozen_string_literal: true
2
- require "pathname"
3
2
  require "zlib"
4
3
 
5
4
  module ThemeCheck
6
- class AssetFile
5
+ class AssetFile < ThemeFile
7
6
  def initialize(relative_path, storage)
8
- @relative_path = relative_path
9
- @storage = storage
7
+ super
10
8
  @loaded = false
11
9
  @content = nil
12
10
  end
13
11
 
14
- def path
15
- @storage.path(@relative_path)
16
- end
17
-
18
- def relative_path
19
- @relative_pathname ||= Pathname.new(@relative_path)
20
- end
21
-
22
- def content
23
- @content ||= @storage.read(@relative_path)
24
- end
12
+ alias_method :content, :source
25
13
 
26
14
  def gzipped_size
27
15
  @gzipped_size ||= Zlib.gzip(content).bytesize
@@ -2,6 +2,8 @@
2
2
  require 'theme_check/version'
3
3
 
4
4
  module ThemeCheck
5
+ class ThemeCheckError < StandardError; end
6
+
5
7
  BUG_POSTAMBLE = <<~EOS
6
8
  Theme Check Version: #{VERSION}
7
9
  Ruby Version: #{RUBY_VERSION}
@@ -15,6 +17,6 @@ module ThemeCheck
15
17
  EOS
16
18
 
17
19
  def self.bug(message)
18
- abort(message + BUG_POSTAMBLE)
20
+ raise ThemeCheckError, message + BUG_POSTAMBLE
19
21
  end
20
22
  end
@@ -9,12 +9,19 @@ module ThemeCheck
9
9
  attr_accessor :options, :ignored_patterns
10
10
  attr_writer :offenses
11
11
 
12
+ # The order matters.
12
13
  SEVERITIES = [
13
14
  :error,
14
15
  :suggestion,
15
16
  :style,
16
17
  ]
17
18
 
19
+ # [severity: sym] => number
20
+ SEVERITY_VALUES = SEVERITIES
21
+ .map
22
+ .with_index { |sev, i| [sev, i] }
23
+ .to_h
24
+
18
25
  CATEGORIES = [
19
26
  :liquid,
20
27
  :translation,
@@ -38,6 +45,10 @@ module ThemeCheck
38
45
  @severity if defined?(@severity)
39
46
  end
40
47
 
48
+ def severity_value(severity)
49
+ SEVERITY_VALUES[severity]
50
+ end
51
+
41
52
  def categories(*categories)
42
53
  @categories ||= []
43
54
  if categories.any?
@@ -85,7 +96,18 @@ module ThemeCheck
85
96
  end
86
97
 
87
98
  def severity
88
- self.class.severity
99
+ @severity ||= self.class.severity
100
+ end
101
+
102
+ def severity=(severity)
103
+ unless SEVERITIES.include?(severity)
104
+ raise ArgumentError, "unknown severity. Use: #{SEVERITIES.join(', ')}"
105
+ end
106
+ @severity = severity
107
+ end
108
+
109
+ def severity_value
110
+ SEVERITY_VALUES[severity]
89
111
  end
90
112
 
91
113
  def categories
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+ module ThemeCheck
3
+ # Reports errors when too much CSS is being referenced from a Theme App
4
+ # Extension block
5
+ class AssetSizeAppBlockCSS < LiquidCheck
6
+ severity :error
7
+ category :performance
8
+ doc docs_url(__FILE__)
9
+
10
+ # Don't allow this check to be disabled with a comment,
11
+ # since we need to be able to enforce this server-side
12
+ can_disable false
13
+
14
+ attr_reader :threshold_in_bytes
15
+
16
+ def initialize(threshold_in_bytes: 100_000)
17
+ @threshold_in_bytes = threshold_in_bytes
18
+ end
19
+
20
+ def on_schema(node)
21
+ schema = JSON.parse(node.value.nodelist.join)
22
+
23
+ if (stylesheet = schema["stylesheet"])
24
+ size = asset_size(stylesheet)
25
+ if size && size > threshold_in_bytes
26
+ add_offense(
27
+ "CSS in Theme App Extension blocks exceeds compressed size threshold (#{threshold_in_bytes} Bytes)",
28
+ node: node
29
+ )
30
+ end
31
+ end
32
+ rescue JSON::ParserError
33
+ # Ignored, handled in ValidSchema.
34
+ end
35
+
36
+ private
37
+
38
+ def asset_size(name)
39
+ asset = @theme["assets/#{name}"]
40
+ return if asset.nil?
41
+ asset.gzipped_size
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+ module ThemeCheck
3
+ # Reports errors when too much JS is being referenced from a Theme App
4
+ # Extension block
5
+ class AssetSizeAppBlockJavaScript < LiquidCheck
6
+ severity :error
7
+ category :performance
8
+ doc docs_url(__FILE__)
9
+
10
+ # Don't allow this check to be disabled with a comment,
11
+ # since we need to be able to enforce this server-side
12
+ can_disable false
13
+
14
+ attr_reader :threshold_in_bytes
15
+
16
+ def initialize(threshold_in_bytes: 10_000)
17
+ @threshold_in_bytes = threshold_in_bytes
18
+ end
19
+
20
+ def on_schema(node)
21
+ schema = JSON.parse(node.value.nodelist.join)
22
+
23
+ if (javascript = schema["javascript"])
24
+ size = asset_size(javascript)
25
+ if size && size > threshold_in_bytes
26
+ add_offense(
27
+ "JavaScript in Theme App Extension blocks exceeds compressed size threshold (#{threshold_in_bytes} Bytes)",
28
+ node: node
29
+ )
30
+ end
31
+ end
32
+ rescue JSON::ParserError
33
+ # Ignored, handled in ValidSchema.
34
+ end
35
+
36
+ private
37
+
38
+ def asset_size(name)
39
+ asset = @theme["assets/#{name}"]
40
+ return if asset.nil?
41
+ asset.gzipped_size
42
+ end
43
+ end
44
+ end
@@ -10,10 +10,11 @@ module ThemeCheck
10
10
  def initialize
11
11
  @path = "."
12
12
  @command = :check
13
- @only_categories = []
13
+ @include_categories = []
14
14
  @exclude_categories = []
15
15
  @auto_correct = false
16
16
  @config_path = nil
17
+ @fail_level = :error
17
18
  end
18
19
 
19
20
  def option_parser(parser = OptionParser.new, help: true)
@@ -25,20 +26,27 @@ module ThemeCheck
25
26
  @option_parser.separator("Basic Options:")
26
27
  @option_parser.on(
27
28
  "-C", "--config PATH",
28
- "Use the config provided, overriding .theme-check.yml if present"
29
+ "Use the config provided, overriding .theme-check.yml if present",
30
+ "Use :theme_app_extension to use default checks for theme app extensions"
29
31
  ) { |path| @config_path = path }
30
32
  @option_parser.on(
31
- "-c", "--category CATEGORY",
32
- "Only run this category of checks"
33
- ) { |category| @only_categories << category.to_sym }
33
+ "-c", "--category CATEGORY", Check::CATEGORIES, "Only run this category of checks",
34
+ "Runs checks matching all categories when specified more than once"
35
+ ) { |category| @include_categories << category.to_sym }
34
36
  @option_parser.on(
35
- "-x", "--exclude-category CATEGORY",
36
- "Exclude this category of checks"
37
+ "-x", "--exclude-category CATEGORY", Check::CATEGORIES, "Exclude this category of checks",
38
+ "Excludes checks matching any category when specified more than once"
37
39
  ) { |category| @exclude_categories << category.to_sym }
38
40
  @option_parser.on(
39
41
  "-a", "--auto-correct",
40
42
  "Automatically fix offenses"
41
43
  ) { @auto_correct = true }
44
+ @option_parser.on(
45
+ "--fail-level SEVERITY", Check::SEVERITIES,
46
+ "Minimum severity (error|suggestion|style) for exit with error code"
47
+ ) do |severity|
48
+ @fail_level = severity.to_sym
49
+ end
42
50
 
43
51
  @option_parser.separator("")
44
52
  @option_parser.separator("Miscellaneous:")
@@ -77,6 +85,8 @@ module ThemeCheck
77
85
 
78
86
  def parse(argv)
79
87
  @path = option_parser.parse(argv).first || "."
88
+ rescue OptionParser::InvalidArgument => e
89
+ abort(e.message)
80
90
  end
81
91
 
82
92
  def run!
@@ -84,13 +94,13 @@ module ThemeCheck
84
94
  @config = if @config_path
85
95
  ThemeCheck::Config.new(
86
96
  root: @path,
87
- configuration: ThemeCheck::Config.load_file(@config_path)
97
+ configuration: ThemeCheck::Config.load_config(@config_path)
88
98
  )
89
99
  else
90
100
  ThemeCheck::Config.from_path(@path)
91
101
  end
92
- @config.only_categories = @only_categories
93
- @config.exclude_categories = @exclude_categories
102
+ @config.include_categories = @include_categories unless @include_categories.empty?
103
+ @config.exclude_categories = @exclude_categories unless @exclude_categories.empty?
94
104
  @config.auto_correct = @auto_correct
95
105
  end
96
106
 
@@ -99,12 +109,16 @@ module ThemeCheck
99
109
 
100
110
  def run
101
111
  run!
112
+ exit(0)
102
113
  rescue Abort => e
103
114
  if e.message.empty?
104
115
  exit(1)
105
116
  else
106
117
  abort(e.message)
107
118
  end
119
+ rescue ThemeCheckError => e
120
+ STDERR.puts(e.message)
121
+ exit(2)
108
122
  end
109
123
 
110
124
  def self.parse_and_run!(argv)
@@ -130,11 +144,16 @@ module ThemeCheck
130
144
  def init
131
145
  dotfile_path = ThemeCheck::Config.find(@path)
132
146
  if dotfile_path.nil?
133
- File.write(File.join(@path, ThemeCheck::Config::DOTFILE), File.read(ThemeCheck::Config::DEFAULT_CONFIG))
147
+ config_name = if @config_path && @config_path[0] == ":"
148
+ "#{@config_path[1..]}.yml"
149
+ else
150
+ "default.yml"
151
+ end
152
+ File.write(File.join(@path, ThemeCheck::Config::DOTFILE), File.read(ThemeCheck::Config.bundled_config_path(config_name)))
134
153
 
135
154
  puts "Writing new #{ThemeCheck::Config::DOTFILE} to #{@path}"
136
155
  else
137
- raise Abort, "#{ThemeCheck::Config::DOTFILE} already exists at #{@path}."
156
+ raise Abort, "#{ThemeCheck::Config::DOTFILE} already exists at #{@path}"
138
157
  end
139
158
  end
140
159
 
@@ -157,7 +176,9 @@ module ThemeCheck
157
176
  analyzer.analyze_theme
158
177
  analyzer.correct_offenses
159
178
  ThemeCheck::Printer.new.print(theme, analyzer.offenses, @config.auto_correct)
160
- raise Abort, "" if analyzer.uncorrectable_offenses.any?
179
+ raise Abort, "" if analyzer.uncorrectable_offenses.any? do |offense|
180
+ offense.check.severity_value <= Check.severity_value(@fail_level)
181
+ end
161
182
  end
162
183
  end
163
184
  end
@@ -3,11 +3,11 @@
3
3
  module ThemeCheck
4
4
  class Config
5
5
  DOTFILE = '.theme-check.yml'
6
- DEFAULT_CONFIG = "#{__dir__}/../../config/default.yml"
6
+ BUNDLED_CONFIGS_DIR = "#{__dir__}/../../config"
7
7
  BOOLEAN = [true, false]
8
8
 
9
9
  attr_reader :root
10
- attr_accessor :only_categories, :exclude_categories, :auto_correct
10
+ attr_accessor :auto_correct
11
11
 
12
12
  class << self
13
13
  attr_reader :last_loaded_config
@@ -42,18 +42,46 @@ module ThemeCheck
42
42
  YAML.load_file(absolute_path)
43
43
  end
44
44
 
45
+ def bundled_config_path(name)
46
+ "#{BUNDLED_CONFIGS_DIR}/#{name}"
47
+ end
48
+
49
+ def load_bundled_config(name)
50
+ load_file(bundled_config_path(name))
51
+ end
52
+
53
+ def load_config(path)
54
+ if path[0] == ":"
55
+ load_bundled_config("#{path[1..]}.yml")
56
+ elsif path.is_a?(Symbol)
57
+ load_bundled_config("#{path}.yml")
58
+ else
59
+ load_file(path)
60
+ end
61
+ end
62
+
45
63
  def default
46
- @default ||= load_file(DEFAULT_CONFIG)
64
+ @default ||= load_config(":default")
47
65
  end
48
66
  end
49
67
 
50
68
  def initialize(root: nil, configuration: nil, should_resolve_requires: true)
51
69
  @configuration = if configuration
70
+ # TODO: Do we need to handle extends here? What base configuration
71
+ # should we validate against once Theme App Extensions has its own
72
+ # checks? :all?
52
73
  validate_configuration(configuration)
53
74
  else
54
75
  {}
55
76
  end
56
- merge_with_default_configuration!(@configuration)
77
+
78
+ # Follow extends
79
+ extends = @configuration["extends"] || ":default"
80
+ while extends
81
+ extended_configuration = self.class.load_config(extends)
82
+ extends = extended_configuration["extends"]
83
+ @configuration = merge_configurations!(@configuration, extended_configuration)
84
+ end
57
85
 
58
86
  @root = if root && @configuration.key?("root")
59
87
  Pathname.new(root).join(@configuration["root"])
@@ -61,8 +89,6 @@ module ThemeCheck
61
89
  Pathname.new(root)
62
90
  end
63
91
 
64
- @only_categories = []
65
- @exclude_categories = []
66
92
  @auto_correct = false
67
93
 
68
94
  resolve_requires if @root && should_resolve_requires
@@ -87,16 +113,18 @@ module ThemeCheck
87
113
  check_class = ThemeCheck.const_get(check_name)
88
114
 
89
115
  next if check_class.categories.any? { |category| exclude_categories.include?(category) }
90
- next if only_categories.any? && check_class.categories.none? { |category| only_categories.include?(category) }
116
+ next if include_categories.any? && !include_categories.all? { |category| check_class.categories.include?(category) }
91
117
 
92
118
  options_for_check = options.transform_keys(&:to_sym)
93
119
  options_for_check.delete(:enabled)
120
+ severity = options_for_check.delete(:severity)
94
121
  ignored_patterns = options_for_check.delete(:ignore) || []
95
122
  check = if options_for_check.empty?
96
123
  check_class.new
97
124
  else
98
125
  check_class.new(**options_for_check)
99
126
  end
127
+ check.severity = severity.to_sym if severity
100
128
  check.ignored_patterns = ignored_patterns
101
129
  check.options = options_for_check
102
130
  check
@@ -107,6 +135,22 @@ module ThemeCheck
107
135
  self["ignore"] || []
108
136
  end
109
137
 
138
+ def include_categories
139
+ self["include_categories"] || []
140
+ end
141
+
142
+ def include_categories=(categories)
143
+ @configuration["include_categories"] = categories
144
+ end
145
+
146
+ def exclude_categories
147
+ self["exclude_categories"] || []
148
+ end
149
+
150
+ def exclude_categories=(categories)
151
+ @configuration["exclude_categories"] = categories
152
+ end
153
+
110
154
  private
111
155
 
112
156
  def check_name?(name)
@@ -133,6 +177,8 @@ module ThemeCheck
133
177
  else
134
178
  warn("bad configuration type for #{name}: expected a Hash, got #{value.inspect}")
135
179
  end
180
+ elsif key == "severity"
181
+ valid_configuration[key] = value
136
182
  elsif default.nil?
137
183
  warn("unknown configuration: #{name}")
138
184
  elsif BOOLEAN.include?(default) && !BOOLEAN.include?(value)
@@ -147,13 +193,13 @@ module ThemeCheck
147
193
  valid_configuration
148
194
  end
149
195
 
150
- def merge_with_default_configuration!(configuration, default_configuration = self.class.default)
151
- default_configuration.each do |key, default|
196
+ def merge_configurations!(configuration, extended_configuration)
197
+ extended_configuration.each do |key, default|
152
198
  value = configuration[key]
153
199
 
154
200
  case value
155
201
  when Hash
156
- merge_with_default_configuration!(value, default)
202
+ merge_configurations!(value, default)
157
203
  when nil
158
204
  configuration[key] = default
159
205
  end
@@ -1,32 +1,34 @@
1
1
  # frozen_string_literal: true
2
2
  require "net/http"
3
3
 
4
- TIMEOUT_EXCEPTIONS = [
5
- Net::ReadTimeout,
6
- Net::OpenTimeout,
7
- Net::WriteTimeout,
8
- Errno::ETIMEDOUT,
9
- Timeout::Error,
10
- ]
4
+ module ThemeCheck
5
+ TIMEOUT_EXCEPTIONS = [
6
+ Net::ReadTimeout,
7
+ Net::OpenTimeout,
8
+ Net::WriteTimeout,
9
+ Errno::ETIMEDOUT,
10
+ Timeout::Error,
11
+ ]
11
12
 
12
- CONNECTION_EXCEPTIONS = [
13
- IOError,
14
- EOFError,
15
- SocketError,
16
- Errno::EINVAL,
17
- Errno::ECONNRESET,
18
- Errno::ECONNABORTED,
19
- Errno::EPIPE,
20
- Errno::ECONNREFUSED,
21
- Errno::EAGAIN,
22
- Errno::EHOSTUNREACH,
23
- Errno::ENETUNREACH,
24
- ]
13
+ CONNECTION_EXCEPTIONS = [
14
+ IOError,
15
+ EOFError,
16
+ SocketError,
17
+ Errno::EINVAL,
18
+ Errno::ECONNRESET,
19
+ Errno::ECONNABORTED,
20
+ Errno::EPIPE,
21
+ Errno::ECONNREFUSED,
22
+ Errno::EAGAIN,
23
+ Errno::EHOSTUNREACH,
24
+ Errno::ENETUNREACH,
25
+ ]
25
26
 
26
- NET_HTTP_EXCEPTIONS = [
27
- Net::HTTPBadResponse,
28
- Net::HTTPHeaderSyntaxError,
29
- Net::ProtocolError,
30
- *TIMEOUT_EXCEPTIONS,
31
- *CONNECTION_EXCEPTIONS,
32
- ]
27
+ NET_HTTP_EXCEPTIONS = [
28
+ Net::HTTPBadResponse,
29
+ Net::HTTPHeaderSyntaxError,
30
+ Net::ProtocolError,
31
+ *TIMEOUT_EXCEPTIONS,
32
+ *CONNECTION_EXCEPTIONS,
33
+ ]
34
+ end
@@ -1,29 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
  require "json"
3
- require "pathname"
4
3
 
5
4
  module ThemeCheck
6
- class JsonFile
5
+ class JsonFile < ThemeFile
7
6
  def initialize(relative_path, storage)
8
- @relative_path = relative_path
9
- @storage = storage
7
+ super
10
8
  @loaded = false
11
9
  @content = nil
12
10
  @parser_error = nil
13
11
  end
14
12
 
15
- def path
16
- @storage.path(@relative_path)
17
- end
18
-
19
- def relative_path
20
- @relative_pathname ||= Pathname.new(@relative_path)
21
- end
22
-
23
- def source
24
- @source ||= @storage.read(@relative_path)
25
- end
26
-
27
13
  def content
28
14
  load!
29
15
  @content
@@ -34,23 +20,10 @@ module ThemeCheck
34
20
  @parser_error
35
21
  end
36
22
 
37
- def name
38
- relative_path.sub_ext('').to_s
39
- end
40
-
41
23
  def json?
42
24
  true
43
25
  end
44
26
 
45
- def liquid?
46
- false
47
- end
48
-
49
- def ==(other)
50
- other.is_a?(JsonFile) && relative_path == other.relative_path
51
- end
52
- alias_method :eql?, :==
53
-
54
27
  private
55
28
 
56
29
  def load!
@@ -10,5 +10,13 @@ module ThemeCheck
10
10
  ^\s*render\s+'(?<partial>[^']*)'|
11
11
  ^\s*render\s+"(?<partial>[^"]*)"
12
12
  }mix
13
+ ASSET_INCLUDE = %r{
14
+ \{\%-?\s*'(?<partial>[^']*)'\s*\|\s*asset_url|
15
+ \{\%-?\s*"(?<partial>[^"]*)"\s*\|\s*asset_url|
16
+
17
+ # in liquid tags the whole line is white space until the asset partial
18
+ ^\s*'(?<partial>[^']*)'\s*\|\s*asset_url|
19
+ ^\s*"(?<partial>[^"]*)"\s*\|\s*asset_url
20
+ }mix
13
21
  end
14
22
  end
@@ -13,7 +13,7 @@ module ThemeCheck
13
13
  def document_links(relative_path)
14
14
  buffer = @storage.read(relative_path)
15
15
  return [] unless buffer
16
- matches(buffer, PARTIAL_RENDER).map do |match|
16
+ snippet_matches = matches(buffer, PARTIAL_RENDER).map do |match|
17
17
  start_line, start_character = from_index_to_row_column(
18
18
  buffer,
19
19
  match.begin(:partial),
@@ -25,7 +25,7 @@ module ThemeCheck
25
25
  )
26
26
 
27
27
  {
28
- target: link(match[:partial]),
28
+ target: snippet_link(match[:partial]),
29
29
  range: {
30
30
  start: {
31
31
  line: start_line,
@@ -38,10 +38,46 @@ module ThemeCheck
38
38
  },
39
39
  }
40
40
  end
41
+ asset_matches = matches(buffer, ASSET_INCLUDE).map do |match|
42
+ start_line, start_character = from_index_to_row_column(
43
+ buffer,
44
+ match.begin(:partial),
45
+ )
46
+
47
+ end_line, end_character = from_index_to_row_column(
48
+ buffer,
49
+ match.end(:partial)
50
+ )
51
+
52
+ {
53
+ target: asset_link(match[:partial]),
54
+ range: {
55
+ start: {
56
+ line: start_line,
57
+ character: start_character,
58
+ },
59
+ end: {
60
+ line: end_line,
61
+ character: end_character,
62
+ },
63
+ },
64
+ }
65
+ end
66
+ snippet_matches + asset_matches
67
+ end
68
+
69
+ def snippet_link(partial)
70
+ file_link('snippets', partial, '.liquid')
41
71
  end
42
72
 
43
- def link(partial)
44
- "file://#{@storage.path('snippets/' + partial + '.liquid')}"
73
+ def asset_link(partial)
74
+ file_link('assets', partial, '')
75
+ end
76
+
77
+ private
78
+
79
+ def file_link(directory, partial, extension)
80
+ "file://#{@storage.path(directory + '/' + partial + extension)}"
45
81
  end
46
82
  end
47
83
  end
@@ -169,14 +169,31 @@ module ThemeCheck
169
169
 
170
170
  class Stylesheet < Liquid::Raw; end
171
171
 
172
- Liquid::Template.register_tag('form', Form)
173
- Liquid::Template.register_tag('layout', Layout)
174
- Liquid::Template.register_tag('render', Render)
175
- Liquid::Template.register_tag('paginate', Paginate)
176
- Liquid::Template.register_tag('section', Section)
177
- Liquid::Template.register_tag('style', Style)
178
- Liquid::Template.register_tag('schema', Schema)
179
- Liquid::Template.register_tag('javascript', Javascript)
180
- Liquid::Template.register_tag('stylesheet', Stylesheet)
172
+ class << self
173
+ attr_writer :register_tags
174
+
175
+ def register_tags?
176
+ @register_tags
177
+ end
178
+
179
+ def register_tag(name, klass)
180
+ Liquid::Template.register_tag(name, klass)
181
+ end
182
+
183
+ def register_tags!
184
+ return if !register_tags? || (defined?(@registered_tags) && @registered_tags)
185
+ @registered_tags = true
186
+ register_tag('form', Form)
187
+ register_tag('layout', Layout)
188
+ register_tag('render', Render)
189
+ register_tag('paginate', Paginate)
190
+ register_tag('section', Section)
191
+ register_tag('style', Style)
192
+ register_tag('schema', Schema)
193
+ register_tag('javascript', Javascript)
194
+ register_tag('stylesheet', Stylesheet)
195
+ end
196
+ end
197
+ self.register_tags = true
181
198
  end
182
199
  end
@@ -1,25 +1,7 @@
1
1
  # frozen_string_literal: true
2
- require "pathname"
3
2
 
4
3
  module ThemeCheck
5
- class Template
6
- def initialize(relative_path, storage)
7
- @storage = storage
8
- @relative_path = relative_path
9
- end
10
-
11
- def path
12
- @storage.path(@relative_path)
13
- end
14
-
15
- def relative_path
16
- @relative_pathname ||= Pathname.new(@relative_path)
17
- end
18
-
19
- def source
20
- @source ||= @storage.read(@relative_path)
21
- end
22
-
4
+ class Template < ThemeFile
23
5
  def write
24
6
  content = updated_content
25
7
  if source != content
@@ -28,14 +10,6 @@ module ThemeCheck
28
10
  end
29
11
  end
30
12
 
31
- def name
32
- relative_path.sub_ext('').to_s
33
- end
34
-
35
- def json?
36
- false
37
- end
38
-
39
13
  def liquid?
40
14
  true
41
15
  end
@@ -88,16 +62,13 @@ module ThemeCheck
88
62
  parse.root
89
63
  end
90
64
 
91
- def ==(other)
92
- other.is_a?(Template) && relative_path == other.relative_path
93
- end
94
- alias_method :eql?, :==
95
-
96
65
  def self.parse(source)
66
+ Tags.register_tags!
97
67
  Liquid::Template.parse(
98
68
  source,
99
69
  line_numbers: true,
100
70
  error_mode: :warn,
71
+ disable_liquid_c_nodes: true,
101
72
  )
102
73
  end
103
74
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+ require "pathname"
3
+
4
+ module ThemeCheck
5
+ class ThemeFile
6
+ def initialize(relative_path, storage)
7
+ @relative_path = relative_path
8
+ @storage = storage
9
+ end
10
+
11
+ def path
12
+ @storage.path(@relative_path)
13
+ end
14
+
15
+ def relative_path
16
+ @relative_pathname ||= Pathname.new(@relative_path)
17
+ end
18
+
19
+ def name
20
+ relative_path.sub_ext('').to_s
21
+ end
22
+
23
+ def source
24
+ @source ||= @storage.read(@relative_path)
25
+ end
26
+
27
+ def json?
28
+ false
29
+ end
30
+
31
+ def liquid?
32
+ false
33
+ end
34
+
35
+ def ==(other)
36
+ other.is_a?(self.class) && relative_path == other.relative_path
37
+ end
38
+ alias_method :eql?, :==
39
+ end
40
+ end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module ThemeCheck
3
- VERSION = "1.0.0"
3
+ VERSION = "1.1.0"
4
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: theme-check
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marc-André Cournoyer
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-06-28 00:00:00.000000000 Z
11
+ date: 2021-07-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: liquid
@@ -62,6 +62,8 @@ files:
62
62
  - Rakefile
63
63
  - bin/liquid-server
64
64
  - config/default.yml
65
+ - config/nothing.yml
66
+ - config/theme_app_extension.yml
65
67
  - data/shopify_liquid/deprecated_filters.yml
66
68
  - data/shopify_liquid/filters.yml
67
69
  - data/shopify_liquid/objects.yml
@@ -74,6 +76,8 @@ files:
74
76
  - docs/api/json_check.md
75
77
  - docs/api/liquid_check.md
76
78
  - docs/checks/TEMPLATE.md.erb
79
+ - docs/checks/asset_size_app_block_css.md
80
+ - docs/checks/asset_size_app_block_javascript.md
77
81
  - docs/checks/asset_size_css.md
78
82
  - docs/checks/asset_size_css_stylesheet_tag.md
79
83
  - docs/checks/asset_size_javascript.md
@@ -120,6 +124,8 @@ files:
120
124
  - lib/theme_check/check.rb
121
125
  - lib/theme_check/checks.rb
122
126
  - lib/theme_check/checks/TEMPLATE.rb.erb
127
+ - lib/theme_check/checks/asset_size_app_block_css.rb
128
+ - lib/theme_check/checks/asset_size_app_block_javascript.rb
123
129
  - lib/theme_check/checks/asset_size_css.rb
124
130
  - lib/theme_check/checks/asset_size_css_stylesheet_tag.rb
125
131
  - lib/theme_check/checks/asset_size_javascript.rb
@@ -209,6 +215,7 @@ files:
209
215
  - lib/theme_check/tags.rb
210
216
  - lib/theme_check/template.rb
211
217
  - lib/theme_check/theme.rb
218
+ - lib/theme_check/theme_file.rb
212
219
  - lib/theme_check/version.rb
213
220
  - lib/theme_check/visitor.rb
214
221
  - packaging/homebrew/theme_check.base.rb