puppet 6.2.0-universal-darwin → 6.3.0-universal-darwin

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of puppet might be problematic. Click here for more details.

Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/CODE_OF_CONDUCT.md +70 -0
  3. data/Gemfile.lock +7 -7
  4. data/lib/puppet/application.rb +1 -1
  5. data/lib/puppet/application/lookup.rb +1 -1
  6. data/lib/puppet/confine/boolean.rb +45 -0
  7. data/lib/puppet/confine/false.rb +7 -1
  8. data/lib/puppet/confine/true.rb +7 -1
  9. data/lib/puppet/defaults.rb +0 -18
  10. data/lib/puppet/functions/call.rb +2 -1
  11. data/lib/puppet/functions/group_by.rb +54 -0
  12. data/lib/puppet/functions/index.rb +167 -0
  13. data/lib/puppet/functions/partition.rb +54 -0
  14. data/lib/puppet/pops/evaluator/evaluator_impl.rb +1 -1
  15. data/lib/puppet/pops/issues.rb +4 -0
  16. data/lib/puppet/pops/model/factory.rb +38 -4
  17. data/lib/puppet/pops/parser/egrammar.ra +2 -2
  18. data/lib/puppet/pops/parser/eparser.rb +2 -2
  19. data/lib/puppet/pops/parser/heredoc_support.rb +17 -7
  20. data/lib/puppet/pops/parser/lexer2.rb +6 -1
  21. data/lib/puppet/pops/parser/locator.rb +106 -86
  22. data/lib/puppet/pops/parser/parser_support.rb +11 -2
  23. data/lib/puppet/pops/types/type_mismatch_describer.rb +1 -1
  24. data/lib/puppet/pops/validation/checker4_0.rb +2 -2
  25. data/lib/puppet/provider/group/windows_adsi.rb +4 -1
  26. data/lib/puppet/provider/package/apt.rb +2 -2
  27. data/lib/puppet/provider/package/dnf.rb +2 -2
  28. data/lib/puppet/provider/package/gem.rb +2 -2
  29. data/lib/puppet/provider/package/openbsd.rb +2 -2
  30. data/lib/puppet/provider/package/pacman.rb +1 -2
  31. data/lib/puppet/provider/package/pip.rb +1 -2
  32. data/lib/puppet/provider/package/pip3.rb +1 -2
  33. data/lib/puppet/provider/package/pkg.rb +2 -4
  34. data/lib/puppet/provider/package/portage.rb +1 -2
  35. data/lib/puppet/provider/package/rpm.rb +1 -2
  36. data/lib/puppet/provider/package/sun.rb +2 -2
  37. data/lib/puppet/provider/package/windows.rb +2 -2
  38. data/lib/puppet/provider/package/yum.rb +1 -2
  39. data/lib/puppet/provider/package/zypper.rb +2 -2
  40. data/lib/puppet/provider/service/upstart.rb +16 -6
  41. data/lib/puppet/transaction/resource_harness.rb +1 -0
  42. data/lib/puppet/type/file.rb +6 -1
  43. data/lib/puppet/util/log.rb +7 -2
  44. data/lib/puppet/util/pidlock.rb +14 -1
  45. data/lib/puppet/util/windows/process.rb +73 -5
  46. data/lib/puppet/version.rb +1 -1
  47. data/locales/puppet.pot +92 -92
  48. data/man/man5/puppet.conf.5 +2 -2
  49. data/man/man8/puppet-agent.8 +1 -1
  50. data/man/man8/puppet-apply.8 +1 -1
  51. data/man/man8/puppet-catalog.8 +1 -1
  52. data/man/man8/puppet-config.8 +1 -1
  53. data/man/man8/puppet-describe.8 +1 -1
  54. data/man/man8/puppet-device.8 +1 -1
  55. data/man/man8/puppet-doc.8 +1 -1
  56. data/man/man8/puppet-epp.8 +1 -1
  57. data/man/man8/puppet-facts.8 +1 -1
  58. data/man/man8/puppet-filebucket.8 +1 -1
  59. data/man/man8/puppet-generate.8 +1 -1
  60. data/man/man8/puppet-help.8 +1 -1
  61. data/man/man8/puppet-key.8 +1 -1
  62. data/man/man8/puppet-lookup.8 +2 -2
  63. data/man/man8/puppet-man.8 +1 -1
  64. data/man/man8/puppet-module.8 +1 -1
  65. data/man/man8/puppet-node.8 +1 -1
  66. data/man/man8/puppet-parser.8 +1 -1
  67. data/man/man8/puppet-plugin.8 +1 -1
  68. data/man/man8/puppet-report.8 +1 -1
  69. data/man/man8/puppet-resource.8 +1 -1
  70. data/man/man8/puppet-script.8 +1 -1
  71. data/man/man8/puppet-ssl.8 +1 -1
  72. data/man/man8/puppet-status.8 +1 -1
  73. data/man/man8/puppet.8 +2 -2
  74. data/spec/unit/application_spec.rb +8 -1
  75. data/spec/unit/confine/false_spec.rb +27 -0
  76. data/spec/unit/confine/true_spec.rb +27 -0
  77. data/spec/unit/defaults_spec.rb +0 -14
  78. data/spec/unit/functions/group_by_spec.rb +40 -0
  79. data/spec/unit/functions/index_spec.rb +184 -0
  80. data/spec/unit/functions/partition_spec.rb +40 -0
  81. data/spec/unit/pops/loaders/loaders_spec.rb +5 -0
  82. data/spec/unit/pops/parser/locator_spec.rb +45 -0
  83. data/spec/unit/pops/parser/parse_heredoc_spec.rb +111 -15
  84. data/spec/unit/pops/types/type_mismatch_describer_spec.rb +9 -0
  85. data/spec/unit/provider/group/windows_adsi_spec.rb +7 -1
  86. data/spec/unit/transaction/resource_harness_spec.rb +26 -0
  87. data/spec/unit/util/execution_spec.rb +2 -2
  88. data/spec/unit/util/log_spec.rb +15 -0
  89. data/spec/unit/util/pidlock_spec.rb +21 -1
  90. metadata +13 -2
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Returns two arrays, the first containing the elements of enum for which the block evaluates to true,
4
+ # the second containing the rest.
5
+ Puppet::Functions.create_function(:partition) do
6
+ # @param collection A collection of things to partition.
7
+ # @example Partition array of empty strings, results in e.g. [[''], [b, c]]
8
+ # ['', b, c].partition |$s| { $s.empty }
9
+ # @example Partition array of strings using index, results in e.g. [['', 'ab'], ['b']]
10
+ # ['', b, ab].partition |$i, $s| { $i == 2 or $s.empty }
11
+ # @example Partition hash of strings by key-value pair, results in e.g. [[['b', []]], [['a', [1, 2]]]]
12
+ # { a => [1, 2], b => [] }.partition |$kv| { $kv[1].empty }
13
+ # @example Partition hash of strings by key and value, results in e.g. [[['b', []]], [['a', [1, 2]]]]
14
+ # { a => [1, 2], b => [] }.partition |$k, $v| { $v.empty }
15
+ dispatch :partition_1 do
16
+ required_param 'Collection', :collection
17
+ block_param 'Callable[1,1]', :block
18
+ return_type 'Tuple[Array, Array]'
19
+ end
20
+
21
+ dispatch :partition_2a do
22
+ required_param 'Array', :array
23
+ block_param 'Callable[2,2]', :block
24
+ return_type 'Tuple[Array, Array]'
25
+ end
26
+
27
+ dispatch :partition_2 do
28
+ required_param 'Collection', :collection
29
+ block_param 'Callable[2,2]', :block
30
+ return_type 'Tuple[Array, Array]'
31
+ end
32
+
33
+ def partition_1(collection)
34
+ collection.partition do |item|
35
+ yield(item)
36
+ end.freeze
37
+ end
38
+
39
+ def partition_2a(array)
40
+ partitioned = array.size.times.zip(array).partition do |k, v|
41
+ yield(k, v)
42
+ end
43
+
44
+ partitioned.map do |part|
45
+ part.map { |item| item[1] }
46
+ end.freeze
47
+ end
48
+
49
+ def partition_2(collection)
50
+ collection.partition do |k, v|
51
+ yield(k, v)
52
+ end.freeze
53
+ end
54
+ end
@@ -1028,7 +1028,7 @@ class EvaluatorImpl
1028
1028
 
1029
1029
  # Evaluates Puppet DSL Heredoc
1030
1030
  def eval_HeredocExpression o, scope
1031
- expr = o.text_expr.expr # Always a SublocatedExpression with an expression
1031
+ expr = o.text_expr
1032
1032
  result = evaluate(o.text_expr, scope)
1033
1033
  unless expr.is_a?(Model::LiteralString)
1034
1034
  # When expr is a LiteralString, validation has already validated this
@@ -759,6 +759,10 @@ module Issues
759
759
  _("An escape char for @() may only appear once. Got '%{escapes}'") % { escapes: escapes.join(', ') }
760
760
  end
761
761
 
762
+ HEREDOC_DIRTY_MARGIN = hard_issue :HEREDOC_DIRTY_MARGIN, :heredoc_line do
763
+ _("Heredoc with text in the margin is not allowed (line %{heredoc_line} in this heredoc)") % { heredoc_line: heredoc_line }
764
+ end
765
+
762
766
  ILLEGAL_BOM = hard_issue :ILLEGAL_BOM, :format_name, :bytes do
763
767
  _("Illegal %{format} Byte Order mark at beginning of input: %{bom} - remove these from the puppet source") % { format: format_name, bom: bytes }
764
768
  end
@@ -28,6 +28,7 @@ class Factory
28
28
  BUILD_VISITOR = Visitor.new(self, 'build')
29
29
  INFER_VISITOR = Visitor.new(self, 'infer')
30
30
  INTERPOLATION_VISITOR = Visitor.new(self, 'interpolate')
31
+ MAPOFFSET_VISITOR = Visitor.new(self, 'map_offset')
31
32
 
32
33
  def self.infer(o)
33
34
  if o.instance_of?(Factory)
@@ -89,6 +90,29 @@ class Factory
89
90
  end
90
91
  end
91
92
 
93
+ def map_offset(model, locator)
94
+ MAPOFFSET_VISITOR.visit_this_1(self, model, locator)
95
+ end
96
+
97
+ def map_offset_Object(o, locator)
98
+ o
99
+ end
100
+
101
+ def map_offset_Factory(o, locator)
102
+ map_offset(o.model, locator)
103
+ end
104
+
105
+ def map_offset_Positioned(o, locator)
106
+ # Transpose the local offset, length to global "coordinates"
107
+ global_offset, global_length = locator.to_global(o.offset, o.length)
108
+
109
+ # mutate
110
+ o.instance_variable_set(:'@offset', global_offset)
111
+ o.instance_variable_set(:'@length', global_length)
112
+ # Change locator since the positions were transposed to the global coordinates
113
+ o.instance_variable_set(:'@locator', locator.locator) if locator.is_a? Puppet::Pops::Parser::Locator::SubLocator
114
+ end
115
+
92
116
  # Polymorphic interpolate
93
117
  def interpolate()
94
118
  INTERPOLATION_VISITOR.visit_this_class(self, @model_class, EMPTY_ARRAY)
@@ -433,8 +457,7 @@ class Factory
433
457
  @init_hash[KEY_LOCATOR] = locator
434
458
  @init_hash['leading_line_count'] = locator.leading_line_count
435
459
  @init_hash['leading_line_offset'] = locator.leading_line_offset
436
- # Index is held in sublocator's parent locator - needed to be able to reconstruct
437
- @init_hash['line_offsets'] = locator.locator.line_index
460
+ @init_hash['line_offsets'] = locator.line_index # index of lines in sublocated
438
461
  end
439
462
 
440
463
  def build_SelectorEntry(o, matching, value)
@@ -729,8 +752,6 @@ class Factory
729
752
 
730
753
  def self.STRING(*args); new(ConcatenatedString, args); end
731
754
 
732
- def self.SUBLOCATE(token, expr) new(SubLocatedExpression, token, expr); end
733
-
734
755
  def self.LIST(entries); new(LiteralList, entries); end
735
756
 
736
757
  def self.PARAM(name, expr=nil); new(Parameter, name, expr); end
@@ -765,6 +786,19 @@ class Factory
765
786
  o.instance_of?(Factory) && o.model_class <= QualifiedReference ? self : new(QualifiedReference, o)
766
787
  end
767
788
 
789
+ def self.SUBLOCATE(token, expr_factory)
790
+ # expr is a Factory wrapped LiteralString, or ConcatenatedString
791
+ # The token is SUBLOCATED token which has a SubLocator as the token's locator
792
+ # Use the SubLocator to recalculate the offsets and lengths.
793
+ model = expr_factory.model
794
+ locator = token.locator
795
+ expr_factory.map_offset(model, locator)
796
+ model._pcore_all_contents([]) { |element| expr_factory.map_offset(element, locator) }
797
+
798
+ # Returned the factory wrapping the now offset/length transformed expression(s)
799
+ expr_factory
800
+ end
801
+
768
802
  def self.TEXT(expr)
769
803
  new(TextExpression, infer(expr).interpolate)
770
804
  end
@@ -857,8 +857,8 @@ heredoc
857
857
  : HEREDOC sublocated_text { result = Factory.HEREDOC(val[0][:value], val[1]); loc result, val[0] }
858
858
 
859
859
  sublocated_text
860
- : SUBLOCATE string { result = Factory.SUBLOCATE(val[0], val[1]); loc result, val[0] }
861
- | SUBLOCATE dq_string { result = Factory.SUBLOCATE(val[0], val[1]); loc result, val[0] }
860
+ : SUBLOCATE string { result = Factory.SUBLOCATE(val[0], val[1]); }
861
+ | SUBLOCATE dq_string { result = Factory.SUBLOCATE(val[0], val[1]); }
862
862
 
863
863
  epp_expression
864
864
  : EPP_START epp_parameters_list optional_statements { result = Factory.EPP(val[1], val[2]); loc result, val[0] }
@@ -3204,14 +3204,14 @@ module_eval(<<'.,.,', 'egrammar.ra', 856)
3204
3204
 
3205
3205
  module_eval(<<'.,.,', 'egrammar.ra', 859)
3206
3206
  def _reduce_259(val, _values, result)
3207
- result = Factory.SUBLOCATE(val[0], val[1]); loc result, val[0]
3207
+ result = Factory.SUBLOCATE(val[0], val[1]);
3208
3208
  result
3209
3209
  end
3210
3210
  .,.,
3211
3211
 
3212
3212
  module_eval(<<'.,.,', 'egrammar.ra', 860)
3213
3213
  def _reduce_260(val, _values, result)
3214
- result = Factory.SUBLOCATE(val[0], val[1]); loc result, val[0]
3214
+ result = Factory.SUBLOCATE(val[0], val[1]);
3215
3215
  result
3216
3216
  end
3217
3217
  .,.,
@@ -94,11 +94,12 @@ module HeredocSupport
94
94
 
95
95
 
96
96
  # Process captured lines - remove leading, and trailing newline
97
- str = heredoc_text(lines, leading, has_margin, remove_break)
97
+ # get processed string and index of removed margin/leading size per line
98
+ str, margin_per_line = heredoc_text(lines, leading, has_margin, remove_break)
98
99
 
99
100
  # Use a new lexer instance configured with a sub-locator to enable correct positioning
100
101
  sublexer = self.class.new()
101
- locator = Locator::SubLocator.new(locator, heredoc_line, heredoc_offset, leading.length())
102
+ locator = Locator::SubLocator.new(locator, str, heredoc_line, heredoc_offset, has_margin, margin_per_line)
102
103
 
103
104
  # Emit a token that provides the grammar with location information about the lines on which the heredoc
104
105
  # content is based.
@@ -120,23 +121,32 @@ module HeredocSupport
120
121
  raise eof_error
121
122
  end
122
123
 
123
- # Produces the heredoc text string given the individual (unprocessed) lines as an array.
124
+ # Produces the heredoc text string given the individual (unprocessed) lines as an array and array with margin sizes per line
124
125
  # @param lines [Array<String>] unprocessed lines of text in the heredoc w/o terminating line
125
126
  # @param leading [String] the leading text up (up to pipe or other terminating char)
126
127
  # @param has_margin [Boolean] if the left margin should be adjusted as indicated by `leading`
127
128
  # @param remove_break [Boolean] if the line break (\r?\n) at the end of the last line should be removed or not
129
+ # @return [Array] - a tuple with resulting string, and an array with margin size per line
128
130
  #
129
131
  def heredoc_text(lines, leading, has_margin, remove_break)
130
- if has_margin
132
+ if has_margin && leading.length > 0
131
133
  leading_pattern = /^#{Regexp.escape(leading)}/
132
- lines = lines.collect {|s| s.gsub(leading_pattern, '') }
134
+ # TODO: This implementation is not according to the specification, but is kept to be bug compatible.
135
+ # The specification says that leading space up to the margin marker should be removed, but this implementation
136
+ # simply leaves lines that have text in the margin untouched.
137
+ #
138
+ processed_lines = lines.collect {|s| s.gsub(leading_pattern, '') }
139
+ margin_per_line = processed_lines.length.times.map {|x| lines[x].length - processed_lines[x].length }
140
+ lines = processed_lines
141
+ else
142
+ # Array with a 0 per line
143
+ margin_per_line = Array.new(lines.length, 0)
133
144
  end
134
145
  result = lines.join('')
135
146
  result.gsub!(/\r?\n\z/m, '') if remove_break
136
- result
147
+ [result, margin_per_line]
137
148
  end
138
149
 
139
-
140
150
  end
141
151
  end
142
152
  end
@@ -189,7 +189,12 @@ class Lexer2
189
189
  ',' => lambda { emit(TOKEN_COMMA, @scanner.pos) },
190
190
  '[' => lambda do
191
191
  before = @scanner.pos
192
- if (before == 0 || locator.string[locator.char_offset(before)-1,1] =~ /[[:blank:]\r\n]+/)
192
+ # Must check the preceding character to see if it is whitespace.
193
+ # The fastest thing to do is to simply byteslice to get the string ending at the offset before
194
+ # and then check what the last character is. (This is the same as what an locator.char_offset needs
195
+ # to compute, but with less overhead of trying to find out the global offset from a local offset in the
196
+ # case when this is sublocated in a heredoc).
197
+ if before == 0 || @scanner.string.byteslice(0, before)[-1] =~ /[[:blank:]\r\n]+/
193
198
  emit(TOKEN_LISTSTART, before)
194
199
  else
195
200
  emit(TOKEN_LBRACK, before)
@@ -71,6 +71,16 @@ class Locator
71
71
  def line_index()
72
72
  end
73
73
 
74
+ # Common byte based impl that works for all rubies (stringscanner is byte based
75
+ def self.compute_line_index(string)
76
+ scanner = StringScanner.new(string)
77
+ result = [0] # first line starts at 0
78
+ while scanner.scan_until(/\n/)
79
+ result << scanner.pos
80
+ end
81
+ result.freeze
82
+ end
83
+
74
84
  # Produces an URI with path?line=n&pos=n. If origin is unknown the URI is string:?line=n&pos=n
75
85
  def to_uri(ast)
76
86
  f = file
@@ -83,81 +93,8 @@ class Locator
83
93
  URI("#{f}?line=#{line_for_offset(offset).to_s}&pos=#{pos_on_line(offset).to_s}")
84
94
  end
85
95
 
86
- # A Sublocator locates a concrete locator (subspace) in a virtual space.
87
- # The `leading_line_count` is the (virtual) number of lines preceding the first line in the concrete locator.
88
- # The `leading_offset` is the (virtual) byte offset of the first byte in the concrete locator.
89
- # The `leading_line_offset` is the (virtual) offset / margin in characters for each line.
90
- #
91
- # This illustrates characters in the sublocator (`.`) inside the subspace (`X`):
92
- #
93
- # 1:XXXXXXXX
94
- # 2:XXXX.... .. ... ..
95
- # 3:XXXX. . .... ..
96
- # 4:XXXX............
97
- #
98
- # This sublocator would be configured with leading_line_count = 1,
99
- # leading_offset=8, and leading_line_offset=4
100
- #
101
- # Note that leading_offset must be the same for all lines and measured in characters.
102
- #
103
- class SubLocator < Locator
104
- attr_reader :locator
105
- attr_reader :leading_line_count
106
- attr_reader :leading_offset
107
- attr_reader :leading_line_offset
108
-
109
- def self.sub_locator(string, file, leading_line_count, leading_offset, leading_line_offset)
110
- self.new(Locator.locator(string, file),
111
- leading_line_count,
112
- leading_offset,
113
- leading_line_offset)
114
- end
115
-
116
- def initialize(locator, leading_line_count, leading_offset, leading_line_offset)
117
- @locator = locator
118
- @leading_line_count = leading_line_count
119
- @leading_offset = leading_offset
120
- @leading_line_offset = leading_line_offset
121
- end
122
-
123
- def file
124
- @locator.file
125
- end
126
-
127
- def string
128
- @locator.string
129
- end
130
-
131
- # Given offset is offset in the subspace
132
- def line_for_offset(offset)
133
- @locator.line_for_offset(offset) + @leading_line_count
134
- end
135
-
136
- # Given offset is offset in the subspace
137
- def offset_on_line(offset)
138
- @locator.offset_on_line(offset) + @leading_line_offset
139
- end
140
-
141
- # Given offset is offset in the subspace
142
- def char_offset(offset)
143
- effective_line = @locator.line_for_offset(offset)
144
- locator.char_offset(offset) + (effective_line * @leading_line_offset) + @leading_offset
145
- end
146
-
147
- # Given offsets are offsets in the subspace
148
- def char_length(offset, end_offset)
149
- effective_line = @locator.line_for_offset(end_offset) - @locator.line_for_offset(offset)
150
- locator.char_length(offset, end_offset) + (effective_line * @leading_line_offset)
151
- end
152
-
153
- def pos_on_line(offset)
154
- offset_on_line(offset) +1
155
- end
156
- end
157
-
158
96
  class AbstractLocator < Locator
159
97
  attr_accessor :line_index
160
- attr_accessor :string
161
98
  attr_reader :string
162
99
  attr_reader :file
163
100
 
@@ -169,8 +106,7 @@ class Locator
169
106
  @file = file.freeze
170
107
  @prev_offset = nil
171
108
  @prev_line = nil
172
- @line_index = line_index
173
- compute_line_index if line_index.nil?
109
+ @line_index = line_index.nil? ? Locator.compute_line_index(@string) : line_index
174
110
  end
175
111
 
176
112
  # Returns the position on line (first position on a line is 1)
@@ -235,16 +171,6 @@ class Locator
235
171
  self.class == o.class && string == o.string && file == o.file && line_index == o.line_index
236
172
  end
237
173
 
238
- # Common impl for 18 and 19 since scanner is byte based
239
- def compute_line_index
240
- scanner = StringScanner.new(string)
241
- result = [0] # first line starts at 0
242
- while scanner.scan_until(/\n/)
243
- result << scanner.pos
244
- end
245
- self.line_index = result.freeze
246
- end
247
-
248
174
  # Returns the line number (first line is 1) for the given offset
249
175
  def line_for_offset(offset)
250
176
  if @prev_offset == offset
@@ -264,6 +190,100 @@ class Locator
264
190
  end
265
191
  end
266
192
 
193
+ # A Sublocator locates a concrete locator (subspace) in a virtual space.
194
+ # The `leading_line_count` is the (virtual) number of lines preceding the first line in the concrete locator.
195
+ # The `leading_offset` is the (virtual) byte offset of the first byte in the concrete locator.
196
+ # The `leading_line_offset` is the (virtual) offset / margin in characters for each line.
197
+ #
198
+ # This illustrates characters in the sublocator (`.`) inside the subspace (`X`):
199
+ #
200
+ # 1:XXXXXXXX
201
+ # 2:XXXX.... .. ... ..
202
+ # 3:XXXX. . .... ..
203
+ # 4:XXXX............
204
+ #
205
+ # This sublocator would be configured with leading_line_count = 1,
206
+ # leading_offset=8, and leading_line_offset=4
207
+ #
208
+ # Note that leading_offset must be the same for all lines and measured in characters.
209
+ #
210
+ # A SubLocator is only used during parsing as the parser will translate the local offsets/lengths to
211
+ # the parent locator when a sublocated expression is reduced. Do not call the methods
212
+ # `char_offset` or `char_length` as those methods will raise an error.
213
+ #
214
+ class SubLocator < AbstractLocator
215
+ attr_reader :locator
216
+ attr_reader :leading_line_count
217
+ attr_reader :leading_offset
218
+ attr_reader :has_margin
219
+ attr_reader :margin_per_line
220
+
221
+ def initialize(locator, str, leading_line_count, leading_offset, has_margin, margin_per_line)
222
+ super(str, locator.file)
223
+ @locator = locator
224
+ @leading_line_count = leading_line_count
225
+ @leading_offset = leading_offset
226
+ @has_margin = has_margin
227
+ @margin_per_line = margin_per_line
228
+
229
+ # Since lines can have different margin - accumulated margin per line must be computed
230
+ # and since this accumulated margin adjustment is needed more than once; both for start offset,
231
+ # and for end offset (to compute global length) it is computed up front here.
232
+ # The accumulated_offset holds the sum of all removed margins before a position on line n (line index is 1-n,
233
+ # and (unused) position 0 is always 0).
234
+ # The last entry is duplicated since there will be the line "after last line" that would otherwise require
235
+ # conditional logic.
236
+ #
237
+ @accumulated_margin = margin_per_line.reduce([0]) {|memo, val| memo << memo[-1] + val; memo }
238
+ @accumulated_margin << @accumulated_margin[-1]
239
+ end
240
+
241
+ def file
242
+ @locator.file
243
+ end
244
+
245
+ # Returns array with transposed (local) offset and (local) length. The transposed values
246
+ # take the margin into account such that it is added to the content to the right
247
+ #
248
+ # Using X to denote margin and where end of line is explicitly shown as \n:
249
+ # ```
250
+ # XXXXabc\n
251
+ # XXXXdef\n
252
+ # ```
253
+ # A local offset of 0 is translated to the start of the first heredoc line, and a length of 1 is adjusted to
254
+ # 5 - i.e to cover "XXXXa". A local offset of 1, with length 1 would cover "b".
255
+ # A local offset of 4 and length 1 would cover "XXXXd"
256
+ #
257
+ # It is possible that lines have different margin and that is taken into account.
258
+ #
259
+ def to_global(offset, length)
260
+ # simple case, no margin
261
+ return [offset + @leading_offset, length] unless @has_margin
262
+
263
+ # compute local start and end line
264
+ start_line = line_for_offset(offset)
265
+ end_line = line_for_offset(offset+length)
266
+
267
+ # complex case when there is a margin
268
+ transposed_offset = offset == 0 ? @leading_offset : offset + @leading_offset + @accumulated_margin[start_line]
269
+ transposed_length = length +
270
+ @accumulated_margin[end_line] - @accumulated_margin[start_line] + # the margins between start and end (0 is line 1)
271
+ (offset_on_line(offset) == 0 ? margin_per_line[start_line - 1] : 0) # include start's margin in position 0
272
+ [transposed_offset, transposed_length]
273
+ end
274
+
275
+ # Do not call this method
276
+ def char_offset(offset)
277
+ raise "Should not be called"
278
+ end
279
+
280
+ # Do not call this method
281
+ def char_length(offset, end_offset)
282
+ raise "Should not be called"
283
+ end
284
+
285
+ end
286
+
267
287
  class LocatorForChars < AbstractLocator
268
288
 
269
289
  def offset_on_line(offset)
@@ -311,7 +331,7 @@ class Locator
311
331
  # Ruby 19 is multibyte but has no character position methods, must use byteslice
312
332
  def offset_on_line(offset)
313
333
  line_offset = line_index[ line_for_offset(offset)-1 ]
314
- string.byteslice(line_offset, offset-line_offset).length
334
+ @string.byteslice(line_offset, offset-line_offset).length
315
335
  end
316
336
 
317
337
  # Returns the character offset for a given byte offset