Skip to content
This repository has been archived by the owner on Jul 27, 2024. It is now read-only.

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
macournoyer committed Nov 24, 2020
0 parents commit 6f48828
Show file tree
Hide file tree
Showing 22 changed files with 756 additions and 0 deletions.
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/.bundle/
/.yardoc
/_yardoc/
/coverage/
/doc/
/pkg/
/spec/reports/
/tmp/

*.gem
Gemfile.lock

.rubocop-https---shopify-github-io-ruby-style-guide-rubocop-yml
14 changes: 14 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
inherit_from:
- 'https://shopify.github.io/ruby-style-guide/rubocop.yml'

Metrics/MethodLength:
Enabled: false

Layout/LineLength:
Enabled: false

Lint/MissingSuper:
Enabled: false

Style/FrozenStringLiteralComment:
Enabled: false
14 changes: 14 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
source "https://rubygems.org"

git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }

# Specify your gem's dependencies in theme-check.gemspec
gemspec

gem 'bundler'
gem 'rake'
gem 'minitest'
gem 'rubocop', require: false

gem 'liquid', github: 'Shopify/liquid', branch: 'master'
gem 'liquid-c', github: 'Shopify/liquid-c', branch: 'master'
12 changes: 12 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
require "rake/testtask"
require 'rubocop/rake_task'

Rake::TestTask.new(:test) do |t|
t.libs << "test"
t.libs << "lib"
t.test_files = FileList["test/**/*_test.rb"]
end

RuboCop::RakeTask.new

task default: [:rubocop, :test]
6 changes: 6 additions & 0 deletions exe/theme-check
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env ruby

require "theme_check"

path = ARGV[0] || abort("usage: theme-check theme/root/path")
puts ThemeCheck.analyze(path)
20 changes: 20 additions & 0 deletions lib/theme_check.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
require "liquid/c"

require_relative "theme_check/analyzer"
require_relative "theme_check/check"
require_relative "theme_check/checks"
require_relative "theme_check/node"
require_relative "theme_check/offense"
require_relative "theme_check/tags"
require_relative "theme_check/theme"
require_relative "theme_check/visitor"

Dir[__dir__ + "/theme_check/checks/*.rb"].each { |file| require file }

module ThemeCheck
def self.analyze(theme_root)
analyzer = Analyzer.new(Theme.new(theme_root))
analyzer.analyze_theme
analyzer.offenses
end
end
34 changes: 34 additions & 0 deletions lib/theme_check/analyzer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
module ThemeCheck
class Analyzer
attr_reader :offenses

def initialize(theme, check_classes = Check.all)
@theme = theme
@offenses = []
@checks = Checks.new
check_classes.each do |check_class|
check = check_class.new
check.theme = @theme
check.offenses = @offenses
@checks << check
end
@visitor = Visitor.new(@checks)
end

def analyze_theme
@theme.all_files_paths.each { |template| analyze_template(template) }
@checks.call(:on_end)
end

def analyze_template(template_path)
template_path = Pathname(template_path)
template = Liquid::Template.parse(
template_path.read,
line_numbers: true,
disable_liquid_c_nodes: true
)
relative_template_path = template_path.relative_path_from(@theme.root)
@visitor.visit_template(template, path: relative_template_path)
end
end
end
61 changes: 61 additions & 0 deletions lib/theme_check/check.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
module ThemeCheck
class Check
attr_accessor :theme
attr_accessor :offenses

SEVERITIES = [
:error,
:suggestion,
:style,
]

class << self
def all
@all ||= []
end

def inherited(klass)
all << klass
end

def severity(severity = nil)
if severity
unless SEVERITIES.include?(severity)
raise ArgumentError, "unknown severity. Use: #{SEVERITIES.join(', ')}"
end
@severity = severity
end
@severity
end

def doc(doc = nil)
@doc = doc if doc
@doc
end
end

def severity
self.class.severity
end

def doc
self.class.doc
end

def ignore!
@ignored = true
end

def unignore!
@ignored = false
end

def ignored?
defined?(@ignored) && @ignored
end

def add_offense(message, node: nil, template: node&.template)
offenses << Offense.new(self, template, node, message)
end
end
end
11 changes: 11 additions & 0 deletions lib/theme_check/checks.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module ThemeCheck
class Checks < Array
def call(method, *args)
each do |check|
if check.respond_to?(method) && !check.ignored?
check.send(method, *args)
end
end
end
end
end
44 changes: 44 additions & 0 deletions lib/theme_check/checks/liquid_tag.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
module ThemeCheck
# Recommends using {% liquid ... %} if 3 or more consecutive {% ... %} are found.
class LiquidTag < Check
severity :suggestion
doc "https://shopify.dev/docs/themes/liquid/reference/tags/theme-tags#liquid"

MIN_CONSECUTIVE_STATEMENTS = 4

def initialize
@first_statement = nil
@consecutive_statements = 0
end

def on_tag(node)
unless node.comment?
increment_consecutive_statements(node)
end
end

def on_string(node)
# Only reset the counter on outputted strings, and ignore empty line-breaks
if node.parent.block? && !node.value.strip.empty?
reset_consecutive_statements
end
end

def after_document(_node)
reset_consecutive_statements
end

def increment_consecutive_statements(node)
@first_statement ||= node
@consecutive_statements += 1
end

def reset_consecutive_statements
if @consecutive_statements >= MIN_CONSECUTIVE_STATEMENTS
add_offense("Use {% liquid ... %} to write multiple tags", node: @first_statement)
end
@first_statement = nil
@consecutive_statements = 0
end
end
end
28 changes: 28 additions & 0 deletions lib/theme_check/checks/unused_snippets.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
require "set"

module ThemeCheck
class UnusedSnippets < Check
severity :suggestion

def initialize
@used_templates = Set.new
end

def on_include(node)
if node.value.template_name_expr.is_a?(String)
@used_templates << "snippets/#{node.value.template_name_expr}.liquid"
else
# Can't reliably track unused snippets if an expression is used, ignore this check
@used_templates.clear
ignore!
end
end
alias_method :on_render, :on_include

def on_end
(theme.snippets - @used_templates.to_a).each do |template|
add_offense("This template is not used", template: template)
end
end
end
end
65 changes: 65 additions & 0 deletions lib/theme_check/node.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
require 'active_support/core_ext/string/inflections'

module ThemeCheck
class Node
attr_reader :value, :parent, :template

def initialize(value, parent, template)
raise ArgumentError, "Expected a Liquid AST Node" if value.is_a?(Node)
@value = value
@parent = parent
@template = template
end

def children
@children ||= begin
nodes =
if defined?(@value.class::ParseTreeVisitor)
@value.class::ParseTreeVisitor.new(@value, {}).children
elsif @value.respond_to?(:nodelist)
Array(@value.nodelist)
else
[]
end
nodes.map { |node| Node.new(node, self, @template) }
end
end

def literal?
@value.is_a?(String) || @value.is_a?(Integer)
end

def tag?
@value.is_a?(Liquid::Tag)
end

def comment?
@value.is_a?(Liquid::Comment)
end

def document?
@value.is_a?(Liquid::Document)
end
alias_method :root?, :document?

def block_tag?
@value.is_a?(Liquid::Block)
end

def block?
block_tag? || block_body? || document?
end

def block_body?
@value.is_a?(Liquid::BlockBody)
end

def line_number
@value.line_number if @value.respond_to?(:line_number)
end

def type_name
@type_name ||= @value.class.name.demodulize.underscore.to_sym
end
end
end
22 changes: 22 additions & 0 deletions lib/theme_check/offense.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
module ThemeCheck
class Offense < Struct.new(:check, :template, :node, :message)
def line_number
node&.line_number
end

def severity
check.severity
end

def doc
check.doc
end

def to_s
out = +''
out << "#{message} at #{template}"
out << ":#{line_number}" if line_number
out
end
end
end
Loading

0 comments on commit 6f48828

Please sign in to comment.