Module: Luoma::StaticAnalysis

Defined in:
lib/luoma/static_analysis.rb,
sig/luoma/static_analysis.rbs

Overview

Template static analysis.

Defined Under Namespace

Classes: Location, Result, StaticScope, StaticVariable, VariableMap

Class Method Summary collapse

Class Method Details

.analyze(template, include_partials:) ⇒ Result

Parameters:

Returns:



220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
# File 'lib/luoma/static_analysis.rb', line 220

def self.analyze(template, include_partials:)
  variables = VariableMap.new
  globals = VariableMap.new
  locals = VariableMap.new

  # @type var filters: Hash[String, Array[Location]]
  filters = Hash.new { |hash, key| hash[key] = [] }
  # @type var tags: Hash[String, Array[Location]]
  tags = Hash.new { |hash, key| hash[key] = [] }

  # @type var template_scope: Set[String]
  template_scope = Set[]
  root_scope = StaticScope.new(template_scope)
  static_context = Luoma::RenderContext.new(template)

  # Names of partial templates that have already been analyzed.
  # Keys are hashes of partial template name and its arguments. If we've
  # visited a template before but with different arguments, later visits
  # only record global variables so as not to double count locals, filters
  # and tags.
  seen = Hash.new { |hash, key| hash[key] = Set[] } #: Hash[String,Set[untyped]]

  # @type var visit: ^(Luoma::Markup, Luoma::Template, StaticScope, bool) -> void
  visit = lambda do |node, template, scope, just_globals|
    seen[template.name].add(nil) if !template.name.empty? && !just_globals

    # Update tags
    # Markup with empty `tag_name` is silenced.
    tags[node.tag_name] << Location.new(template, node.token) if !just_globals && !node.tag_name.empty?

    # Update variables from node.expressions
    node.expressions.each do |expr|
      if expr.is_a?(Luoma::Expression)
        analyze_variables(
          expr,
          template,
          scope,
          globals,
          just_globals ? VariableMap.new : variables,
          static_context
        )
      end

      next if just_globals

      # Update filters from expr
      extract_filters(expr, template, static_context).each do |name, span|
        filters[name] << span
      end
    end

    # Update template scope from node.template_scope
    node.template_scope.each do |name|
      scope.add(name.value)
      locals.add(StaticVariable.new([name.value], Location.new(template, name.token)))
    end

    # Set block scope before descending into child nodes.
    scope.push(node.block_scope.to_set(&:value))

    node.children(static_context).each do |child|
      visit.call(child, template, scope, just_globals)
    end

    scope.pop

    # Descend into partial templates?
    partial = include_partials && node.partial(static_context)
    if partial.is_a?(Luoma::Partial)
      name = partial.template.name
      just_globals_ = seen.include?(name)

      unless seen[name].include?(partial.key)
        seen[name].add(partial.key)

        partial_scope = if partial.scope_kind == :isolated
                          StaticScope.new(partial.in_scope.to_set(&:value))
                        else
                          root_scope.push(partial.in_scope.to_set(&:value))
                        end

        partial.template.nodes.each do |p_node|
          visit.call(p_node, partial.template, partial_scope, just_globals_) if p_node.is_a?(Luoma::Markup)
        end

        partial_scope.pop if partial.scope_kind == :isolated
      end
    end
  end

  template.nodes.each do |node|
    visit.call(node, template, root_scope, false) if node.is_a?(Luoma::Markup)
  end

  Result.new(
    variables.to_h,
    globals.to_h,
    locals.to_h,
    to_locations(filters),
    to_locations(tags)
  )
end

.analyze_variables(expression, template, scope, globals, variables, static_context) ⇒ Object

(Luoma:_Traversable, Luoma::Template, StaticScope, VariableMap, VariableMap, RenderContext) -> void

Parameters:

Returns:

  • (Object)


339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
# File 'lib/luoma/static_analysis.rb', line 339

def self.analyze_variables(expression, template, scope, globals, variables, static_context)
  if expression.is_a?(Luoma::Variable) # steep:ignore
    token = if expression.segments.last.is_a?(Luoma::Predicate)
              # Don't include a the trailing predicate.
              if expression.segments.length > 1
                Luoma.span(expression.root.span,
                           expression.segments[-2].span)
              else
                expression.root.span
              end
            else
              expression.span
            end

    var = StaticVariable.new(segments(expression, template), Location.new(template, token))

    variables.add(var)
    globals.add(var) unless scope.include?(expression.root.to_s)
  end

  # For lambda expressions.
  child_scope = expression.is_a?(Luoma::Expression) ? expression.scope : [] # steep:ignore
  scope.push(child_scope.to_set(&:value))

  expression.children.each do |expr|
    analyze_variables(
      expr,
      template,
      scope,
      globals,
      variables,
      static_context
    )
  end

  scope.pop
end

.extract_filters(expression, template, static_context) ⇒ Array[[String, Location]]

(Luoma::_Traversable, Luoma::Template, Luoma::RenderContext) -> Array[[String, Location]]

Parameters:

Returns:



324
325
326
327
328
329
330
331
332
333
334
335
336
# File 'lib/luoma/static_analysis.rb', line 324

def self.extract_filters(expression, template, static_context)
  filters = [] # : Array[[String, Location]]

  if expression.is_a?(Luoma::Filter) # steep:ignore
    filters << [expression.name.to_s, Location.new(template, expression.span)]
  end

  expression.children.each do |expr|
    filters.concat(extract_filters(expr, template, static_context))
  end

  filters
end

.segments(var, template) ⇒ Array[untyped]

(Luoma::Variable, Luoma::Template) -> Array

Parameters:

Returns:

  • (Array[untyped])


378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
# File 'lib/luoma/static_analysis.rb', line 378

def self.segments(var, template)
  segments_ = [] #: Array[untyped]

  segments_ << if var.root.is_a?(Luoma::Variable)
                 segments(var.root, template)
               else
                 var.root.value
               end

  var.segments.each do |s|
    segments_ << if s.is_a?(Luoma::Variable)
                   segments(s, template)
                 elsif s.is_a?(Luoma::Predicate)
                   next
                 else
                   s.value
                 end
  end

  segments_
end

.to_locations(map) ⇒ Object

(Hash[String, Array[Location]]) -> Hash[String, Array[untyped]]

Parameters:

  • map (Object)

Returns:

  • (Object)


401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
# File 'lib/luoma/static_analysis.rb', line 401

def self.to_locations(map)
  result = {} #: Hash[String, Array[untyped]]

  map.each do |k, v|
    a = [] #: Array[untyped]

    v.each do |l|
      span = l.span
      a << {
        start_index: l.token[1],
        end_index: l.token[2],
        start_line: span.first.first,
        start_column: span.first.last,
        end_line: span.last.first,
        end_column: span.last.last,
        value: l.value,
        template_name: l.template.name
      }
    end

    result[k] = a
  end

  result
end