Compare commits

..

22 Commits

Author SHA1 Message Date
Mike Angell
8318be2edc Update readme 2019-09-11 05:20:05 +10:00
Mike Angell
b6547f322e Simplify usage 2019-09-11 04:56:25 +10:00
Mike Angell
b316ff8413 Add usage tracking 2019-09-11 04:20:34 +10:00
Justin Li
806b2622da Switch back to Liquid-C master, since https://github.com/Shopify/liquid-c/pull/50 is merged 2019-09-04 15:12:51 -04:00
Mike Angell
c34f7c9b2c Merge pull request #1145 from Shopify/master-fixes
Render tag styling fixes
2019-09-04 14:25:38 +10:00
Mike Angell
604d899496 Render tag styling fixes 2019-08-31 22:48:25 +10:00
Mike Angell
ddb45cd658 Merge pull request #1139 from Shopify/shopify_ruby_style
Follow Shopify ruby style guide
2019-08-31 21:43:45 +10:00
Justin Li
9876096cf4 Merge pull request #1141 from ashmaroli/reduce-context-constructor-allocations
Reduce allocations from `Liquid::Context.new`
2019-08-30 12:53:50 -04:00
Ashwin Maroli
8750b4b006 Reduce allocations from Liquid::Context.new 2019-08-30 09:01:47 +05:30
Samuel Doiron
34083c96d5 Merge pull request #1122 from Shopify/render-tag
Add new `{% render %}` tag
2019-08-29 16:49:56 -04:00
Samuel
9672ed5285 Add a new {% render %} tag
Example:

```
// the_count.liquid
{{ number }}! Ah ah ah.

// my_template.liquid
{% for number in range (1..3) %}
  {% render "the_count", number: number %}
{% endfor %}

Output:
1! Ah ah ah.
2! Ah ah ah.
3! Ah ah ah.
```

The `render` tag is a more strict version of the `include` tag. It is
designed to isolate itself from the parent rendering context both by
creating a new scope (which does not inherit the parent scope) and by
only inheriting "static" registers.

Static registers are those that do not hold mutable state which could
affect rendering. This again helps `render`ed templates remain entirely
separate from their calling context.

Unlike `include`, `render` does not permit specifying the target
template using a variable, only a string literal. For example, this
means that `{% render my_dynamic_template %}` is invalid syntax. This
will make it possible to statically analyze the dependencies between
templates without making Turing angry.

Note that the `static_environment` of a rendered template is inherited, unlike
the scope and regular environment. This environment is immutable from within the
template.

An alternate syntax, which mimics the `{% include ... for %}` tag is
currently in design discussion.
2019-08-29 16:32:05 -04:00
Justin Li
f3112fc038 Merge pull request #1136 from ashmaroli/travis-selected-branches
Build only pushes to certain branches on Travis CI
2019-08-29 13:47:59 -04:00
Samuel
d338ccb9a6 Add isolated subcontexts
An isolated subcontext inherits the environment, filters,
and static registers of its supercontext, but with a fresh
(isolated) scope.

This will pave the way for adding the `render` tag, which renders
templates in such a subcontext.
2019-08-29 10:27:15 -04:00
Mike Angell
d67de1c9b2 Follow Shopify ruby style
This is the first step in bringing Liquid style inline with Shopify ruby style
2019-08-29 13:39:57 +10:00
Ashwin Maroli
b3097f143c Build only pushes to certain branches on Travis CI 2019-08-28 21:28:49 +05:30
Mike Angell
7b309dc75d Merge pull request #1135 from Shopify/fix-failing-rubocop
Resolve failing rubocop issues
2019-08-29 01:11:25 +10:00
Mike Angell
8f68cffdf1 Resolve failing rubocop issues 2019-08-29 00:45:38 +10:00
Mike Angell
dd27d0fd1d Merge pull request #1133 from Shopify/liquid-tag-fixes
Bugfix for new Liquid tag
2019-08-29 00:36:13 +10:00
Mike Angell
7a26e6b3d8 Merge pull request #1131 from Shopify/bump-ruby-2-4
Rubocop upgrade, Ruby 2.4 minimum and TruffleRuby
2019-08-29 00:33:42 +10:00
Mike Angell
cf4e77ab0c Merge branch 'master' into bump-ruby-2-4 2019-08-29 00:24:45 +10:00
Mike Angell
7bae55dd39 Bugfix for new Liquid tag 2019-08-28 23:39:19 +10:00
Mike Angell
b16b109a80 Bump Minimum version to 2.4 and bump Rubocop 2019-08-28 00:31:44 +10:00
25 changed files with 1916 additions and 378 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,131 +1,15 @@
inherit_from: inherit_from:
- https://shopify.github.io/ruby-style-guide/rubocop.yml
- .rubocop_todo.yml - .rubocop_todo.yml
- ./.rubocop_todo.yml
require: rubocop-performance
Performance:
Enabled: true
AllCops: AllCops:
Exclude: Exclude:
- 'performance/shopify/*' - 'vendor/bundle/**/*'
- 'pkg/**'
Metrics/BlockNesting:
Max: 3
Metrics/ModuleLength:
Enabled: false
Metrics/ClassLength:
Enabled: false
Lint/AssignmentInCondition:
Enabled: false
Lint/AmbiguousOperator:
Enabled: false
Lint/AmbiguousRegexpLiteral:
Enabled: false
Lint/ParenthesesAsGroupedExpression:
Enabled: false
Lint/UnusedBlockArgument:
Enabled: false
Layout/EndAlignment:
EnforcedStyleAlignWith: variable
Lint/UnusedMethodArgument:
Enabled: false
Style/SingleLineBlockParams:
Enabled: false
Style/DoubleNegation:
Enabled: false
Style/StringLiteralsInInterpolation:
Enabled: false
Style/AndOr:
Enabled: false
Style/SignalException:
Enabled: false
Style/StringLiterals:
Enabled: false
Style/BracesAroundHashParameters:
Enabled: false
Style/NumericLiterals:
Enabled: false
Layout/SpaceInsideArrayLiteralBrackets:
Enabled: false
Layout/SpaceBeforeBlockBraces:
Enabled: false
Style/Documentation:
Enabled: false
Style/ClassAndModuleChildren:
Enabled: false
Style/TrailingCommaInArrayLiteral:
Enabled: false
Style/TrailingCommaInHashLiteral:
Enabled: false
Layout/IndentHash:
EnforcedStyle: consistent
Style/FormatString:
Enabled: false
Layout/AlignParameters:
EnforcedStyle: with_fixed_indentation
Layout/MultilineOperationIndentation:
EnforcedStyle: indented
Style/IfUnlessModifier:
Enabled: false
Style/RaiseArgs:
Enabled: false
Style/PreferredHashMethods:
Enabled: false
Style/RegexpLiteral:
Enabled: false
Style/SymbolLiteral:
Enabled: false
Performance/Count:
Enabled: false
Naming/ConstantName:
Enabled: false
Layout/CaseIndentation:
Enabled: false
Style/ClassVars:
Enabled: false
Style/PerlBackrefs:
Enabled: false
Style/TrivialAccessors:
AllowPredicates: true
Style/WordArray:
Enabled: false
Naming/MethodName: Naming/MethodName:
Exclude: Exclude:

View File

@@ -1,23 +1,37 @@
# This configuration was generated by # This configuration was generated by
# `rubocop --auto-gen-config` # `rubocop --auto-gen-config`
# on 2019-04-22 19:11:24 -0400 using RuboCop version 0.53.0. # on 2019-08-29 12:16:25 +1000 using RuboCop version 0.74.0.
# The point is for the user to remove these configuration records # The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base. # one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new # Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again. # versions of RuboCop, may require this file to be generated again.
# Offense count: 1 # Offense count: 13
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: Include, TreatCommentsAsGroupSeparators. # Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle.
# Include: **/*.gemspec # SupportedHashRocketStyles: key, separator, table
Gemspec/OrderedDependencies: # SupportedColonStyles: key, separator, table
# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit
Layout/AlignHash:
Exclude: Exclude:
- 'liquid.gemspec' - 'lib/liquid/condition.rb'
- 'lib/liquid/expression.rb'
- 'performance/shopify/comment_form.rb'
- 'performance/shopify/database.rb'
- 'performance/shopify/paginate.rb'
- 'test/unit/context_unit_test.rb'
# Offense count: 3
# Cop supports --auto-correct.
# Configuration parameters: AllowForAlignment, AllowBeforeTrailingComments, ForceEqualSignAlignment.
Layout/ExtraSpacing:
Exclude:
- 'performance/shopify/paginate.rb'
# Offense count: 5 # Offense count: 5
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle. # Configuration parameters: EnforcedStyle.
# SupportedStyles: auto_detection, squiggly, active_support, powerpack, unindent # SupportedStyles: squiggly, active_support, powerpack, unindent
Layout/IndentHeredoc: Layout/IndentHeredoc:
Exclude: Exclude:
- 'test/integration/tags/for_tag_test.rb' - 'test/integration/tags/for_tag_test.rb'
@@ -32,6 +46,62 @@ Layout/MultilineMethodCallBraceLayout:
- 'test/integration/error_handling_test.rb' - 'test/integration/error_handling_test.rb'
- 'test/unit/strainer_unit_test.rb' - 'test/unit/strainer_unit_test.rb'
# Offense count: 4
# Cop supports --auto-correct.
# Configuration parameters: AllowForAlignment.
Layout/SpaceAroundOperators:
Exclude:
- 'lib/liquid/condition.rb'
- 'performance/shopify/paginate.rb'
# Offense count: 9
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces.
# SupportedStyles: space, no_space
# SupportedStylesForEmptyBraces: space, no_space
Layout/SpaceBeforeBlockBraces:
Exclude:
- 'example/server/server.rb'
- 'lib/liquid/variable.rb'
- 'test/integration/drop_test.rb'
- 'test/integration/standard_filter_test.rb'
- 'test/integration/tags/if_else_tag_test.rb'
# Offense count: 19
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBrackets.
# SupportedStyles: space, no_space, compact
# SupportedStylesForEmptyBrackets: space, no_space
Layout/SpaceInsideArrayLiteralBrackets:
Exclude:
- 'test/integration/drop_test.rb'
- 'test/integration/standard_filter_test.rb'
- 'test/integration/tags/for_tag_test.rb'
- 'test/integration/tags/include_tag_test.rb'
- 'test/integration/tags/standard_tag_test.rb'
- 'test/unit/context_unit_test.rb'
# Offense count: 2
Lint/AmbiguousOperator:
Exclude:
- 'test/unit/condition_unit_test.rb'
# Offense count: 16
# Configuration parameters: AllowSafeAssignment.
Lint/AssignmentInCondition:
Exclude:
- 'lib/liquid/block_body.rb'
- 'lib/liquid/lexer.rb'
- 'lib/liquid/standardfilters.rb'
- 'lib/liquid/tags/for.rb'
- 'lib/liquid/tags/if.rb'
- 'lib/liquid/tags/include.rb'
- 'lib/liquid/tags/raw.rb'
- 'lib/liquid/variable.rb'
- 'performance/profile.rb'
- 'test/test_helper.rb'
- 'test/unit/tokenizer_unit_test.rb'
# Offense count: 2 # Offense count: 2
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle. # Configuration parameters: EnforcedStyle.
@@ -40,145 +110,184 @@ Lint/InheritException:
Exclude: Exclude:
- 'lib/liquid/interrupts.rb' - 'lib/liquid/interrupts.rb'
# Offense count: 10
# Cop supports --auto-correct.
# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments.
Lint/UnusedBlockArgument:
Exclude:
- 'lib/liquid/condition.rb'
- 'lib/liquid/context.rb'
- 'lib/liquid/document.rb'
- 'lib/liquid/parse_context.rb'
- 'lib/liquid/template.rb'
- 'performance/shopify/json_filter.rb'
- 'test/integration/filter_test.rb'
- 'test/integration/render_profiling_test.rb'
- 'test/integration/variable_test.rb'
- 'test/unit/condition_unit_test.rb'
# Offense count: 12
# Cop supports --auto-correct.
# Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods.
Lint/UnusedMethodArgument:
Exclude:
- 'example/server/liquid_servlet.rb'
- 'test/integration/blank_test.rb'
- 'test/integration/error_handling_test.rb'
- 'test/integration/filter_test.rb'
- 'test/integration/output_test.rb'
- 'test/integration/standard_filter_test.rb'
- 'test/integration/tags/include_tag_test.rb'
- 'test/unit/strainer_unit_test.rb'
# Offense count: 2
Lint/UselessAssignment:
Exclude:
- 'performance/shopify/database.rb'
# Offense count: 1 # Offense count: 1
# Configuration parameters: CheckForMethodsWithNoSideEffects. # Configuration parameters: CheckForMethodsWithNoSideEffects.
Lint/Void: Lint/Void:
Exclude: Exclude:
- 'lib/liquid/parse_context.rb' - 'lib/liquid/parse_context.rb'
# Offense count: 53 # Offense count: 95
Metrics/AbcSize: # Cop supports --auto-correct.
Max: 56 # Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
# URISchemes: http, https
Metrics/LineLength:
Max: 294
# Offense count: 12 # Offense count: 44
Metrics/CyclomaticComplexity: Naming/ConstantName:
Max: 13
# Offense count: 112
# Configuration parameters: CountComments.
Metrics/MethodLength:
Max: 38
# Offense count: 8
Metrics/PerceivedComplexity:
Max: 11
# Offense count: 52
# Configuration parameters: Blacklist.
# Blacklist: END, (?-mix:EO[A-Z]{1})
Naming/HeredocDelimiterNaming:
Exclude: Exclude:
- 'test/integration/assign_test.rb' - 'lib/liquid.rb'
- 'test/integration/capture_test.rb' - 'lib/liquid/block_body.rb'
- 'test/integration/trim_mode_test.rb' - 'lib/liquid/tags/assign.rb'
- 'lib/liquid/tags/capture.rb'
# Offense count: 23 - 'lib/liquid/tags/case.rb'
# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. - 'lib/liquid/tags/cycle.rb'
# AllowedNames: io, id - 'lib/liquid/tags/for.rb'
Naming/UncommunicativeMethodParamName:
Exclude:
- 'example/server/example_servlet.rb'
- 'lib/liquid/condition.rb'
- 'lib/liquid/context.rb'
- 'lib/liquid/standardfilters.rb'
- 'lib/liquid/tags/if.rb' - 'lib/liquid/tags/if.rb'
- 'lib/liquid/utils.rb' - 'lib/liquid/tags/include.rb'
- 'lib/liquid/tags/raw.rb'
- 'lib/liquid/tags/table_row.rb'
- 'lib/liquid/variable.rb' - 'lib/liquid/variable.rb'
- 'test/integration/filter_test.rb' - 'performance/shopify/comment_form.rb'
- 'test/integration/standard_filter_test.rb' - 'performance/shopify/paginate.rb'
- 'test/integration/tags/for_tag_test.rb' - 'test/integration/tags/include_tag_test.rb'
- 'test/integration/template_test.rb'
- 'test/unit/condition_unit_test.rb'
# Offense count: 12 # Offense count: 2
# Configuration parameters: .
# SupportedStyles: snake_case, camelCase
Naming/MethodName:
EnforcedStyle: snake_case
# Offense count: 3
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle. # Configuration parameters: EnforcedStyle.
# SupportedStyles: prefer_alias, prefer_alias_method # SupportedStyles: always, conditionals
Style/Alias: Style/AndOr:
Exclude: Exclude:
- 'lib/liquid/drop.rb'
- 'lib/liquid/i18n.rb' - 'lib/liquid/i18n.rb'
- 'lib/liquid/profiler/hooks.rb' - 'lib/liquid/tags/table_row.rb'
- 'lib/liquid/standardfilters.rb' - 'lib/liquid/tokenizer.rb'
- 'lib/liquid/tag.rb'
- 'lib/liquid/tags/include.rb'
- 'lib/liquid/variable.rb'
# Offense count: 22 # Offense count: 40
Style/CommentedKeyword: # Cop supports --auto-correct.
Enabled: false # Configuration parameters: EnforcedStyle.
# SupportedStyles: braces, no_braces, context_dependent
Style/BracesAroundHashParameters:
Exclude:
- 'test/integration/error_handling_test.rb'
- 'test/integration/filter_test.rb'
- 'test/integration/render_profiling_test.rb'
- 'test/integration/standard_filter_test.rb'
- 'test/integration/tags/echo_test.rb'
- 'test/integration/tags/increment_tag_test.rb'
- 'test/integration/tags/standard_tag_test.rb'
- 'test/integration/template_test.rb'
- 'test/unit/condition_unit_test.rb'
- 'test/unit/context_unit_test.rb'
# Offense count: 1 # Offense count: 5
Style/ClassVars:
Exclude:
- 'lib/liquid/condition.rb'
- 'lib/liquid/strainer.rb'
- 'lib/liquid/template.rb'
# Offense count: 2
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SingleLineConditionsOnly, IncludeTernaryExpressions. # Configuration parameters: EnforcedStyle, SingleLineConditionsOnly, IncludeTernaryExpressions.
# SupportedStyles: assign_to_condition, assign_inside_condition # SupportedStyles: assign_to_condition, assign_inside_condition
Style/ConditionalAssignment: Style/ConditionalAssignment:
Exclude: Exclude:
- 'lib/liquid/errors.rb' - 'lib/liquid/errors.rb'
- 'performance/shopify/shop_filter.rb'
# Offense count: 1 # Offense count: 1
# Configuration parameters: AllowCoercion.
Style/DateTime: Style/DateTime:
Exclude: Exclude:
- 'test/unit/context_unit_test.rb' - 'test/unit/context_unit_test.rb'
# Offense count: 2 # Offense count: 2
# Cop supports --auto-correct. # Cop supports --auto-correct.
Style/EachWithObject:
Exclude:
- 'performance/shopify/database.rb'
# Offense count: 1
# Cop supports --auto-correct.
Style/EmptyCaseCondition: Style/EmptyCaseCondition:
Exclude: Exclude:
- 'lib/liquid/block_body.rb'
- 'lib/liquid/lexer.rb' - 'lib/liquid/lexer.rb'
# Offense count: 5 # Offense count: 1
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle. # Configuration parameters: EnforcedStyle.
# SupportedStyles: compact, expanded # SupportedStyles: each, for
Style/EmptyMethod: Style/For:
Exclude: Exclude:
- 'lib/liquid/tag.rb' - 'performance/shopify/shop_filter.rb'
- 'lib/liquid/tags/comment.rb'
- 'lib/liquid/tags/include.rb'
- 'test/integration/tags/include_tag_test.rb'
- 'test/unit/context_unit_test.rb'
# Offense count: 3 # Offense count: 9
# Cop supports --auto-correct. # Cop supports --auto-correct.
Style/Encoding:
Exclude:
- 'lib/liquid/version.rb'
- 'liquid.gemspec'
- 'test/integration/standard_filter_test.rb'
# Offense count: 2
# Cop supports --auto-correct.
Style/ExpandPathArguments:
Exclude:
- 'Rakefile'
- 'liquid.gemspec'
# Offense count: 7
# Configuration parameters: EnforcedStyle. # Configuration parameters: EnforcedStyle.
# SupportedStyles: annotated, template, unannotated # SupportedStyles: format, sprintf, percent
Style/FormatStringToken: Style/FormatString:
Exclude: Exclude:
- 'example/server/example_servlet.rb'
- 'performance/shopify/money_filter.rb'
- 'performance/shopify/weight_filter.rb'
- 'test/integration/filter_test.rb' - 'test/integration/filter_test.rb'
- 'test/integration/hash_ordering_test.rb' - 'test/integration/hash_ordering_test.rb'
# Offense count: 14 # Offense count: 115
# Configuration parameters: MinBodyLength. # Cop supports --auto-correct.
Style/GuardClause: # Configuration parameters: EnforcedStyle.
# SupportedStyles: always, never
Style/FrozenStringLiteralComment:
Enabled: false
# Offense count: 30
# Cop supports --auto-correct.
# Configuration parameters: IgnoreMacros, IgnoredMethods, IncludedMacros, AllowParenthesesInMultilineCall, AllowParenthesesInChaining, AllowParenthesesInCamelCaseMethod, EnforcedStyle.
# SupportedStyles: require_parentheses, omit_parentheses
Style/MethodCallWithArgsParentheses:
Exclude: Exclude:
- 'lib/liquid/condition.rb' - 'Gemfile'
- 'lib/liquid/lexer.rb' - 'Rakefile'
- 'lib/liquid/strainer.rb' - 'lib/liquid/block_body.rb'
- 'lib/liquid/tags/assign.rb' - 'lib/liquid/parser.rb'
- 'lib/liquid/tags/capture.rb'
- 'lib/liquid/tags/case.rb'
- 'lib/liquid/tags/for.rb' - 'lib/liquid/tags/for.rb'
- 'lib/liquid/tags/include.rb' - 'liquid.gemspec'
- 'lib/liquid/tags/raw.rb' - 'performance/shopify/database.rb'
- 'lib/liquid/tags/table_row.rb' - 'performance/shopify/liquid.rb'
- 'lib/liquid/variable.rb' - 'test/test_helper.rb'
- 'test/unit/tokenizer_unit_test.rb' - 'test/unit/condition_unit_test.rb'
- 'test/unit/tags/if_tag_unit_test.rb'
# Offense count: 1 # Offense count: 1
# Cop supports --auto-correct. # Cop supports --auto-correct.
@@ -188,27 +297,17 @@ Style/Next:
Exclude: Exclude:
- 'lib/liquid/tags/for.rb' - 'lib/liquid/tags/for.rb'
# Offense count: 4 # Offense count: 52
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: AutoCorrect, EnforcedStyle. Style/PerlBackrefs:
# SupportedStyles: predicate, comparison Enabled: false
Style/NumericPredicate:
Exclude:
- 'spec/**/*'
- 'lib/liquid/context.rb'
- 'lib/liquid/forloop_drop.rb'
- 'lib/liquid/standardfilters.rb'
- 'lib/liquid/tablerowloop_drop.rb'
# Offense count: 14 # Offense count: 33
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: PreferredDelimiters. # Configuration parameters: EnforcedStyle.
Style/PercentLiteralDelimiters: # SupportedStyles: compact, exploded
Exclude: Style/RaiseArgs:
- 'lib/liquid/tags/if.rb' Enabled: false
- 'liquid.gemspec'
- 'test/integration/assign_test.rb'
- 'test/integration/standard_filter_test.rb'
# Offense count: 1 # Offense count: 1
# Cop supports --auto-correct. # Cop supports --auto-correct.
@@ -216,21 +315,52 @@ Style/RedundantSelf:
Exclude: Exclude:
- 'lib/liquid/strainer.rb' - 'lib/liquid/strainer.rb'
# Offense count: 9 # Offense count: 5
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, AllowInnerSlashes.
# SupportedStyles: slashes, percent_r, mixed
Style/RegexpLiteral:
Exclude:
- 'lib/liquid/file_system.rb'
- 'lib/liquid/standardfilters.rb'
- 'performance/shopify/shop_filter.rb'
- 'test/unit/condition_unit_test.rb'
# Offense count: 3
# Cop supports --auto-correct.
# Configuration parameters: ConvertCodeThatCanStartToReturnNil, Whitelist.
# Whitelist: present?, blank?, presence, try, try!
Style/SafeNavigation:
Exclude:
- 'lib/liquid/drop.rb'
- 'lib/liquid/strainer.rb'
- 'lib/liquid/tokenizer.rb'
# Offense count: 10
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: AllowAsExpressionSeparator. # Configuration parameters: AllowAsExpressionSeparator.
Style/Semicolon: Style/Semicolon:
Exclude: Exclude:
- 'performance/shopify/database.rb'
- 'test/integration/error_handling_test.rb' - 'test/integration/error_handling_test.rb'
- 'test/integration/template_test.rb' - 'test/integration/template_test.rb'
- 'test/unit/context_unit_test.rb' - 'test/unit/context_unit_test.rb'
# Offense count: 7 # Offense count: 1
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: MinSize. # Configuration parameters: EnforcedStyle.
# SupportedStyles: percent, brackets # SupportedStyles: use_perl_names, use_english_names
Style/SymbolArray: Style/SpecialGlobalVars:
EnforcedStyle: brackets Exclude:
- 'performance/shopify/liquid.rb'
# Offense count: 2
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: single_quotes, double_quotes
Style/StringLiteralsInInterpolation:
Exclude:
- 'performance/shopify/tag_filter.rb'
# Offense count: 2 # Offense count: 2
# Cop supports --auto-correct. # Cop supports --auto-correct.
@@ -241,6 +371,33 @@ Style/TernaryParentheses:
- 'lib/liquid/context.rb' - 'lib/liquid/context.rb'
- 'lib/liquid/utils.rb' - 'lib/liquid/utils.rb'
# Offense count: 21
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyleForMultiline.
# SupportedStylesForMultiline: comma, consistent_comma, no_comma
Style/TrailingCommaInArrayLiteral:
Exclude:
- 'lib/liquid/parse_tree_visitor.rb'
- 'lib/liquid/tags/include.rb'
- 'test/integration/parse_tree_visitor_test.rb'
- 'test/integration/standard_filter_test.rb'
# Offense count: 9
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyleForMultiline.
# SupportedStylesForMultiline: comma, consistent_comma, no_comma
Style/TrailingCommaInHashLiteral:
Exclude:
- 'lib/liquid/condition.rb'
- 'lib/liquid/lexer.rb'
- 'lib/liquid/standardfilters.rb'
- 'performance/shopify/comment_form.rb'
- 'performance/shopify/database.rb'
- 'performance/shopify/paginate.rb'
- 'performance/theme_runner.rb'
- 'test/integration/output_test.rb'
- 'test/unit/context_unit_test.rb'
# Offense count: 2 # Offense count: 2
# Cop supports --auto-correct. # Cop supports --auto-correct.
Style/UnneededPercentQ: Style/UnneededPercentQ:
@@ -252,9 +409,3 @@ Style/UnneededPercentQ:
Style/WhileUntilModifier: Style/WhileUntilModifier:
Exclude: Exclude:
- 'lib/liquid/tags/case.rb' - 'lib/liquid/tags/case.rb'
# Offense count: 648
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
# URISchemes: http, https
Metrics/LineLength:
Max: 294

View File

@@ -1,20 +1,14 @@
language: ruby language: ruby
cache: bundler
rvm: rvm:
- 2.1
- 2.2
- 2.3
- 2.4 - 2.4
- 2.5 - 2.5
- &latest_ruby 2.6 - &latest_ruby 2.6
- 2.7
- ruby-head - ruby-head
- jruby-head - jruby-head
# - rbx-2 - truffleruby
addons:
apt:
packages:
- libgmp3-dev
matrix: matrix:
include: include:
@@ -24,11 +18,13 @@ matrix:
allow_failures: allow_failures:
- rvm: ruby-head - rvm: ruby-head
- rvm: jruby-head - rvm: jruby-head
- rvm: truffleruby
install: branches:
- bundle install only:
- master
script: bundle exec rake - gh-pages
- /.*-stable/
notifications: notifications:
disable: true disable: true

View File

@@ -10,15 +10,16 @@ group :benchmark, :test do
gem 'memory_profiler' gem 'memory_profiler'
gem 'terminal-table' gem 'terminal-table'
install_if -> { RUBY_PLATFORM !~ /mingw|mswin|java/ } do install_if -> { RUBY_PLATFORM !~ /mingw|mswin|java/ && RUBY_ENGINE != 'truffleruby' } do
gem 'stackprof' gem 'stackprof'
end end
end end
group :test do group :test do
gem 'rubocop', '~> 0.53.0' gem 'rubocop', '~> 0.74.0', require: false
gem 'rubocop-performance', require: false
platform :mri do platform :mri, :truffleruby do
gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'liquid-tag' gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'master'
end end
end end

View File

@@ -106,3 +106,9 @@ template = Liquid::Template.parse("{{x}} {{y}}")
template.render!({ 'x' => 1}, { strict_variables: true }) template.render!({ 'x' => 1}, { strict_variables: true })
#=> Liquid::UndefinedVariable: Liquid error: undefined variable y #=> Liquid::UndefinedVariable: Liquid error: undefined variable y
``` ```
### Usage tracking
To help track usages of a feature or code path in production, we have released opt-in usage tracking. To enable this, we provide an empty `Liquid:: Usage.increment` method which you can customize to your needs. The feature is well suited to https://github.com/Shopify/statsd-instrument. However, the choice of implementation is up to you.
Once you have enabled usage tracking, we recommend reporting any events through Github Issues that your system may be logging. It is highly likely this event has been added to consider deprecating or improving code specific to this event, so please raise any concerns.

View File

@@ -19,8 +19,10 @@ task :warn_test do
end end
task :rubocop do task :rubocop do
require 'rubocop/rake_task' if RUBY_ENGINE == 'ruby'
RuboCop::RakeTask.new require 'rubocop/rake_task'
RuboCop::RakeTask.new
end
end end
desc 'runs test suite with both strict and lax parsers' desc 'runs test suite with both strict and lax parsers'
@@ -32,8 +34,8 @@ task :test do
Rake::Task['base_test'].reenable Rake::Task['base_test'].reenable
Rake::Task['base_test'].invoke Rake::Task['base_test'].invoke
if RUBY_ENGINE == 'ruby' if RUBY_ENGINE == 'ruby' || RUBY_ENGINE == 'truffleruby'
ENV['LIQUID-C'] = '1' ENV['LIQUID_C'] = '1'
ENV['LIQUID_PARSER_MODE'] = 'lax' ENV['LIQUID_PARSER_MODE'] = 'lax'
Rake::Task['base_test'].reenable Rake::Task['base_test'].reenable

View File

@@ -74,6 +74,8 @@ require 'liquid/condition'
require 'liquid/utils' require 'liquid/utils'
require 'liquid/tokenizer' require 'liquid/tokenizer'
require 'liquid/parse_context' require 'liquid/parse_context'
require 'liquid/partial_cache'
require 'liquid/usage'
# Load all the tags of the standard library # Load all the tags of the standard library
# #

View File

@@ -12,26 +12,41 @@ module Liquid
# #
# context['bob'] #=> nil class Context # context['bob'] #=> nil class Context
class Context class Context
attr_reader :scopes, :errors, :registers, :environments, :resource_limits attr_reader :scopes, :errors, :registers, :environments, :resource_limits, :static_registers, :static_environments
attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil) # rubocop:disable Metrics/ParameterLists
@environments = [environments].flatten def self.build(environments: {}, outer_scope: {}, registers: {}, rethrow_errors: false, resource_limits: nil, static_registers: {}, static_environments: {})
@scopes = [(outer_scope || {})] new(environments, outer_scope, registers, rethrow_errors, resource_limits, static_registers, static_environments)
@registers = registers end
@errors = []
@interrupts = [] def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil, static_registers = {}, static_environments = {})
@filters = [] @environments = [environments]
@global_filter = nil @environments.flatten!
@partial = false
@strict_variables = false @static_environments = [static_environments].flat_map(&:freeze).freeze
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits) @scopes = [(outer_scope || {})]
@registers = registers
@static_registers = static_registers.freeze
@errors = []
@partial = false
@strict_variables = false
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
@base_scope_depth = 0
squash_instance_assigns_with_environments
@this_stack_used = false
self.exception_renderer = Template.default_exception_renderer self.exception_renderer = Template.default_exception_renderer
self.exception_renderer = ->(e) { raise } if rethrow_errors if rethrow_errors
self.exception_renderer = ->(e) { raise }
end
squash_instance_assigns_with_environments @interrupts = []
@filters = []
@global_filter = nil
end end
# rubocop:enable Metrics/ParameterLists
def warnings def warnings
@warnings ||= [] @warnings ||= []
@@ -83,9 +98,9 @@ module Liquid
end end
# Push new local scope on the stack. use <tt>Context#stack</tt> instead # Push new local scope on the stack. use <tt>Context#stack</tt> instead
def push def push(new_scope = {})
@scopes.unshift({}) @scopes.unshift(new_scope)
raise StackLevelError, "Nesting too deep".freeze if @scopes.length > (Block::MAX_DEPTH + 1) check_overflow
end end
# Merge a hash of variables in the current local scope # Merge a hash of variables in the current local scope
@@ -107,15 +122,50 @@ module Liquid
# end # end
# #
# context['var] #=> nil # context['var] #=> nil
def stack def stack(new_scope = nil)
push old_stack_used = @this_stack_used
if new_scope
push(new_scope)
@this_stack_used = true
else
@this_stack_used = false
end
yield yield
ensure ensure
pop pop if @this_stack_used
@this_stack_used = old_stack_used
end
# Creates a new context inheriting resource limits, filters, environment etc.,
# but with an isolated scope.
def new_isolated_subcontext
check_overflow
Context.build(
resource_limits: resource_limits,
static_environments: static_environments,
static_registers: static_registers
).tap do |subcontext|
subcontext.base_scope_depth = base_scope_depth + 1
subcontext.exception_renderer = exception_renderer
subcontext.filters = @filters
subcontext.strainer = nil
subcontext.errors = errors
subcontext.warnings = warnings
end
end
def clear_instance_assigns
@scopes[0] = {}
end end
# Only allow String, Numeric, Hash, Array, Proc, Boolean or <tt>Liquid::Drop</tt> # Only allow String, Numeric, Hash, Array, Proc, Boolean or <tt>Liquid::Drop</tt>
def []=(key, value) def []=(key, value)
unless @this_stack_used
@this_stack_used = true
push({})
end
@scopes[0][key] = value @scopes[0][key] = value
end end
@@ -143,12 +193,15 @@ module Liquid
def find_variable(key, raise_on_not_found: true) def find_variable(key, raise_on_not_found: true)
# This was changed from find() to find_index() because this is a very hot # This was changed from find() to find_index() because this is a very hot
# path and find_index() is optimized in MRI to reduce object allocation # path and find_index() is optimized in MRI to reduce object allocation
index = @scopes.find_index { |s| s.key?(key) }
scope = (index = @scopes.find_index { |s| s.key?(key) }) && @scopes[index] variable = if index
scope ||= (index = @environments.find_index { |s| s.key?(key) }) && @environments[index] lookup_and_evaluate(@scopes[index], key, raise_on_not_found: raise_on_not_found)
scope ||= {} else
try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found)
end
variable = lookup_and_evaluate(scope, key, raise_on_not_found: raise_on_not_found).to_liquid variable = variable.to_liquid
variable.context = self if variable.respond_to?(:context=) variable.context = self if variable.respond_to?(:context=)
variable variable
@@ -168,8 +221,38 @@ module Liquid
end end
end end
protected
attr_writer :base_scope_depth, :warnings, :errors, :strainer, :filters
private private
attr_reader :base_scope_depth
def try_variable_find_in_environments(key, raise_on_not_found:)
@environments.each do |environment|
found_variable = lookup_and_evaluate(environment, key, raise_on_not_found: raise_on_not_found)
if !found_variable.nil? || @strict_variables && raise_on_not_found
return found_variable
end
end
@static_environments.each do |environment|
found_variable = lookup_and_evaluate(environment, key, raise_on_not_found: raise_on_not_found)
if !found_variable.nil? || @strict_variables && raise_on_not_found
return found_variable
end
end
nil
end
def check_overflow
raise StackLevelError, "Nesting too deep".freeze if overflow?
end
def overflow?
base_scope_depth + @scopes.length > Block::MAX_DEPTH
end
def internal_error def internal_error
# raise and catch to set backtrace and cause on exception # raise and catch to set backtrace and cause on exception
raise Liquid::InternalError, 'internal' raise Liquid::InternalError, 'internal'

View File

@@ -22,5 +22,6 @@
tag_never_closed: "'%{block_name}' tag was never closed" tag_never_closed: "'%{block_name}' tag was never closed"
meta_syntax_error: "Liquid syntax error: #{e.message}" meta_syntax_error: "Liquid syntax error: #{e.message}"
table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3" table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
render: "Syntax error in tag 'render' - Template name must be a quoted string"
argument: argument:
include: "Argument error in tag 'include' - Illegal template name" include: "Argument error in tag 'include' - Illegal template name"

View File

@@ -0,0 +1,18 @@
module Liquid
class PartialCache
def self.load(template_name, context:, parse_context:)
cached_partials = (context.registers[:cached_partials] ||= {})
cached = cached_partials[template_name]
return cached if cached
file_system = (context.registers[:file_system] ||= Liquid::Template.file_system)
source = file_system.read_template_file(template_name)
parse_context.partial = true
partial = Liquid::Template.parse(source, parse_context)
cached_partials[template_name] = partial
ensure
parse_context.partial = false
end
end
end

View File

@@ -421,6 +421,7 @@ module Liquid
def default(input, default_value = ''.freeze) def default(input, default_value = ''.freeze)
if !input || input.respond_to?(:empty?) && input.empty? if !input || input.respond_to?(:empty?) && input.empty?
Usage.increment("default_filter_received_false_value") if input == false # See https://github.com/Shopify/liquid/issues/1127
default_value default_value
else else
input input

View File

@@ -16,7 +16,7 @@ module Liquid
end end
def render(context) def render(context)
@variable.render(context) @variable.render_to_output_buffer(context, '')
end end
end end

View File

@@ -46,7 +46,12 @@ module Liquid
template_name = context.evaluate(@template_name_expr) template_name = context.evaluate(@template_name_expr)
raise ArgumentError.new(options[:locale].t("errors.argument.include")) unless template_name raise ArgumentError.new(options[:locale].t("errors.argument.include")) unless template_name
partial = load_cached_partial(template_name, context) partial = PartialCache.load(
template_name,
context: context,
parse_context: parse_context
)
context_variable_name = template_name.split('/'.freeze).last context_variable_name = template_name.split('/'.freeze).last
variable = if @variable_name_expr variable = if @variable_name_expr
@@ -83,35 +88,9 @@ module Liquid
output output
end end
private
alias_method :parse_context, :options alias_method :parse_context, :options
private :parse_context private :parse_context
def load_cached_partial(template_name, context)
cached_partials = context.registers[:cached_partials] || {}
if cached = cached_partials[template_name]
return cached
end
source = read_template_from_file_system(context)
begin
parse_context.partial = true
partial = Liquid::Template.parse(source, parse_context)
ensure
parse_context.partial = false
end
cached_partials[template_name] = partial
context.registers[:cached_partials] = cached_partials
partial
end
def read_template_from_file_system(context)
file_system = context.registers[:file_system] || Liquid::Template.file_system
file_system.read_template_file(context.evaluate(@template_name_expr))
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children def children
[ [

View File

@@ -23,6 +23,7 @@ module Liquid
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
value = context.environments.first[@variable] ||= 0 value = context.environments.first[@variable] ||= 0
context.environments.first[@variable] = value + 1 context.environments.first[@variable] = value + 1
output << value.to_s output << value.to_s
output output
end end

54
lib/liquid/tags/render.rb Normal file
View File

@@ -0,0 +1,54 @@
module Liquid
class Render < Tag
SYNTAX = /(#{QuotedString})#{QuotedFragment}*/o
attr_reader :template_name_expr, :attributes
def initialize(tag_name, markup, options)
super
raise SyntaxError.new(options[:locale].t("errors.syntax.render".freeze)) unless markup =~ SYNTAX
template_name = $1
@template_name_expr = Expression.parse(template_name)
@attributes = {}
markup.scan(TagAttributes) do |key, value|
@attributes[key] = Expression.parse(value)
end
end
def render_to_output_buffer(context, output)
# Though we evaluate this here we will only ever parse it as a string literal.
template_name = context.evaluate(@template_name_expr)
raise ArgumentError.new(options[:locale].t("errors.argument.include")) unless template_name
partial = PartialCache.load(
template_name,
context: context,
parse_context: parse_context
)
inner_context = context.new_isolated_subcontext
inner_context.template_name = template_name
inner_context.partial = true
@attributes.each do |key, value|
inner_context[key] = context.evaluate(value)
end
partial.render_to_output_buffer(inner_context, output)
output
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[
@node.template_name_expr,
] + @node.attributes.values
end
end
end
Template.register_tag('render'.freeze, Render)
end

6
lib/liquid/usage.rb Normal file
View File

@@ -0,0 +1,6 @@
module Liquid
module Usage
def self.increment(name)
end
end
end

View File

@@ -16,7 +16,7 @@ Gem::Specification.new do |s|
s.license = "MIT" s.license = "MIT"
# s.description = "A secure, non-evaling end user template engine with aesthetic markup." # s.description = "A secure, non-evaling end user template engine with aesthetic markup."
s.required_ruby_version = ">= 2.1.0" s.required_ruby_version = ">= 2.4.0"
s.required_rubygems_version = ">= 1.3.7" s.required_rubygems_version = ">= 1.3.7"
s.test_files = Dir.glob("{test}/**/*") s.test_files = Dir.glob("{test}/**/*")

View File

@@ -30,6 +30,7 @@ class Profiler
@retained << "#{report.scale_bytes(report.total_retained_memsize)} (#{report.total_retained} objects)" @retained << "#{report.scale_bytes(report.total_retained_memsize)} (#{report.total_retained} objects)"
return if ENV['CI'] return if ENV['CI']
require 'fileutils' require 'fileutils'
report_file = File.join(REPORTS_DIR, "#{sanitize(phase)}.txt") report_file = File.join(REPORTS_DIR, "#{sanitize(phase)}.txt")
FileUtils.mkdir_p(REPORTS_DIR) FileUtils.mkdir_p(REPORTS_DIR)

View File

@@ -5,72 +5,72 @@ class LiquidTagTest < Minitest::Test
def test_liquid_tag def test_liquid_tag
assert_template_result('1 2 3', <<~LIQUID, 'array' => [1, 2, 3]) assert_template_result('1 2 3', <<~LIQUID, 'array' => [1, 2, 3])
{%- liquid {%- liquid
echo array | join: " " echo array | join: " "
-%} -%}
LIQUID LIQUID
assert_template_result('1 2 3', <<~LIQUID, 'array' => [1, 2, 3]) assert_template_result('1 2 3', <<~LIQUID, 'array' => [1, 2, 3])
{%- liquid {%- liquid
for value in array for value in array
echo value echo value
unless forloop.last unless forloop.last
echo " " echo " "
endunless endunless
endfor endfor
-%} -%}
LIQUID LIQUID
assert_template_result('4 8 12 6', <<~LIQUID, 'array' => [1, 2, 3]) assert_template_result('4 8 12 6', <<~LIQUID, 'array' => [1, 2, 3])
{%- liquid {%- liquid
for value in array for value in array
assign double_value = value | times: 2 assign double_value = value | times: 2
echo double_value | times: 2 echo double_value | times: 2
unless forloop.last unless forloop.last
echo " " echo " "
endunless endunless
endfor endfor
echo " " echo " "
echo double_value echo double_value
-%} -%}
LIQUID LIQUID
assert_template_result('abc', <<~LIQUID) assert_template_result('abc', <<~LIQUID)
{%- liquid echo "a" -%} {%- liquid echo "a" -%}
b b
{%- liquid echo "c" -%} {%- liquid echo "c" -%}
LIQUID LIQUID
end end
def test_liquid_tag_errors def test_liquid_tag_errors
assert_match_syntax_error("syntax error (line 1): Unknown tag 'error'", <<~LIQUID) assert_match_syntax_error("syntax error (line 1): Unknown tag 'error'", <<~LIQUID)
{%- liquid error no such tag -%} {%- liquid error no such tag -%}
LIQUID LIQUID
assert_match_syntax_error("syntax error (line 7): Unknown tag 'error'", <<~LIQUID) assert_match_syntax_error("syntax error (line 7): Unknown tag 'error'", <<~LIQUID)
{{ test }} {{ test }}
{%- {%-
liquid liquid
for value in array for value in array
error no such tag error no such tag
endfor endfor
-%} -%}
LIQUID LIQUID
assert_match_syntax_error("syntax error (line 2): Unknown tag '!!! the guards are vigilant'", <<~LIQUID) assert_match_syntax_error("syntax error (line 2): Unknown tag '!!! the guards are vigilant'", <<~LIQUID)
{%- liquid {%- liquid
!!! the guards are vigilant !!! the guards are vigilant
-%} -%}
LIQUID LIQUID
assert_match_syntax_error("syntax error (line 4): 'for' tag was never closed", <<~LIQUID) assert_match_syntax_error("syntax error (line 4): 'for' tag was never closed", <<~LIQUID)
{%- liquid {%- liquid
for value in array for value in array
echo 'forgot to close the for tag' echo 'forgot to close the for tag'
-%} -%}
LIQUID LIQUID
end end
@@ -81,24 +81,24 @@ class LiquidTagTest < Minitest::Test
def test_cannot_open_blocks_living_past_a_liquid_tag def test_cannot_open_blocks_living_past_a_liquid_tag
assert_match_syntax_error("syntax error (line 3): 'if' tag was never closed", <<~LIQUID) assert_match_syntax_error("syntax error (line 3): 'if' tag was never closed", <<~LIQUID)
{%- liquid {%- liquid
if true if true
-%} -%}
{%- endif -%} {%- endif -%}
LIQUID LIQUID
end end
def test_quirk_can_close_blocks_created_before_a_liquid_tag def test_quirk_can_close_blocks_created_before_a_liquid_tag
assert_template_result("42", <<~LIQUID) assert_template_result("42", <<~LIQUID)
{%- if true -%} {%- if true -%}
42 42
{%- liquid endif -%} {%- liquid endif -%}
LIQUID LIQUID
end end
def test_liquid_tag_in_raw def test_liquid_tag_in_raw
assert_template_result("{% liquid echo 'test' %}\n", <<~LIQUID) assert_template_result("{% liquid echo 'test' %}\n", <<~LIQUID)
{% raw %}{% liquid echo 'test' %}{% endraw %} {% raw %}{% liquid echo 'test' %}{% endraw %}
LIQUID LIQUID
end end
end end

View File

@@ -0,0 +1,149 @@
require 'test_helper'
class RenderTagTest < Minitest::Test
include Liquid
def test_render_with_no_arguments
Liquid::Template.file_system = StubFileSystem.new('source' => 'rendered content')
assert_template_result 'rendered content', '{% render "source" %}'
end
def test_render_tag_looks_for_file_system_in_registers_first
file_system = StubFileSystem.new('pick_a_source' => 'from register file system')
assert_equal 'from register file system',
Template.parse('{% render "pick_a_source" %}').render!({}, registers: { file_system: file_system })
end
def test_render_passes_named_arguments_into_inner_scope
Liquid::Template.file_system = StubFileSystem.new('product' => '{{ inner_product.title }}')
assert_template_result 'My Product', '{% render "product", inner_product: outer_product %}',
'outer_product' => { 'title' => 'My Product' }
end
def test_render_accepts_literals_as_arguments
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ price }}')
assert_template_result '123', '{% render "snippet", price: 123 %}'
end
def test_render_accepts_multiple_named_arguments
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ one }} {{ two }}')
assert_template_result '1 2', '{% render "snippet", one: 1, two: 2 %}'
end
def test_render_does_not_inherit_parent_scope_variables
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ outer_variable }}')
assert_template_result '', '{% assign outer_variable = "should not be visible" %}{% render "snippet" %}'
end
def test_render_does_not_inherit_variable_with_same_name_as_snippet
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ snippet }}')
assert_template_result '', "{% assign snippet = 'should not be visible' %}{% render 'snippet' %}"
end
def test_render_sets_the_correct_template_name_for_errors
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ unsafe }}')
with_taint_mode :error do
template = Liquid::Template.parse('{% render "snippet", unsafe: unsafe %}')
context = Context.new('unsafe' => (+'unsafe').tap(&:taint))
template.render(context)
assert_equal [Liquid::TaintedError], template.errors.map(&:class)
assert_equal 'snippet', template.errors.first.template_name
end
end
def test_render_sets_the_correct_template_name_for_warnings
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ unsafe }}')
with_taint_mode :warn do
template = Liquid::Template.parse('{% render "snippet", unsafe: unsafe %}')
context = Context.new('unsafe' => (+'unsafe').tap(&:taint))
template.render(context)
assert_equal [Liquid::TaintedError], context.warnings.map(&:class)
assert_equal 'snippet', context.warnings.first.template_name
end
end
def test_render_does_not_mutate_parent_scope
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{% assign inner = 1 %}')
assert_template_result '', "{% render 'snippet' %}{{ inner }}"
end
def test_nested_render_tag
Liquid::Template.file_system = StubFileSystem.new(
'one' => "one {% render 'two' %}",
'two' => 'two'
)
assert_template_result 'one two', "{% render 'one' %}"
end
def test_recursively_rendered_template_does_not_produce_endless_loop
Liquid::Template.file_system = StubFileSystem.new('loop' => '{% render "loop" %}')
assert_raises Liquid::StackLevelError do
Template.parse('{% render "loop" %}').render!
end
end
def test_includes_and_renders_count_towards_the_same_recursion_limit
Liquid::Template.file_system = StubFileSystem.new(
'loop_render' => '{% render "loop_include" %}',
'loop_include' => '{% include "loop_render" %}'
)
assert_raises Liquid::StackLevelError do
Template.parse('{% render "loop_include" %}').render!
end
end
def test_dynamically_choosen_templates_are_not_allowed
Liquid::Template.file_system = StubFileSystem.new('snippet' => 'should not be rendered')
assert_raises Liquid::SyntaxError do
Liquid::Template.parse("{% assign name = 'snippet' %}{% render name %}")
end
end
def test_include_tag_caches_second_read_of_same_partial
file_system = StubFileSystem.new('snippet' => 'echo')
assert_equal 'echoecho',
Template.parse('{% render "snippet" %}{% render "snippet" %}')
.render!({}, registers: { file_system: file_system })
assert_equal 1, file_system.file_read_count
end
def test_render_tag_doesnt_cache_partials_across_renders
file_system = StubFileSystem.new('snippet' => 'my message')
assert_equal 'my message',
Template.parse('{% include "snippet" %}').render!({}, registers: { file_system: file_system })
assert_equal 1, file_system.file_read_count
assert_equal 'my message',
Template.parse('{% include "snippet" %}').render!({}, registers: { file_system: file_system })
assert_equal 2, file_system.file_read_count
end
def test_render_tag_within_if_statement
Liquid::Template.file_system = StubFileSystem.new('snippet' => 'my message')
assert_template_result 'my message', '{% if true %}{% render "snippet" %}{% endif %}'
end
def test_break_through_render
Liquid::Template.file_system = StubFileSystem.new('break' => '{% break %}')
assert_template_result '1', '{% for i in (1..3) %}{{ i }}{% break %}{{ i }}{% endfor %}'
assert_template_result '112233', '{% for i in (1..3) %}{{ i }}{% render "break" %}{{ i }}{% endfor %}'
end
def test_increment_is_isolated_between_renders
Liquid::Template.file_system = StubFileSystem.new('incr' => '{% increment %}')
assert_template_result '010', '{% increment %}{% increment %}{% render "incr" %}'
end
def test_decrement_is_isolated_between_renders
Liquid::Template.file_system = StubFileSystem.new('decr' => '{% decrement %}')
assert_template_result '-1-2-1', '{% decrement %}{% decrement %}{% render "decr" %}'
end
end

View File

@@ -80,7 +80,10 @@ class VariableTest < Minitest::Test
assigns['test'] = 'Tobi' assigns['test'] = 'Tobi'
assert_equal 'Hello Tobi', template.render!(assigns) assert_equal 'Hello Tobi', template.render!(assigns)
assigns.delete('test') assigns.delete('test')
assert_equal "Hello ", template.render!(assigns) e = assert_raises(RuntimeError) do
template.render!(assigns)
end
assert_equal "Unknown variable 'test'", e.message
end end
def test_multiline_variable def test_multiline_variable

View File

@@ -14,7 +14,7 @@ if env_mode = ENV['LIQUID_PARSER_MODE']
end end
Liquid::Template.error_mode = mode Liquid::Template.error_mode = mode
if ENV['LIQUID-C'] == '1' if ENV['LIQUID_C'] == '1'
puts "-- LIQUID C" puts "-- LIQUID C"
require 'liquid/c' require 'liquid/c'
end end
@@ -121,3 +121,17 @@ class ErrorDrop < Liquid::Drop
raise Exception, 'exception' raise Exception, 'exception'
end end
end end
class StubFileSystem
attr_reader :file_read_count
def initialize(values)
@file_read_count = 0
@values = values
end
def read_template_file(template_path)
@file_read_count += 1
@values.fetch(template_path)
end
end

View File

@@ -468,11 +468,79 @@ class ContextUnitTest < Minitest::Test
assert_equal 'hi filtered', context.apply_global_filter('hi') assert_equal 'hi filtered', context.apply_global_filter('hi')
end end
def test_static_environments_are_read_with_lower_priority_than_environments
context = Context.build(
static_environments: { 'shadowed' => 'static', 'unshadowed' => 'static' },
environments: { 'shadowed' => 'dynamic' }
)
assert_equal 'dynamic', context['shadowed']
assert_equal 'static', context['unshadowed']
end
def test_apply_global_filter_when_no_global_filter_exist def test_apply_global_filter_when_no_global_filter_exist
context = Context.new context = Context.new
assert_equal 'hi', context.apply_global_filter('hi') assert_equal 'hi', context.apply_global_filter('hi')
end end
def test_new_isolated_subcontext_does_not_inherit_variables
super_context = Context.new
super_context['my_variable'] = 'some value'
subcontext = super_context.new_isolated_subcontext
assert_nil subcontext['my_variable']
end
def test_new_isolated_subcontext_inherits_static_environment
super_context = Context.build(static_environments: { 'my_environment_value' => 'my value' })
subcontext = super_context.new_isolated_subcontext
assert_equal 'my value', subcontext['my_environment_value']
end
def test_new_isolated_subcontext_inherits_resource_limits
resource_limits = ResourceLimits.new({})
super_context = Context.new({}, {}, {}, false, resource_limits)
subcontext = super_context.new_isolated_subcontext
assert_equal resource_limits, subcontext.resource_limits
end
def test_new_isolated_subcontext_inherits_exception_renderer
super_context = Context.new
super_context.exception_renderer = ->(_e) { 'my exception message' }
subcontext = super_context.new_isolated_subcontext
assert_equal 'my exception message', subcontext.handle_error(Liquid::Error.new)
end
def test_new_isolated_subcontext_does_not_inherit_non_static_registers
registers = {
my_register: :my_value
}
super_context = Context.new({}, {}, registers)
subcontext = super_context.new_isolated_subcontext
assert_nil subcontext.registers[:my_register]
end
def test_new_isolated_subcontext_inherits_static_registers
super_context = Context.build(static_registers: { my_register: :my_value })
subcontext = super_context.new_isolated_subcontext
assert_equal :my_value, subcontext.static_registers[:my_register]
end
def test_new_isolated_subcontext_inherits_filters
my_filter = Module.new do
def my_filter(*)
'my filter result'
end
end
super_context = Context.new
super_context.add_filters([my_filter])
subcontext = super_context.new_isolated_subcontext
template = Template.parse('{{ 123 | my_filter }}')
assert_equal 'my filter result', template.render(subcontext)
end
private private
def assert_no_object_allocations def assert_no_object_allocations

View File

@@ -0,0 +1,91 @@
require 'test_helper'
class PartialCacheUnitTest < Minitest::Test
def test_uses_the_file_system_register_if_present
context = Liquid::Context.build(
registers: {
file_system: StubFileSystem.new('my_partial' => 'my partial body'),
}
)
partial = Liquid::PartialCache.load(
'my_partial',
context: context,
parse_context: Liquid::ParseContext.new
)
assert_equal 'my partial body', partial.render
end
def test_reads_from_the_file_system_only_once_per_file
file_system = StubFileSystem.new('my_partial' => 'some partial body')
context = Liquid::Context.build(
registers: { file_system: file_system }
)
2.times do
Liquid::PartialCache.load(
'my_partial',
context: context,
parse_context: Liquid::ParseContext.new
)
end
assert_equal 1, file_system.file_read_count
end
def test_cache_state_is_stored_per_context
parse_context = Liquid::ParseContext.new
shared_file_system = StubFileSystem.new(
'my_partial' => 'my shared value'
)
context_one = Liquid::Context.build(
registers: {
file_system: shared_file_system,
}
)
context_two = Liquid::Context.build(
registers: {
file_system: shared_file_system,
}
)
2.times do
Liquid::PartialCache.load(
'my_partial',
context: context_one,
parse_context: parse_context
)
end
Liquid::PartialCache.load(
'my_partial',
context: context_two,
parse_context: parse_context
)
assert_equal 2, shared_file_system.file_read_count
end
def test_cache_is_not_broken_when_a_different_parse_context_is_used
file_system = StubFileSystem.new('my_partial' => 'some partial body')
context = Liquid::Context.build(
registers: { file_system: file_system }
)
Liquid::PartialCache.load(
'my_partial',
context: context,
parse_context: Liquid::ParseContext.new(my_key: 'value one')
)
Liquid::PartialCache.load(
'my_partial',
context: context,
parse_context: Liquid::ParseContext.new(my_key: 'value two')
)
# Technically what we care about is that the file was parsed twice,
# but measuring file reads is an OK proxy for this.
assert_equal 1, file_system.file_read_count
end
end