bade 0.2.5 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -83,9 +83,10 @@ class String
83
83
  count = 0
84
84
 
85
85
  each_char do |char|
86
- if char == SPACE_CHAR
86
+ case char
87
+ when SPACE_CHAR
87
88
  count += 1
88
- elsif char == TAB_CHAR
89
+ when TAB_CHAR
89
90
  count += tabsize
90
91
  else
91
92
  break
@@ -94,13 +95,4 @@ class String
94
95
 
95
96
  count
96
97
  end
97
-
98
- # source: http://apidock.com/rails/String/strip_heredoc
99
- # @return [String]
100
- #
101
- def strip_heredoc
102
- min_val = scan(/^[ \t]*(?=\S)/).min
103
- indent = (min_val && min_val.size) || 0
104
- gsub(/^[ \t]{#{indent}}/, '')
105
- end
106
98
  end
@@ -4,8 +4,6 @@ require_relative '../ruby2_keywords'
4
4
 
5
5
  module Bade
6
6
  module Runtime
7
- class RuntimeError < ::StandardError; end
8
-
9
7
  class Block
10
8
  class MissingBlockDefinitionError < RuntimeError
11
9
  # @return [String]
@@ -16,8 +14,8 @@ module Bade
16
14
  #
17
15
  attr_accessor :context
18
16
 
19
- def initialize(name, context, msg = nil)
20
- super()
17
+ def initialize(name, context, msg, template_backtrace)
18
+ super(msg, template_backtrace)
21
19
 
22
20
  self.name = name
23
21
  self.context = context
@@ -38,34 +36,51 @@ module Bade
38
36
  #
39
37
  attr_reader :name
40
38
 
39
+ # @return [RenderBinding::Location, nil]
40
+ #
41
+ attr_reader :location
42
+
41
43
  # @return [RenderBinding]
42
44
  #
43
45
  attr_reader :render_binding
44
46
 
45
47
  # @param [String] name name of the block
48
+ # @param [RenderBinding::Location, nil] location
46
49
  # @param [RenderBinding] render_binding reference to current binding instance
47
50
  # @param [Proc] block reference to lambda
48
51
  #
49
- def initialize(name, render_binding, &block)
52
+ def initialize(name, location, render_binding, &block)
50
53
  @name = name
54
+ @location = location
51
55
  @render_binding = render_binding
52
56
  @block = block
53
57
  end
54
58
 
55
59
  # --- Calling methods
56
60
 
61
+ # Calls the block and adds rendered content into current buffer stack.
62
+ #
63
+ # @return [Void]
57
64
  ruby2_keywords def call(*args)
58
65
  call!(*args) unless @block.nil?
59
66
  end
60
67
 
68
+ # Calls the block and adds rendered content into current buffer stack.
69
+ #
70
+ # @return [Void]
61
71
  ruby2_keywords def call!(*args)
62
- raise MissingBlockDefinitionError.new(name, :call) if @block.nil?
72
+ raise MissingBlockDefinitionError.new(name, :call, nil, render_binding.__location_stack) if @block.nil?
63
73
 
64
- render_binding.__buff.concat(@block.call(*args))
74
+ __call(*args)
65
75
  end
66
76
 
67
77
  # --- Rendering methods
68
78
 
79
+ # Calls the block and returns rendered content in string.
80
+ #
81
+ # Returns empty string when there is no block.
82
+ #
83
+ # @return [String]
69
84
  def render(*args)
70
85
  if @block.nil?
71
86
  ''
@@ -74,10 +89,33 @@ module Bade
74
89
  end
75
90
  end
76
91
 
92
+ # Calls the block and returns rendered content in string.
93
+ #
94
+ # Throws error when there is no block.
95
+ #
96
+ # @return [String]
77
97
  def render!(*args)
78
- raise MissingBlockDefinitionError.new(name, :render) if @block.nil?
98
+ raise MissingBlockDefinitionError.new(name, :render, nil, render_binding.__location_stack) if @block.nil?
99
+
100
+ loc = location.dup
101
+ render_binding.__buffs_push(loc)
102
+
103
+ @block.call(*args)
104
+
105
+ render_binding.__buffs_pop&.join || ''
106
+ end
107
+
108
+ # Calls the block and adds rendered content into current buffer stack.
109
+ #
110
+ # @return [Void]
111
+ ruby2_keywords def __call(*args)
112
+ loc = location.dup
113
+ render_binding.__buffs_push(loc)
114
+
115
+ @block.call(*args)
79
116
 
80
- @block.call(*args).join
117
+ res = render_binding.__buffs_pop
118
+ render_binding.__buff&.concat(res) if !res.nil? && !res.empty?
81
119
  end
82
120
  end
83
121
  end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'utils/where'
4
+
5
+ module Bade
6
+ module Runtime
7
+ # Tracks created global variables and constants in block.
8
+ class GlobalsTracker
9
+ # @return [Array<Symbol>]
10
+ attr_accessor :caught_variables
11
+
12
+ # @return [Array<[Object, :Symbol]>]
13
+ attr_accessor :caught_constants
14
+
15
+ # @return [Array<String>, nil]
16
+ attr_accessor :constants_location_prefixes
17
+
18
+ # @param [Array<String>, nil] constants_location_prefixes If given, only constants whose location starts with one
19
+ # of the prefixes will be removed. If nil, all constants
20
+ # will be removed.
21
+ def initialize(constants_location_prefixes: nil)
22
+ @caught_variables = []
23
+ @caught_constants = []
24
+ @constants_location_prefixes = constants_location_prefixes
25
+ end
26
+
27
+ # @yieldreturn [T]
28
+ # @return [T]
29
+ def catch
30
+ before_variables = global_variables
31
+ before_global_constants = Object.constants
32
+ before_binding_constants = Bade::Runtime::RenderBinding.constants(false)
33
+
34
+ res = nil
35
+ begin
36
+ res = yield
37
+ ensure
38
+ @caught_variables += global_variables - before_variables
39
+
40
+ @caught_constants += (Object.constants - before_global_constants)
41
+ .map { |name| [Object, name] }
42
+ @caught_constants += (Bade::Runtime::RenderBinding.constants(false) - before_binding_constants)
43
+ .map { |name| [Bade::Runtime::RenderBinding, name] }
44
+ end
45
+
46
+ res
47
+ end
48
+
49
+ def clear_all
50
+ clear_global_variables
51
+ clear_constants
52
+ end
53
+
54
+ def clear_constants
55
+ _filtered_constants.each do |(obj, name)|
56
+ obj.send(:remove_const, name)
57
+ end
58
+ @caught_constants = []
59
+ end
60
+
61
+ def clear_global_variables
62
+ @caught_variables.each do |name|
63
+ eval("#{name} = nil", binding, __FILE__, __LINE__)
64
+ end
65
+ end
66
+
67
+ # Filteres caught constants by location prefixes and returns ones that should be removed.
68
+ #
69
+ # @return [Array<[Object, :Symbol]>]
70
+ def _filtered_constants
71
+ @caught_constants.select do |(obj, name)|
72
+ next unless obj.const_defined?(name)
73
+ next true if constants_location_prefixes.nil?
74
+
75
+ konst = obj.const_get(name)
76
+ begin
77
+ location = Bade.where_is(konst)
78
+ rescue ::ArgumentError
79
+ next
80
+ end
81
+
82
+ path = location.first
83
+ constants_location_prefixes&.any? { |prefix| path.start_with?(prefix) }
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -9,8 +9,8 @@ module Bade
9
9
  class Mixin < Block
10
10
  ruby2_keywords def call!(blocks, *args)
11
11
  begin
12
- block.call(blocks, *args)
13
- rescue ArgumentError => e
12
+ __call(blocks, *args)
13
+ rescue ::ArgumentError => e
14
14
  case e.message
15
15
  when /\Awrong number of arguments \(given ([0-9]+), expected ([0-9]+)\)\Z/,
16
16
  /\Awrong number of arguments \(([0-9]+) for ([0-9]+)\)\Z/
@@ -19,12 +19,19 @@ module Bade
19
19
  # minus one, because first argument is always hash of blocks
20
20
  given = $1.to_i - 1
21
21
  expected = $2.to_i - 1
22
- raise ArgumentError, "wrong number of arguments (given #{given}, expected #{expected}) for mixin `#{name}`"
22
+ msg = "wrong number of arguments (given #{given}, expected #{expected}) for mixin `#{name}`"
23
+ raise Bade::Runtime::ArgumentError.new(msg, render_binding.__location_stack)
23
24
 
24
25
  when /\Aunknown keyword: (.*)\Z/
25
26
  # handle unknown key-value parameter
26
27
  key_name = $1
27
- raise ArgumentError, "unknown key-value argument `#{key_name}` for mixin `#{name}`"
28
+ msg = "unknown key-value argument `#{key_name}` for mixin `#{name}`"
29
+ raise Bade::Runtime::ArgumentError.new(msg, render_binding.__location_stack)
30
+
31
+ when /\Amissing keyword: :?(.*)\Z/
32
+ key_name = $1
33
+ msg = "missing value for required key-value argument `#{key_name}` for mixin `#{name}`"
34
+ raise Bade::Runtime::ArgumentError.new(msg, render_binding.__location_stack)
28
35
 
29
36
  else
30
37
  raise
@@ -36,10 +43,18 @@ module Bade
36
43
  when :render
37
44
  "Mixin `#{name}` requires block to get rendered content of block `#{e.name}`"
38
45
  else
39
- raise ::ArgumentError, "Unknown context #{e.context} of error #{e}!"
46
+ raise Bade::Runtime::ArgumentError.new("Unknown context #{e.context} of error #{e}!",
47
+ render_binding.__location_stack)
40
48
  end
41
49
 
42
- raise Block::MissingBlockDefinitionError.new(e.name, e.context, msg)
50
+ raise Block::MissingBlockDefinitionError.new(e.name, e.context, msg, render_binding.__location_stack)
51
+
52
+ rescue RuntimeError
53
+ raise
54
+
55
+ rescue Exception => e
56
+ msg = "Exception raised during execution of mixin `#{name}`: #{e}"
57
+ raise Bade::Runtime::RuntimeError.wrap_existing_error(msg, e, render_binding.__location_stack)
43
58
  end
44
59
  end
45
60
  end
@@ -5,12 +5,15 @@ require_relative 'block'
5
5
  module Bade
6
6
  module Runtime
7
7
  class RenderBinding
8
- class KeyError < ::StandardError; end
8
+ Location = Bade::Runtime::Location
9
9
 
10
10
  # @return [Array<Array<String>>]
11
11
  #
12
12
  attr_accessor :__buffs_stack
13
13
 
14
+ # @return [Array<Location>]
15
+ attr_accessor :__location_stack
16
+
14
17
  # @return [Hash<String, Mixin>]
15
18
  #
16
19
  attr_accessor :__mixins
@@ -34,13 +37,14 @@ module Bade
34
37
  end
35
38
  end
36
39
 
37
- # Resets this binding to default state, this method should be envoked after running the template lambda
40
+ # Resets this binding to default state, this method should be evoked after running the template lambda
38
41
  #
39
42
  # @return [nil]
40
43
  #
41
44
  def __reset
42
- @__buffs_stack = [[]]
43
- @__mixins = Hash.new { |_hash, key| raise "Undefined mixin '#{key}'" }
45
+ @__buffs_stack = []
46
+ @__location_stack = []
47
+ @__mixins = Hash.new { |_hash, key| raise Bade::Runtime::KeyError.new("Undefined mixin '#{key}'", __location_stack) }
44
48
  end
45
49
 
46
50
  # @return [Binding]
@@ -51,26 +55,56 @@ module Bade
51
55
 
52
56
  # Shortcut for creating blocks
53
57
  #
54
- def __create_block(name, &block)
55
- Bade::Runtime::Block.new(name, self, &block)
58
+ def __create_block(name, location = nil, &block)
59
+ Bade::Runtime::Block.new(name, location, self, &block)
56
60
  end
57
61
 
58
- def __create_mixin(name, &block)
59
- Bade::Runtime::Mixin.new(name, self, &block)
62
+ def __create_mixin(name, location, &block)
63
+ Bade::Runtime::Mixin.new(name, location, self, &block)
60
64
  end
61
65
 
62
- # --- Methods for dealing with pushing and poping buffers in stack
66
+ # --- Methods for dealing with pushing and popping buffers in stack
63
67
 
64
68
  def __buff
65
- __buffs_stack.last
69
+ __buffs_stack.first
66
70
  end
67
71
 
68
- def __buffs_push
69
- __buffs_stack.push([])
72
+ # @param [RenderBinding::Location, nil] location
73
+ def __buffs_push(location)
74
+ __buffs_stack.unshift([])
75
+ __location_stack.unshift(location) unless location.nil?
70
76
  end
71
77
 
78
+ # @return [Array<String>, nil]
72
79
  def __buffs_pop
73
- __buffs_stack.pop
80
+ __location_stack.shift
81
+ __buffs_stack.shift
82
+ end
83
+
84
+ # --- Other internal methods
85
+
86
+ # @param [String] filename
87
+ def __load(filename)
88
+ # FakeFS does not fake `load` method
89
+ if Object.const_defined?(:FakeFS) && Object.const_get(:FakeFS).activated?
90
+ # rubocop:disable Security/Eval
91
+ eval(File.read(filename), __get_binding, filename)
92
+ # rubocop:enable Security/Eval
93
+ else
94
+ load(filename)
95
+ end
96
+ end
97
+
98
+ # @param [String] filename
99
+ def require_relative(filename)
100
+ # FakeFS does not fake `require_relative` method
101
+ if Object.const_defined?(:FakeFS) && Object.const_get(:FakeFS).activated?
102
+ # rubocop:disable Security/Eval
103
+ eval(File.read(filename), __get_binding, filename)
104
+ # rubocop:enable Security/Eval
105
+ else
106
+ Kernel.require_relative(filename)
107
+ end
74
108
  end
75
109
 
76
110
  # Escape input text with html escapes
@@ -94,6 +128,15 @@ module Bade
94
128
 
95
129
  %( #{name}="#{values.join(' ')}")
96
130
  end
131
+
132
+ def __update_lineno(number)
133
+ __location_stack.first&.lineno = number
134
+ end
135
+
136
+ # @return [Location, nil]
137
+ def __current_location
138
+ __location_stack.first
139
+ end
97
140
  end
98
141
  end
99
142
  end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Inspired by https://gist.github.com/wtaysom/1236979
4
+
5
+ module Bade
6
+ module Where
7
+ class << self
8
+ def is_proc(proc)
9
+ source_location(proc)
10
+ end
11
+
12
+ def is_method(klass, method_name)
13
+ source_location(klass.method(method_name))
14
+ end
15
+
16
+ def is_instance_method(klass, method_name)
17
+ source_location(klass.instance_method(method_name))
18
+ end
19
+
20
+ def are_methods(klass, method_name)
21
+ are_via_extractor(:method, klass, method_name)
22
+ end
23
+
24
+ def are_instance_methods(klass, method_name)
25
+ are_via_extractor(:method, klass, method_name)
26
+ end
27
+
28
+ def is_class(klass)
29
+ defined_methods(klass)
30
+ .group_by { |sl| sl[0] }
31
+ .map do |file, sls|
32
+ lines = sls.map { |sl| sl[1] }
33
+ count = lines.size
34
+ line = lines.min
35
+
36
+ {
37
+ file: file,
38
+ count: count,
39
+ line: line
40
+ }
41
+ end
42
+ .sort_by { |fc| fc[:count] }
43
+ .map { |fc| [fc[:file], fc[:line]] }
44
+ end
45
+
46
+ # Raises ArgumentError if klass does not have any Ruby methods defined in it.
47
+ def is_class_primarily(klass)
48
+ source_locations = is_class(klass)
49
+ if source_locations.empty?
50
+ methods = defined_methods(klass)
51
+ msg = if methods.empty?
52
+ "#{klass} has no methods"
53
+ else
54
+ "#{klass} only has built-in methods (#{methods.size} in total)"
55
+ end
56
+
57
+ raise ::ArgumentError, msg
58
+ end
59
+ source_locations[0]
60
+ end
61
+
62
+ private
63
+
64
+ def source_location(method)
65
+ method.source_location || (
66
+ method.to_s =~ /: (.*)>/
67
+ Regexp.last_match(1)
68
+ )
69
+ end
70
+
71
+ def are_via_extractor(extractor, klass, method_name)
72
+ klass.ancestors
73
+ .map do |ancestor|
74
+ method = ancestor.send(extractor, method_name)
75
+ source_location(method) if method.owner == ancestor
76
+ end
77
+ .compact
78
+ end
79
+
80
+ def defined_methods(klass)
81
+ methods = klass.methods(false).map { |m| klass.method(m) } +
82
+ klass.instance_methods(false).map { |m| klass.instance_method(m) }
83
+ methods
84
+ .map(&:source_location)
85
+ .compact
86
+ end
87
+ end
88
+ end
89
+
90
+ def self.where_is(klass, method = nil)
91
+ if method
92
+ begin
93
+ Where.is_instance_method(klass, method)
94
+ rescue NameError
95
+ Where.is_method(klass, method)
96
+ end
97
+ else
98
+ Where.is_class_primarily(klass)
99
+ end
100
+ end
101
+ end
data/lib/bade/runtime.rb CHANGED
@@ -1,9 +1,116 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Epuber
3
+ module Bade
4
4
  module Runtime
5
+ Location = Struct.new(:path, :lineno, :label, keyword_init: true) do
6
+ def template?
7
+ path == TEMPLATE_FILE_NAME || path&.include?('.bade')
8
+ end
9
+
10
+ def to_s
11
+ "#{path || TEMPLATE_FILE_NAME}:#{lineno}:in `#{label}'"
12
+ end
13
+ end
14
+
15
+ class RuntimeError < ::StandardError
16
+ # @return [Array<Location>]
17
+ #
18
+ attr_reader :template_backtrace
19
+
20
+ # @return [Boolean]
21
+ #
22
+ attr_reader :print_locations_warning
23
+
24
+ # @param [String] msg
25
+ # @param [Array<Location>] template_backtrace
26
+ # @param [Exception, nil] original
27
+ def initialize(msg, template_backtrace = [], original: nil, print_locations_warning: false)
28
+ super(msg)
29
+ @template_backtrace = template_backtrace
30
+ @original = original
31
+ @print_locations_warning = print_locations_warning
32
+ end
33
+
34
+ def message
35
+ if @template_backtrace.empty?
36
+ super
37
+ else
38
+ warning = if print_locations_warning
39
+ <<~TEXT
40
+
41
+ !!! WARNING !!!, filenames and line numbers of functions can be misleading due to using Ruby
42
+ functions in different Bade file. Trust only functions names. Mixins are fine.
43
+
44
+ This will be fixed in https://github.com/epuber-io/bade/issues/32
45
+ TEXT
46
+ else
47
+ ''
48
+ end
49
+
50
+ <<~MSG.rstrip
51
+ #{super}
52
+ template backtrace:
53
+ #{__formatted_backtrace.join("\n")}
54
+ #{warning}
55
+ MSG
56
+ end
57
+ end
58
+
59
+ def cause
60
+ @original
61
+ end
62
+
63
+ # @return [Array<String>]
64
+ def __formatted_backtrace
65
+ bt = @template_backtrace
66
+
67
+ # delete first location if is same as second (can happen when arguments are incorrect)
68
+ last = bt.first
69
+ bt.delete_at(0) if last && bt.length > 1 && last == bt[1]
70
+
71
+ bt.map { |loc| " #{loc}" }
72
+ end
73
+
74
+ # @param [Array<Thread::Backtrace::Location>, nil] locations
75
+ def self.process_locations(locations)
76
+ return [] if locations.nil?
77
+
78
+ # map to Bade's Location
79
+ new_locations = locations.map { |loc| Location.new(path: loc.path, lineno: loc.lineno, label: loc.label) }
80
+
81
+ # find location to use or drop
82
+ index = new_locations.rindex(&:template?)
83
+ return [] if index.nil?
84
+
85
+ # get only locations inside template
86
+ new_locations = new_locations[0...index] || []
87
+
88
+ # filter out not interested locations
89
+ new_locations
90
+ .reject { |loc| loc.path.start_with?(__dir__) }
91
+ .reject { |loc| loc.template? && loc.label.include?('lambda_instance') }
92
+ end
93
+
94
+ # @param [String] msg
95
+ # @param [Exception] error
96
+ # @param [Array<Location>] template_backtrace
97
+ # @return [RuntimeError]
98
+ def self.wrap_existing_error(msg, error, template_backtrace)
99
+ ruby_locs = Bade::Runtime::RuntimeError.process_locations(error.backtrace_locations)
100
+ locs = ruby_locs + template_backtrace
101
+ Bade::Runtime::RuntimeError.new(msg, locs, original: error, print_locations_warning: !ruby_locs.empty?)
102
+ end
103
+ end
104
+
105
+ class KeyError < RuntimeError; end
106
+
107
+ class ArgumentError < RuntimeError; end
108
+
109
+ TEMPLATE_FILE_NAME = '(__template__)'.freeze
110
+
5
111
  require_relative 'runtime/block'
6
112
  require_relative 'runtime/mixin'
7
113
  require_relative 'runtime/render_binding'
114
+ require_relative 'runtime/globals_tracker'
8
115
  end
9
116
  end
data/lib/bade/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bade
4
- VERSION = '0.2.5'.freeze
4
+ VERSION = '0.3.2'.freeze
5
5
  end
data/lib/bade.rb CHANGED
@@ -7,4 +7,5 @@ module Bade
7
7
  require_relative 'bade/generator'
8
8
  require_relative 'bade/renderer'
9
9
  require_relative 'bade/optimizer'
10
+ require_relative 'bade/precompiled'
10
11
  end