csv_plus_plus 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -2
  3. data/{CHANGELOG.md → docs/CHANGELOG.md} +9 -0
  4. data/lib/csv_plus_plus/benchmarked_compiler.rb +70 -20
  5. data/lib/csv_plus_plus/cell.rb +46 -24
  6. data/lib/csv_plus_plus/cli.rb +23 -13
  7. data/lib/csv_plus_plus/cli_flag.rb +1 -2
  8. data/lib/csv_plus_plus/color.rb +32 -7
  9. data/lib/csv_plus_plus/compiler.rb +82 -60
  10. data/lib/csv_plus_plus/entities/ast_builder.rb +27 -43
  11. data/lib/csv_plus_plus/entities/boolean.rb +18 -9
  12. data/lib/csv_plus_plus/entities/builtins.rb +23 -9
  13. data/lib/csv_plus_plus/entities/cell_reference.rb +200 -29
  14. data/lib/csv_plus_plus/entities/date.rb +38 -5
  15. data/lib/csv_plus_plus/entities/entity.rb +27 -61
  16. data/lib/csv_plus_plus/entities/entity_with_arguments.rb +57 -0
  17. data/lib/csv_plus_plus/entities/function.rb +23 -11
  18. data/lib/csv_plus_plus/entities/function_call.rb +24 -9
  19. data/lib/csv_plus_plus/entities/number.rb +24 -10
  20. data/lib/csv_plus_plus/entities/runtime_value.rb +22 -5
  21. data/lib/csv_plus_plus/entities/string.rb +19 -6
  22. data/lib/csv_plus_plus/entities/variable.rb +16 -4
  23. data/lib/csv_plus_plus/entities.rb +20 -13
  24. data/lib/csv_plus_plus/error/error.rb +11 -1
  25. data/lib/csv_plus_plus/error/formula_syntax_error.rb +1 -0
  26. data/lib/csv_plus_plus/error/modifier_syntax_error.rb +53 -5
  27. data/lib/csv_plus_plus/error/modifier_validation_error.rb +34 -14
  28. data/lib/csv_plus_plus/error/syntax_error.rb +22 -9
  29. data/lib/csv_plus_plus/error/writer_error.rb +8 -0
  30. data/lib/csv_plus_plus/error.rb +1 -0
  31. data/lib/csv_plus_plus/google_api_client.rb +7 -2
  32. data/lib/csv_plus_plus/google_options.rb +23 -18
  33. data/lib/csv_plus_plus/lexer/lexer.rb +8 -4
  34. data/lib/csv_plus_plus/lexer/tokenizer.rb +6 -1
  35. data/lib/csv_plus_plus/lexer.rb +24 -0
  36. data/lib/csv_plus_plus/modifier/conditional_formatting.rb +1 -0
  37. data/lib/csv_plus_plus/modifier/data_validation.rb +138 -0
  38. data/lib/csv_plus_plus/modifier/expand.rb +61 -0
  39. data/lib/csv_plus_plus/modifier/google_sheet_modifier.rb +133 -0
  40. data/lib/csv_plus_plus/modifier/modifier.rb +222 -0
  41. data/lib/csv_plus_plus/modifier/modifier_validator.rb +243 -0
  42. data/lib/csv_plus_plus/modifier/rubyxl_modifier.rb +84 -0
  43. data/lib/csv_plus_plus/modifier.rb +82 -158
  44. data/lib/csv_plus_plus/options.rb +64 -19
  45. data/lib/csv_plus_plus/parser/cell_value.tab.rb +5 -5
  46. data/lib/csv_plus_plus/parser/code_section.tab.rb +8 -13
  47. data/lib/csv_plus_plus/parser/modifier.tab.rb +17 -23
  48. data/lib/csv_plus_plus/row.rb +53 -12
  49. data/lib/csv_plus_plus/runtime/can_define_references.rb +87 -0
  50. data/lib/csv_plus_plus/runtime/can_resolve_references.rb +209 -0
  51. data/lib/csv_plus_plus/runtime/graph.rb +68 -0
  52. data/lib/csv_plus_plus/runtime/position_tracker.rb +231 -0
  53. data/lib/csv_plus_plus/runtime/references.rb +110 -0
  54. data/lib/csv_plus_plus/runtime/runtime.rb +126 -0
  55. data/lib/csv_plus_plus/runtime.rb +34 -191
  56. data/lib/csv_plus_plus/source_code.rb +66 -0
  57. data/lib/csv_plus_plus/template.rb +62 -35
  58. data/lib/csv_plus_plus/version.rb +2 -1
  59. data/lib/csv_plus_plus/writer/base_writer.rb +30 -5
  60. data/lib/csv_plus_plus/writer/csv.rb +11 -9
  61. data/lib/csv_plus_plus/writer/excel.rb +9 -2
  62. data/lib/csv_plus_plus/writer/file_backer_upper.rb +1 -0
  63. data/lib/csv_plus_plus/writer/google_sheet_builder.rb +71 -23
  64. data/lib/csv_plus_plus/writer/google_sheets.rb +79 -29
  65. data/lib/csv_plus_plus/writer/open_document.rb +6 -1
  66. data/lib/csv_plus_plus/writer/rubyxl_builder.rb +103 -30
  67. data/lib/csv_plus_plus/writer.rb +39 -9
  68. data/lib/csv_plus_plus.rb +29 -12
  69. metadata +18 -14
  70. data/lib/csv_plus_plus/can_define_references.rb +0 -88
  71. data/lib/csv_plus_plus/can_resolve_references.rb +0 -8
  72. data/lib/csv_plus_plus/data_validation.rb +0 -138
  73. data/lib/csv_plus_plus/expand.rb +0 -20
  74. data/lib/csv_plus_plus/graph.rb +0 -62
  75. data/lib/csv_plus_plus/references.rb +0 -68
  76. data/lib/csv_plus_plus/scope.rb +0 -196
  77. data/lib/csv_plus_plus/validated_modifier.rb +0 -164
  78. data/lib/csv_plus_plus/writer/google_sheet_modifier.rb +0 -77
  79. data/lib/csv_plus_plus/writer/rubyxl_modifier.rb +0 -59
@@ -0,0 +1,209 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ module CSVPlusPlus
5
+ module Runtime
6
+ # Methods for resolving functions and variables. These should be included onto a class that has +@variables+ and
7
+ # +@functions+ instance variables.
8
+ module CanResolveReferences
9
+ # Resolve all values in the ast of the current cell being processed
10
+ #
11
+ # @return [Entity]
12
+ def resolve_cell_value
13
+ return unless (ast = @cell&.ast)
14
+
15
+ last_round = nil
16
+ loop do
17
+ refs = ::CSVPlusPlus::Runtime::References.extract(ast, self)
18
+ return ast if refs.empty?
19
+
20
+ # TODO: throw an error here instead I think - basically we did a round and didn't make progress
21
+ return ast if last_round == refs
22
+
23
+ ast = resolve_functions(resolve_variables(ast, refs.variables), refs.functions)
24
+ end
25
+ end
26
+
27
+ # Bind +var_id+ to the current cell
28
+ #
29
+ # @param var_id [Symbol] The name of the variable to bind the cell reference to
30
+ #
31
+ # @return [CellReference]
32
+ def bind_variable_to_cell(var_id)
33
+ def_variable(
34
+ var_id,
35
+ ::CSVPlusPlus::Entities::CellReference.new(
36
+ cell_index: @cell_index,
37
+ row_index: @row_index
38
+ )
39
+ )
40
+ end
41
+
42
+ # Bind +var_id+ relative to an ![[expand]] modifier.
43
+ #
44
+ # @param var_id [Symbol] The name of the variable to bind the cell reference to
45
+ # @param expand [Expand] The expand where the variable is accessible (where it will be bound relative to)
46
+ #
47
+ # @return [CellReference]
48
+ def bind_variable_in_expand(var_id, expand)
49
+ def_variable(
50
+ var_id,
51
+ ::CSVPlusPlus::Entities::CellReference.new(
52
+ scoped_to_expand: expand,
53
+ cell_index: @cell_index
54
+ )
55
+ )
56
+ end
57
+
58
+ # Variables outside of an ![[expand=...] are always in scope. If it's defined within an expand then things
59
+ # get trickier because the variable is only in scope while we're processing cells within that expand.
60
+ #
61
+ # @param var_id [Symbol] The variable's identifier that we are checking if it's in scope
62
+ #
63
+ # @return [boolean]
64
+ def in_scope?(var_id)
65
+ value = @variables[var_id]
66
+
67
+ raise_modifier_syntax_error('Undefined variable reference', var_id.to_s) if value.nil?
68
+
69
+ expand = value.type == ::CSVPlusPlus::Entities::Type::CellReference && value.scoped_to_expand
70
+ return true unless expand
71
+
72
+ unless expand.starts_at
73
+ raise(::CSVPlusPlus::Error::Error, 'Must call Template.expand_rows! before checking the scope of expands.')
74
+ end
75
+
76
+ @row_index >= expand.starts_at && (expand.ends_at.nil? || row_index <= expand.ends_at)
77
+ end
78
+
79
+ private
80
+
81
+ # Resolve all variable references defined statically in the code section
82
+ # def resolve_static_variables!
83
+ # last_var_dependencies = {}
84
+ # loop do
85
+ # var_dependencies, resolution_order = variable_resolution_order(only_static_vars(variables))
86
+ # return if var_dependencies == last_var_dependencies
87
+ #
88
+ # # TODO: make the contract better here
89
+ # @variables = resolve_dependencies(var_dependencies, resolution_order, variables)
90
+ # last_var_dependencies = var_dependencies.clone
91
+ # end
92
+ # end
93
+ #
94
+ # def only_static_vars(var_dependencies)
95
+ # var_dependencies.reject { |k| @runtime.builtin_variable?(k) }
96
+ # end
97
+
98
+ def resolve_functions(ast, refs)
99
+ refs.reduce(ast.dup) do |acc, elem|
100
+ function_replace(acc, elem.id, resolve_function(elem.id))
101
+ end
102
+ end
103
+
104
+ def resolve_variables(ast, refs)
105
+ refs.reduce(ast.dup) do |acc, elem|
106
+ variable_replace(acc, elem.id, resolve_variable(elem.id))
107
+ end
108
+ end
109
+
110
+ # Make a copy of the AST represented by +node+ and replace +fn_id+ with +replacement+ throughout
111
+ # rubocop:disable Metrics/MethodLength
112
+ def function_replace(node, fn_id, replacement)
113
+ if node.type == ::CSVPlusPlus::Entities::Type::FunctionCall && node.id == fn_id
114
+ call_function_or_builtin(replacement, node)
115
+ elsif node.type == ::CSVPlusPlus::Entities::Type::FunctionCall
116
+ # not our function, but continue our depth first search on it
117
+ ::CSVPlusPlus::Entities::FunctionCall.new(
118
+ node.id,
119
+ node.arguments.map { |n| function_replace(n, fn_id, replacement) },
120
+ infix: node.infix
121
+ )
122
+ else
123
+ node
124
+ end
125
+ end
126
+ # rubocop:enable Metrics/MethodLength
127
+
128
+ def resolve_function(fn_id)
129
+ id = fn_id.to_sym
130
+ return @functions[id] if defined_function?(id)
131
+
132
+ ::CSVPlusPlus::Entities::Builtins::FUNCTIONS[id]
133
+ end
134
+
135
+ def call_function_or_builtin(function_or_builtin, function_call)
136
+ if function_or_builtin.type == ::CSVPlusPlus::Entities::Type::Function
137
+ call_function(function_or_builtin, function_call)
138
+ else
139
+ function_or_builtin.resolve_fn.call(self, function_call.arguments)
140
+ end
141
+ end
142
+
143
+ def call_function(function, function_call)
144
+ i = 0
145
+ function.arguments.reduce(function.body.dup) do |ast, argument|
146
+ variable_replace(ast, argument, function_call.arguments[i]).tap do
147
+ i += 1
148
+ end
149
+ end
150
+ end
151
+
152
+ # Make a copy of the AST represented by +node+ and replace +var_id+ with +replacement+ throughout
153
+ def variable_replace(node, var_id, replacement)
154
+ if node.type == ::CSVPlusPlus::Entities::Type::FunctionCall
155
+ arguments = node.arguments.map { |n| variable_replace(n, var_id, replacement) }
156
+ # TODO: refactor these places where we copy functions... it's brittle with the kwargs
157
+ ::CSVPlusPlus::Entities::FunctionCall.new(node.id, arguments, infix: node.infix)
158
+ elsif node.type == ::CSVPlusPlus::Entities::Type::Variable && node.id == var_id
159
+ replacement
160
+ else
161
+ node
162
+ end
163
+ end
164
+
165
+ def resolve_variable(var_id)
166
+ id = var_id.to_sym
167
+ return @variables[id] if defined_variable?(id)
168
+
169
+ raise_formula_syntax_error('Undefined variable', var_id) unless builtin_variable?(var_id)
170
+
171
+ ::CSVPlusPlus::Entities::Builtins::VARIABLES[var_id.to_sym].resolve_fn.call(self)
172
+ end
173
+
174
+ # def check_unbound_vars(dependencies, variables)
175
+ # unbound_vars = dependencies.values.flatten - variables.keys
176
+ # return if unbound_vars.empty?
177
+ #
178
+ # raise_formula_syntax_error('Undefined variables', unbound_vars.map(&:to_s).join(', '))
179
+ # end
180
+
181
+ # def variable_resolution_order(variables)
182
+ # # we have a hash of variables => ASTs but they might have references to each other, so
183
+ # # we need to interpolate them first (before interpolating the cell values)
184
+ # var_dependencies = ::CSVPlusPlus::Graph.dependency_graph(variables, @runtime)
185
+ # # are there any references that we don't have variables for? (undefined variable)
186
+ # check_unbound_vars(var_dependencies, variables)
187
+ #
188
+ # # a topological sort will give us the order of dependencies
189
+ # [var_dependencies, ::CSVPlusPlus::Graph.topological_sort(var_dependencies)]
190
+ # # TODO: don't expose this exception directly to the caller
191
+ # rescue ::TSort::Cyclic
192
+ # @runtime.raise_formula_syntax_error('Cyclic variable dependency detected', var_refs.keys)
193
+ # end
194
+
195
+ # def resolve_dependencies(var_dependencies, resolution_order, variables)
196
+ # {}.tap do |resolved_vars|
197
+ # # for each var and each dependency it has, build up and mutate resolved_vars
198
+ # resolution_order.each do |var|
199
+ # resolved_vars[var] = variables[var].dup
200
+ #
201
+ # var_dependencies[var].each do |dependency|
202
+ # resolved_vars[var] = variable_replace(resolved_vars[var], dependency, variables[dependency])
203
+ # end
204
+ # end
205
+ # end
206
+ # end
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,68 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ require 'tsort'
5
+
6
+ module CSVPlusPlus
7
+ module Runtime
8
+ # Graph ordering and searching functions
9
+ module Graph
10
+ # Get a list of all variables references in a given +ast+
11
+ # TODO: this is only used in one place - refactor it
12
+ def self.variable_references(ast, runtime, include_runtime_variables: false)
13
+ depth_first_search(ast) do |node|
14
+ next unless node.type == ::CSVPlusPlus::Entities::Type::Variable
15
+
16
+ node.id if !runtime.builtin_variable?(node.id) || include_runtime_variables
17
+ end
18
+ end
19
+
20
+ # Create a dependency graph of +variables+
21
+ def self.dependency_graph(variables, runtime)
22
+ ::CSVPlusPlus::Runtime::Graph::DependencyGraph[
23
+ variables.map { |var_id, ast| [var_id, variable_references(ast, runtime)] }
24
+ ]
25
+ end
26
+
27
+ # TODO: I don't think we use this anymore - it was useful when I wanted to resolve variables in their dependency
28
+ # order
29
+ #
30
+ # Perform a topological sort on a +DependencyGraph+. A toplogical sort is noteworthy
31
+ # because it will give us the order in which we need to resolve our variable dependencies.
32
+ #
33
+ # Given this dependency graph:
34
+ #
35
+ # { a: [b c], b: [c], c: [d], d: [] }
36
+ #
37
+ # it will return:
38
+ #
39
+ # [d, c, b, a]
40
+ #
41
+ def self.topological_sort(dependencies)
42
+ dependencies.tsort
43
+ end
44
+
45
+ # Do a DFS on an AST starting at +node+
46
+ def self.depth_first_search(node, accum = [], &)
47
+ ret = yield(node)
48
+ accum << ret unless ret.nil?
49
+
50
+ return accum unless node.type == ::CSVPlusPlus::Entities::Type::FunctionCall
51
+
52
+ node.arguments.each { |n| depth_first_search(n, accum, &) }
53
+ accum
54
+ end
55
+
56
+ # A dependency graph represented as a +Hash+ which will be used by our +topological_sort+ function
57
+ class DependencyGraph < Hash
58
+ include ::TSort
59
+ alias tsort_each_node each_key
60
+
61
+ # sort each child
62
+ def tsort_each_child(node, &)
63
+ fetch(node).each(&)
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,231 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module CSVPlusPlus
5
+ module Runtime
6
+ # Functions needed to track all of the runtime pointers: current line number, current row number, current cell, etc.
7
+ # rubocop:disable Metrics/ModuleLength
8
+ module PositionTracker
9
+ extend ::T::Sig
10
+
11
+ sig { params(cell: ::T.nilable(::CSVPlusPlus::Cell)).returns(::T.nilable(::CSVPlusPlus::Cell)) }
12
+ attr_writer :cell
13
+
14
+ sig { params(cell_index: ::T.nilable(::Integer)).returns(::T.nilable(::Integer)) }
15
+ attr_writer :cell_index
16
+
17
+ sig { params(line_number: ::T.nilable(::Integer)).returns(::T.nilable(::Integer)) }
18
+ attr_writer :line_number
19
+
20
+ sig { params(row_index: ::T.nilable(::Integer)).returns(::T.nilable(::Integer)) }
21
+ attr_writer :row_index
22
+
23
+ sig { returns(::CSVPlusPlus::Cell) }
24
+ # The current cell index. This will only be set when processing the CSV section
25
+ #
26
+ # @return [Cell]
27
+ def cell
28
+ @cell ||= ::T.let(nil, ::T.nilable(::CSVPlusPlus::Cell))
29
+ assert_initted!(@cell)
30
+ end
31
+
32
+ sig { returns(::Integer) }
33
+ # The current CSV cell index.
34
+ #
35
+ # This will only be set when processing the CSV section and will throw an exception otherwise. It is up to the
36
+ # caller (the compiler) to make sure it's called in the context of a compilation stage and/or a
37
+ # +#map_row+/+#map_rows+/+#map_lines+
38
+ #
39
+ # @return [Integer]
40
+ def cell_index
41
+ @cell_index ||= ::T.let(nil, ::T.nilable(::Integer))
42
+ assert_initted!(@cell_index)
43
+ end
44
+
45
+ sig { returns(::Integer) }
46
+ # The current CSV row index. This will only be set when processing the CSV section
47
+ #
48
+ # This will only be set when processing the CSV section and will throw an exception otherwise. It is up to the
49
+ # caller (the compiler) to make sure it's called in the context of a compilation stage and/or a
50
+ # +#map_row+/+#map_rows+/+#map_lines+
51
+ #
52
+ # @return [Integer]
53
+ def row_index
54
+ @row_index ||= ::T.let(nil, ::T.nilable(::Integer))
55
+ assert_initted!(@row_index)
56
+ end
57
+
58
+ sig { returns(::Integer) }
59
+ # The current line number being processed. The line number is based on the entire file, irregardless of if it's
60
+ # parsing the code section or the CSV section
61
+ #
62
+ # This will only be set when processing the csvpp file and will throw an exception otherwise. It is up to the
63
+ # caller (the compiler) to make sure it's called in the context of a compilation stage and/or a
64
+ # +#map_row+/+#map_rows+/+#map_lines+
65
+ #
66
+ # @return [Integer]
67
+ def line_number
68
+ @line_number ||= ::T.let(nil, ::T.nilable(::Integer))
69
+ assert_initted!(@line_number)
70
+ end
71
+
72
+ sig { void }
73
+ # Clean up the Tempfile we're using for parsing
74
+ def cleanup!
75
+ input&.close
76
+ input&.unlink
77
+ end
78
+
79
+ sig { returns(::T.nilable(::Tempfile)) }
80
+ # The currently available input for parsing. The tmp state will be re-written
81
+ # between parsing the code section and the CSV section
82
+ #
83
+ # @return [::Tempfile]
84
+ def input
85
+ @input ||= ::T.let(::Tempfile.new, ::T.nilable(::Tempfile))
86
+ end
87
+
88
+ sig do
89
+ type_parameters(:I, :O).params(
90
+ lines: ::T::Enumerable[::T.type_parameter(:I)],
91
+ block: ::T.proc.params(args0: ::T.type_parameter(:I)).returns(::T.type_parameter(:O))
92
+ ).returns(::T::Array[::T.type_parameter(:O)])
93
+ end
94
+ # Map over a csvpp file and keep track of line_number and row_index
95
+ #
96
+ # @param lines [Array]
97
+ #
98
+ # @return [Array]
99
+ def map_lines(lines, &block)
100
+ self.line_number = 1
101
+ lines.map do |line|
102
+ ret = block.call(line)
103
+ next_line!
104
+ ret
105
+ end
106
+ end
107
+
108
+ sig do
109
+ type_parameters(:I, :O)
110
+ .params(
111
+ row: ::T::Enumerable[::T.all(::T.type_parameter(:I), ::Object)],
112
+ block: ::T.proc.params(
113
+ cell: ::T.all(::T.type_parameter(:I), ::Object),
114
+ index: ::Integer
115
+ ).returns(::T.type_parameter(:O))
116
+ )
117
+ .returns(::T::Array[::T.type_parameter(:O)])
118
+ end
119
+ # Map over a single row and keep track of the cell and it's index
120
+ #
121
+ # @param row [Array<Cell>] The row to map each cell over
122
+ #
123
+ # @return [Array]
124
+ def map_row(row, &block)
125
+ row.map.with_index do |cell, index|
126
+ self.cell_index = index
127
+ self.cell = cell if cell.is_a?(::CSVPlusPlus::Cell)
128
+ block.call(cell, index)
129
+ end
130
+ end
131
+
132
+ sig do
133
+ type_parameters(:O).params(
134
+ rows: ::T::Enumerable[::CSVPlusPlus::Row],
135
+ block: ::T.proc.params(row: ::CSVPlusPlus::Row).returns(::T.type_parameter(:O))
136
+ ).returns(::T::Array[::T.type_parameter(:O)])
137
+ end
138
+ # Map over all rows and keep track of row and line numbers
139
+ #
140
+ # @param rows [Array<Row>] The rows to map over (and keep track of indexes)
141
+ #
142
+ # @return [Array]
143
+ def map_rows(rows, &block)
144
+ self.row_index = 0
145
+ map_lines(rows) do |row|
146
+ block.call(row)
147
+ end
148
+ end
149
+
150
+ sig do
151
+ type_parameters(:R)
152
+ .params(rows: ::T::Enumerable[::CSVPlusPlus::Row],
153
+ block: ::T.proc.params(cell: ::CSVPlusPlus::Cell, index: ::Integer).returns(::T.type_parameter(:R)))
154
+ .returns(::T::Array[::T::Array[::T.type_parameter(:R)]])
155
+ end
156
+ # Map over all +rows+ and over all of their +cells+, calling the +&block+ with each +Cell+
157
+ #
158
+ # @param rows [Array<Row>]
159
+ #
160
+ # @return [Array<Array>]
161
+ # rubocop:disable Naming/BlockForwarding
162
+ def map_all_cells(rows, &block)
163
+ self.row_index = 0
164
+ map_lines(rows) { |row| map_row(row.cells, &block) }
165
+ end
166
+ # rubocop:enable Naming/BlockForwarding
167
+
168
+ sig { returns(::Integer) }
169
+ # Return the current spreadsheet row number. It parallels +@row_index+ but starts at 1.
170
+ #
171
+ # @return [Integer, nil]
172
+ def rownum
173
+ row_index + 1
174
+ end
175
+
176
+ sig do
177
+ type_parameters(:R).params(block: ::T.proc.returns(::T.type_parameter(:R))).returns(::T.type_parameter(:R))
178
+ end
179
+ # Each time we run a parse on the input, reset the runtime state starting at the beginning of the file
180
+ def start!(&block)
181
+ @row_index = @cell_index = 0
182
+ self.line_number = 1
183
+
184
+ ret = block.call
185
+ finish!
186
+ ret
187
+ end
188
+
189
+ sig { params(data: ::String).void }
190
+ # We mutate the input over and over. It's ok because it's just a Tempfile
191
+ #
192
+ # @param data [::String] The data to rewrite our input file to
193
+ def rewrite_input!(data)
194
+ input&.truncate(0)
195
+ input&.write(data)
196
+ input&.rewind
197
+ end
198
+
199
+ private
200
+
201
+ sig do
202
+ type_parameters(:R).params(runtime_value: ::T.nilable(::T.type_parameter(:R))).returns(::T.type_parameter(:R))
203
+ end
204
+ def assert_initted!(runtime_value)
205
+ ::T.must_because(runtime_value) do
206
+ 'Runtime value accessed without an initialized runtime. Make sure you call Runtime#start! or ' \
207
+ 'Runtime#start_at_csv! first.'
208
+ end
209
+ end
210
+
211
+ sig { void }
212
+ # Called to mark the trackers dirty. It should be an error to use them outside of an initialized context.
213
+ def finish!
214
+ @line_number = nil
215
+ @row_index = nil
216
+ @cell_index = nil
217
+ @cell = nil
218
+ end
219
+
220
+ sig { returns(::Integer) }
221
+ # Increment state to the next line
222
+ #
223
+ # @return [Integer]
224
+ def next_line!
225
+ self.row_index += 1
226
+ self.line_number += 1
227
+ end
228
+ end
229
+ # rubocop:enable Metrics/ModuleLength
230
+ end
231
+ end
@@ -0,0 +1,110 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module CSVPlusPlus
5
+ module Runtime
6
+ # References in an AST that need to be resolved
7
+ #
8
+ # @attr functions [Array<Entities::Function>] Functions references
9
+ # @attr variables [Array<Entities::Variable>] Variable references
10
+ # TODO: turn this into a CanExtractReferences?
11
+ class References
12
+ extend ::T::Sig
13
+
14
+ sig { returns(::T::Array[::CSVPlusPlus::Entities::FunctionCall]) }
15
+ attr_accessor :functions
16
+
17
+ sig { returns(::T::Array[::CSVPlusPlus::Entities::Variable]) }
18
+ attr_accessor :variables
19
+
20
+ sig do
21
+ params(
22
+ ast: ::CSVPlusPlus::Entities::Entity,
23
+ runtime: ::CSVPlusPlus::Runtime::Runtime
24
+ ).returns(::CSVPlusPlus::Runtime::References)
25
+ end
26
+ # Extract references from an AST and return them in a new +References+ object
27
+ #
28
+ # @param ast [Entity] An +Entity+ to do a depth first search on for references. Entities can be
29
+ # infinitely deep because they can contain other function calls as params to a function call
30
+ # @param runtime [Runtime] The current runtime
31
+ #
32
+ # @return [References]
33
+ def self.extract(ast, runtime)
34
+ new.tap do |refs|
35
+ ::CSVPlusPlus::Runtime::Graph.depth_first_search(ast) do |node|
36
+ unless node.type == ::CSVPlusPlus::Entities::Type::FunctionCall \
37
+ || node.type == ::CSVPlusPlus::Entities::Type::Variable
38
+
39
+ next
40
+ end
41
+
42
+ refs.functions << node if function_reference?(node, runtime)
43
+ refs.variables << node if variable_reference?(node, runtime)
44
+ end
45
+ end
46
+ end
47
+
48
+ sig do
49
+ params(node: ::CSVPlusPlus::Entities::Entity, runtime: ::CSVPlusPlus::Runtime::Runtime).returns(::T::Boolean)
50
+ end
51
+ # Is the node a resolvable variable reference?
52
+ #
53
+ # @param node [Entity] The node to check if it's resolvable
54
+ # @param runtime [Runtime] The current runtime
55
+ #
56
+ # @return [boolean]
57
+ def self.variable_reference?(node, runtime)
58
+ return false unless node.type == ::CSVPlusPlus::Entities::Type::Variable
59
+
60
+ if runtime.in_scope?(node.id)
61
+ true
62
+ else
63
+ runtime.raise_modifier_syntax_error(
64
+ "#{node.id} can only be referenced within the ![[expand]] where it was defined.",
65
+ node.id.to_s
66
+ )
67
+ end
68
+ end
69
+ private_class_method :variable_reference?
70
+
71
+ sig do
72
+ params(node: ::CSVPlusPlus::Entities::Entity, runtime: ::CSVPlusPlus::Runtime::Runtime).returns(::T::Boolean)
73
+ end
74
+ # Is the node a resolvable function reference?
75
+ #
76
+ # @param node [Entity] The node to check if it's resolvable
77
+ # @param runtime [Runtime] The current runtime
78
+ #
79
+ # @return [boolean]
80
+ def self.function_reference?(node, runtime)
81
+ node.type == ::CSVPlusPlus::Entities::Type::FunctionCall \
82
+ && (runtime.defined_function?(node.id) || runtime.builtin_function?(::T.must(node.id)))
83
+ end
84
+ private_class_method :function_reference?
85
+
86
+ sig { void }
87
+ # Create an object with empty references. The caller will build them up as it depth-first-searches
88
+ def initialize
89
+ @functions = ::T.let([], ::T::Array[::CSVPlusPlus::Entities::FunctionCall])
90
+ @variables = ::T.let([], ::T::Array[::CSVPlusPlus::Entities::Variable])
91
+ end
92
+
93
+ sig { params(other: ::CSVPlusPlus::Runtime::References).returns(::T::Boolean) }
94
+ # @param other [References]
95
+ #
96
+ # @return [boolean]
97
+ def ==(other)
98
+ @functions == other.functions && @variables == other.variables
99
+ end
100
+
101
+ sig { returns(::T::Boolean) }
102
+ # Are there any references to be resolved?
103
+ #
104
+ # @return [::T::Boolean]
105
+ def empty?
106
+ @functions.empty? && @variables.empty?
107
+ end
108
+ end
109
+ end
110
+ end