Module: MilkTea::Linter::LinterRules

Included in:
MilkTea::Linter
Defined in:
lib/milk_tea/tooling/linter/rules.rb

Constant Summary collapse

PURE_EXPRESSION_TYPES =

── useless-expression ───────────────────────────────────────────────────

[
  AST::IntegerLiteral, AST::FloatLiteral, AST::StringLiteral, AST::FormatString,
  AST::BooleanLiteral, AST::NullLiteral,
  AST::BinaryOp, AST::UnaryOp,
  AST::Identifier, AST::UnsafeExpr,
].freeze
SIDE_EFFECTING_EXPRESSION_TYPES =
[
  AST::Call, AST::AwaitExpr,
].freeze

Instance Method Summary collapse

Instance Method Details

#always_returns?(stmts) ⇒ Boolean

Returns true if every execution path through stmts ends with a guaranteed return. Conservative: only IfStmt with else + MatchStmt with arms are considered exhaustive.

Returns:

  • (Boolean)


59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/milk_tea/tooling/linter/rules.rb', line 59

def always_returns?(stmts)
  stmts.any? do |stmt|
    case stmt
    when AST::ReturnStmt
      true
    when AST::ExpressionStmt
      terminating_expression?(stmt.expression)
    when AST::IfStmt
      # Only exhaustive if there is an else branch AND every branch returns
      stmt.else_body && !stmt.else_body.empty? &&
        stmt.branches.all? { |b| always_returns?(b.body) } &&
        always_returns?(stmt.else_body)
    when AST::WhileStmt
      infinite_while_without_break?(stmt)
    when AST::MatchStmt
      stmt.arms.any? && stmt.arms.all? { |arm| always_returns?(arm.body) }
    when AST::WhenStmt
      all_branches = stmt.branches.map(&:body)
      all_branches << stmt.else_body if stmt.else_body && !stmt.else_body.empty?
      all_branches.any? && all_branches.all? { |b| always_returns?(b) }
    when AST::StaticAssert
      stmt.condition.is_a?(AST::BooleanLiteral) && stmt.condition.value == false
    when AST::UnsafeStmt
      always_returns?(stmt.body)
    else
      false
    end
  end
end

#binding_type_for_declaration(statement) ⇒ Object



478
479
480
481
482
483
484
485
486
# File 'lib/milk_tea/tooling/linter/rules.rb', line 478

def binding_type_for_declaration(statement)
  binding_resolution = @sema_facts&.binding_resolution
  return nil unless binding_resolution&.binding_types

  binding_id = binding_resolution.declaration_binding_ids[statement.object_id]
  return nil unless binding_id

  binding_resolution.binding_types[binding_id]
end

#check_directional_ffi_call(expression) ⇒ Object

── directional-ffi-arg ──────────────────────────────────────────────



504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
# File 'lib/milk_tea/tooling/linter/rules.rb', line 504

def check_directional_ffi_call(expression)
  call = resolve_directional_ffi_call(expression.callee)
  return unless call

  call[:params].zip(expression.arguments).each do |parameter, argument|
    next unless parameter && argument

    passing_mode = parameter_passing_mode(parameter)
    next unless %i[in out inout].include?(passing_mode)
    next unless legacy_directional_argument_expression?(argument.value)

    @warnings << Warning.new(
      path: @path,
      line: expression_line(argument.value),
      column: expression_column(argument.value),
      length: expression_length(argument.value),
      code: "directional-ffi-arg",
      message: "pass the lvalue directly to '#{call[:name]}'; parameter '#{parameter_name(parameter)}' already declares #{passing_mode} passing",
      severity: :hint,
      symbol_name: parameter_name(parameter)
    )
  end
end

#check_duplicate_if_conditions(statement) ⇒ Object



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/milk_tea/tooling/linter/rules.rb', line 297

def check_duplicate_if_conditions(statement)
  return unless statement.is_a?(AST::IfStmt)

  seen_signatures = {}
  statement.branches.each do |branch|
    signature = expression_signature(branch.condition)
    next unless signature

    existing = seen_signatures[signature]
    if existing
      @warnings << Warning.new(
        path: @path,
        line: expression_line(branch.condition) || branch.line,
        column: expression_column(branch.condition),
        length: expression_length(branch.condition),
        code: "duplicate-if-condition",
        message: "duplicate condition matches an earlier if/else-if branch and is unreachable",
        severity: :warning,
      )
      next
    end

    seen_signatures[signature] = branch
  end
end

#check_missing_return(function) ⇒ Object

── missing-return ───────────────────────────────────────────────────



8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# File 'lib/milk_tea/tooling/linter/rules.rb', line 8

def check_missing_return(function)
  return unless function.return_type           # implicit void — no check
  return if void_return_type?(function.return_type)
  return if always_returns?(function.body)

  @warnings << Warning.new(
    path: @path,
    line: function.line,
    column: function.column,
    length: function.name.length,
    code: "missing-return",
    message: "function '#{function.name}' does not always return a value",
    severity: :error,
    symbol_name: function.name
  )
end

#check_noop_compound_assignment(statement) ⇒ Object



350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
# File 'lib/milk_tea/tooling/linter/rules.rb', line 350

def check_noop_compound_assignment(statement)
  return unless statement.is_a?(AST::Assignment)
  return unless %w[+= -= *= /= |= ^= <<= >>=].include?(statement.operator)

  identity = noop_compound_identity_value(statement.operator, statement.value)
  return unless identity

  @warnings << Warning.new(
    path: @path,
    line: statement.line,
    column: expression_column(statement.target),
    length: statement.target.respond_to?(:name) ? statement.target.name.length : expression_length(statement.target),
    code: "noop-compound-assignment",
    message: "compound assignment with identity value has no effect",
    severity: :hint,
  )
end

#check_redundant_bool_compare(expr) ⇒ Object



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
# File 'lib/milk_tea/tooling/linter/rules.rb', line 246

def check_redundant_bool_compare(expr)
  return unless %w[== !=].include?(expr.operator)

  left_bool = expr.left.is_a?(AST::BooleanLiteral)
  right_bool = expr.right.is_a?(AST::BooleanLiteral)
  return unless left_bool ^ right_bool

  literal = left_bool ? expr.left : expr.right
  compared = left_bool ? expr.right : expr.left
  return if compared.is_a?(AST::BooleanLiteral)

  suggestion = if expr.operator == "=="
                 literal.value ? "use the expression directly" : "invert the expression with 'not'"
               else
                 literal.value ? "invert the expression with 'not'" : "use the expression directly"
               end

  @warnings << Warning.new(
    path: @path,
    line: expression_line(expr),
    column: expression_column(expr),
    length: redundant_bool_compare_length(expr),
    code: "redundant-bool-compare",
    message: "boolean comparison against literal is redundant; #{suggestion}",
    severity: :hint,
  )
end

#check_redundant_else(stmt) ⇒ Object

── redundant-else ─────────────────────────────────────────────────── Fire when every explicit if/else if branch always returns, making the else block an unnecessary level of indentation.



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/milk_tea/tooling/linter/rules.rb', line 137

def check_redundant_else(stmt)
  return unless stmt.else_body && !stmt.else_body.empty?
  return unless stmt.branches.all? { |b| always_returns?(b.body) }

  # Use the line of the first else-body statement as the diagnostic anchor.
  else_line = stmt.else_body.first.line || stmt.line
  @warnings << Warning.new(
    path: @path,
    line: stmt.else_line || else_line,
    column: stmt.else_column,
    length: 4,
    code: "redundant-else",
    message: "else block is redundant because all preceding branches return"
  )
end

#check_self_assignment(stmt) ⇒ Object

── self-assignment ────────────────────────────────────────────────────



208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/milk_tea/tooling/linter/rules.rb', line 208

def check_self_assignment(stmt)
  return unless stmt.operator == "="
  return unless stmt.target.is_a?(AST::Identifier) && stmt.value.is_a?(AST::Identifier)
  return unless stmt.target.name == stmt.value.name

  @warnings << Warning.new(
    path: @path,
    line: expression_line(stmt.target) || stmt.line,
    column: expression_column(stmt.target),
    length: expression_length(stmt.target),
    code: "self-assignment",
    message: "'#{stmt.target.name}' is assigned to itself",
    severity: :warning,
    symbol_name: stmt.target.name
  )
end

#check_self_comparison(expr) ⇒ Object

── self-comparison ────────────────────────────────────────────────────



227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/milk_tea/tooling/linter/rules.rb', line 227

def check_self_comparison(expr)
  return unless %w[== !=].include?(expr.operator)
  return unless expr.left.is_a?(AST::Identifier) && expr.right.is_a?(AST::Identifier)
  return unless expr.left.name == expr.right.name

  line = expression_line(expr) || expr.left.line
  always = expr.operator == "==" ? "always true" : "always false"
  @warnings << Warning.new(
    path: @path,
    line:,
    column: expression_column(expr),
    length: expression_length(expr),
    code: "self-comparison",
    message: "'#{expr.left.name}' is compared to itself — #{always}",
    severity: :warning,
    symbol_name: expr.left.name
  )
end

#check_useless_expression(stmt) ⇒ Object



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/milk_tea/tooling/linter/rules.rb', line 165

def check_useless_expression(stmt)
  expr = stmt.expression
  return unless PURE_EXPRESSION_TYPES.any? { |t| expr.is_a?(t) }
  return if expr.is_a?(AST::UnaryOp) && expr.operator == "?"
  return if contains_side_effecting_expression?(expr)

  line = expression_line(expr) || (stmt.line)
  column = expression_column(expr)
  length = expression_length(expr)
  if line && (!column || !length || expr.is_a?(AST::UnsafeExpr))
    fallback_span = source_statement_span(line)
    column ||= fallback_span&.first
    length = fallback_span&.last if !length || expr.is_a?(AST::UnsafeExpr)
  end

  @warnings << Warning.new(
    path: @path,
    line:,
    column:,
    length:,
    code: "useless-expression",
    message: "expression result is unused and has no side effects",
    severity: :warning
  )
end

#contains_side_effecting_expression?(expr) ⇒ Boolean

Returns:

  • (Boolean)


191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/milk_tea/tooling/linter/rules.rb', line 191

def contains_side_effecting_expression?(expr)
  return true if SIDE_EFFECTING_EXPRESSION_TYPES.any? { |t| expr.is_a?(t) }
  return true if expr.is_a?(AST::UnaryOp) && expr.operator == "?"

  case expr
  when AST::BinaryOp
    contains_side_effecting_expression?(expr.left) || contains_side_effecting_expression?(expr.right)
  when AST::UnsafeExpr
    contains_side_effecting_expression?(expr.expression)
  when AST::FormatString
    expr.parts.any? { |part| part.is_a?(AST::FormatExprPart) && contains_side_effecting_expression?(part.expression) }
  else
    false
  end
end

#directional_ffi_binding?(binding) ⇒ Boolean

Returns:

  • (Boolean)


558
559
560
561
562
563
# File 'lib/milk_tea/tooling/linter/rules.rb', line 558

def directional_ffi_binding?(binding)
  return false unless binding
  return false unless binding.respond_to?(:ast) && (binding.ast.is_a?(AST::ExternFunctionDecl) || binding.ast.is_a?(AST::ForeignFunctionDecl))

  binding.type.params.any? { |parameter| %i[in out inout].include?(parameter.passing_mode) }
end

#emit_prefer_else_warnings(stmts, expected_kind:, code:) ⇒ Object



399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
# File 'lib/milk_tea/tooling/linter/rules.rb', line 399

def emit_prefer_else_warnings(stmts, expected_kind:, code:)
  return if stmts.nil? || stmts.empty?

  walk_statement_lists(stmts) do |statement_list|
    statement_list.each_with_index do |_statement, index|
      candidate = prefer_else_candidate(statement_list, index, expected_kind:)
      next unless candidate

      branch = candidate[:branch]
      line, column, length = condition_span(branch.condition, line: candidate[:if_stmt].line, keyword_pattern: "if")

      @warnings << Warning.new(
        path: @path,
        line:,
        column:,
        length:,
        code:,
        message: "nullable guard for '#{candidate[:declaration].name}' can use #{expected_kind} ... else",
        severity: :hint,
        symbol_name: candidate[:declaration].name
      )
    end
  end
end

#emit_prefer_let_else_warnings(stmts) ⇒ Object

── prefer-let-else / prefer-var-else ──────────────────────────────



391
392
393
# File 'lib/milk_tea/tooling/linter/rules.rb', line 391

def emit_prefer_let_else_warnings(stmts)
  emit_prefer_else_warnings(stmts, expected_kind: :let, code: "prefer-let-else")
end

#emit_prefer_var_else_warnings(stmts) ⇒ Object



395
396
397
# File 'lib/milk_tea/tooling/linter/rules.rb', line 395

def emit_prefer_var_else_warnings(stmts)
  emit_prefer_else_warnings(stmts, expected_kind: :var, code: "prefer-var-else")
end

#emit_redundant_return_warnings(function) ⇒ Object



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/milk_tea/tooling/linter/rules.rb', line 25

def emit_redundant_return_warnings(function)
  return unless function.return_type
  return unless void_return_type?(function.return_type)

  final_statement = function.body&.last
  return unless final_statement.is_a?(AST::ReturnStmt)
  return unless final_statement.value.nil?

  @warnings << Warning.new(
    path: @path,
    line: final_statement.line,
    column: final_statement.column,
    length: final_statement.length || "return".length,
    code: "redundant-return",
    message: "final bare return in void function is redundant",
    severity: :hint,
  )
end

#expression_signature(expression) ⇒ Object



323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
# File 'lib/milk_tea/tooling/linter/rules.rb', line 323

def expression_signature(expression)
  case expression
  when AST::Identifier
    "id:#{expression.name}"
  when AST::BooleanLiteral
    "bool:#{expression.value}"
  when AST::IntegerLiteral, AST::FloatLiteral, AST::StringLiteral
    "lit:#{expression.lexeme}"
  when AST::NullLiteral
    "null"
  when AST::MemberAccess
    receiver = expression_signature(expression.receiver)
    receiver ? "member:(#{receiver}).#{expression.member}" : nil
  when AST::UnaryOp
    operand = expression_signature(expression.operand)
    operand ? "unary:#{expression.operator}(#{operand})" : nil
  when AST::BinaryOp
    left = expression_signature(expression.left)
    right = expression_signature(expression.right)
    return nil unless left && right

    "binary:(#{left})#{expression.operator}(#{right})"
  else
    nil
  end
end

#infinite_while_without_break?(stmt) ⇒ Boolean

Returns:

  • (Boolean)


89
90
91
92
93
# File 'lib/milk_tea/tooling/linter/rules.rb', line 89

def infinite_while_without_break?(stmt)
  stmt.condition.is_a?(AST::BooleanLiteral) &&
    stmt.condition.value == true &&
    !loop_body_can_break?(stmt.body)
end

#integer_literal_zero?(value) ⇒ Boolean

Returns:

  • (Boolean)


379
380
381
# File 'lib/milk_tea/tooling/linter/rules.rb', line 379

def integer_literal_zero?(value)
  value.is_a?(AST::IntegerLiteral) && value.lexeme.gsub("_", "") == "0"
end

#legacy_directional_argument_expression?(expression) ⇒ Boolean

Returns:

  • (Boolean)


573
574
575
576
577
578
579
580
581
582
583
584
# File 'lib/milk_tea/tooling/linter/rules.rb', line 573

def legacy_directional_argument_expression?(expression)
  return true if expression.is_a?(AST::UnaryOp) && %w[in out inout].include?(expression.operator)

  if expression.is_a?(AST::Call) && expression.callee.is_a?(AST::Identifier) && %w[ptr_of ref_of].include?(expression.callee.name)
    return expression.arguments.length == 1
  end

  cast = pointer_like_cast_expression(expression)
  return false unless cast

  lvalue_expression?(cast[:source]) || legacy_directional_argument_expression?(cast[:source])
end

#loop_body_can_break?(body) ⇒ Boolean

Returns:

  • (Boolean)


95
96
97
98
99
100
101
102
103
# File 'lib/milk_tea/tooling/linter/rules.rb', line 95

def loop_body_can_break?(body)
  return false if body.nil? || body.empty?

  graph = ControlFlow::Builder.new.build_loop_body(body)
  reachability = ControlFlow::Reachability.solve(graph)
  graph.each_node.any? do |node|
    node.kind == :break_exit && reachability.reachable_ids.include?(node.id)
  end
end

#lvalue_expression?(expression) ⇒ Boolean

Returns:

  • (Boolean)


586
587
588
# File 'lib/milk_tea/tooling/linter/rules.rb', line 586

def lvalue_expression?(expression)
  expression.is_a?(AST::Identifier) || expression.is_a?(AST::MemberAccess) || expression.is_a?(AST::IndexAccess)
end

#noop_compound_identity_value(operator, value) ⇒ Object



368
369
370
371
372
373
374
375
376
377
# File 'lib/milk_tea/tooling/linter/rules.rb', line 368

def noop_compound_identity_value(operator, value)
  case operator
  when "+=", "-=", "|=", "^=", "<<=", ">>="
    integer_literal_zero?(value)
  when "*=", "/="
    numeric_literal_one?(value)
  else
    false
  end
end

#null_equality_identifier(cond) ⇒ Object



450
451
452
453
454
455
456
457
458
# File 'lib/milk_tea/tooling/linter/rules.rb', line 450

def null_equality_identifier(cond)
  return nil unless cond.is_a?(AST::BinaryOp) && cond.operator == "=="

  if cond.left.is_a?(AST::Identifier) && cond.right.is_a?(AST::NullLiteral)
    cond.left
  elsif cond.left.is_a?(AST::NullLiteral) && cond.right.is_a?(AST::Identifier)
    cond.right
  end
end

#nullable_binding_declaration?(statement) ⇒ Boolean

Returns:

  • (Boolean)


460
461
462
463
464
465
# File 'lib/milk_tea/tooling/linter/rules.rb', line 460

def nullable_binding_declaration?(statement)
  binding_type = binding_type_for_declaration(statement)
  return binding_type.is_a?(Types::Nullable) if binding_type

  nullable_initializer_without_binding_type?(statement.value)
end

#nullable_initializer_without_binding_type?(expression) ⇒ Boolean

Returns:

  • (Boolean)


467
468
469
470
471
472
473
474
475
476
# File 'lib/milk_tea/tooling/linter/rules.rb', line 467

def nullable_initializer_without_binding_type?(expression)
  case expression
  when AST::Identifier, AST::MemberAccess, AST::IndexAccess, AST::Call, AST::IfExpr, AST::MatchExpr
    true
  when AST::AwaitExpr, AST::UnsafeExpr
    nullable_initializer_without_binding_type?(expression.expression)
  else
    false
  end
end

#numeric_literal_one?(value) ⇒ Boolean

Returns:

  • (Boolean)


383
384
385
386
387
388
# File 'lib/milk_tea/tooling/linter/rules.rb', line 383

def numeric_literal_one?(value)
  return true if value.is_a?(AST::IntegerLiteral) && value.lexeme.gsub("_", "") == "1"
  return true if value.is_a?(AST::FloatLiteral) && %w[1.0 1.].include?(value.lexeme.gsub("_", ""))

  false
end

#parameter_name(parameter) ⇒ Object



569
570
571
# File 'lib/milk_tea/tooling/linter/rules.rb', line 569

def parameter_name(parameter)
  parameter.respond_to?(:name) ? parameter.name : "argument"
end

#parameter_passing_mode(parameter) ⇒ Object



565
566
567
# File 'lib/milk_tea/tooling/linter/rules.rb', line 565

def parameter_passing_mode(parameter)
  parameter.respond_to?(:passing_mode) ? parameter.passing_mode : parameter.mode
end

#pointer_like_cast_expression(expression) ⇒ Object



487
488
489
490
491
492
493
494
495
# File 'lib/milk_tea/tooling/linter/rules.rb', line 487

def pointer_like_cast_expression(expression)
  return unless expression.is_a?(AST::PrefixCast)

  target_type = expression.target_type
  return unless target_type.is_a?(AST::TypeRef)
  return unless pointer_like_type_ref?(target_type)

  { target_type:, source: expression.expression }
end

#pointer_like_type_ref?(type_ref) ⇒ Boolean

Returns:

  • (Boolean)


497
498
499
500
501
# File 'lib/milk_tea/tooling/linter/rules.rb', line 497

def pointer_like_type_ref?(type_ref)
  return false if type_ref.nullable

  %w[ptr const_ptr ref].include?(type_ref.name.to_s)
end

#prefer_else_candidate(stmts, index, expected_kind:) ⇒ Object



424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
# File 'lib/milk_tea/tooling/linter/rules.rb', line 424

def prefer_else_candidate(stmts, index, expected_kind:)
  declaration = stmts[index]
  if_stmt = stmts[index + 1]
  return unless declaration.is_a?(AST::LocalDecl)
  return unless declaration.kind == expected_kind
  return if declaration.type || declaration.else_binding || declaration.else_body || declaration.recovered_else
  return unless declaration.value && declaration.name
  return if ignored_binding_name?(declaration.name)
  return unless if_stmt.is_a?(AST::IfStmt)
  return unless if_stmt.else_body.nil? && if_stmt.branches.length == 1

  branch = if_stmt.branches.first
  identifier = null_equality_identifier(branch.condition)
  return unless identifier&.name == declaration.name
  return unless nullable_binding_declaration?(declaration)
  return if expected_kind == :var && !prefer_var_else_binding_mutated?(declaration)
  return unless ControlFlow::Termination.block_always_terminates?(branch.body, ignore_name: method(:ignored_binding_name?), binding_resolution: cfg_binding_resolution)

  { declaration:, if_stmt:, branch: }
end

#prefer_var_else_binding_mutated?(declaration) ⇒ Boolean

Returns:

  • (Boolean)


445
446
447
448
# File 'lib/milk_tea/tooling/linter/rules.rb', line 445

def prefer_var_else_binding_mutated?(declaration)
  binding = @scopes.last[declaration.name]
  binding&.mutated == true
end

#redundant_bool_compare_length(expr) ⇒ Object



274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# File 'lib/milk_tea/tooling/linter/rules.rb', line 274

def redundant_bool_compare_length(expr)
  length = expression_length(expr)
  return length if length

  line = expression_line(expr)
  column = expression_column(expr)
  return nil unless line && column

  text = source_code_line(line)
  return nil unless text

  snippet = text[(column - 1)..]
  return nil unless snippet

  right_literal = snippet.match(/\A\s*(.+?)\s*(==|!=)\s*(true|false)\b/)
  return right_literal[0].rstrip.length if right_literal

  left_literal = snippet.match(/\A\s*(true|false)\s*(==|!=)\s*(.+?)(?=\s*(?::|\)|,|and\b|or\b|$))/)
  return left_literal[0].rstrip.length if left_literal

  nil
end

#resolve_directional_ffi_call(callee) ⇒ Object



528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
# File 'lib/milk_tea/tooling/linter/rules.rb', line 528

def resolve_directional_ffi_call(callee)
  case callee
  when AST::Specialization
    resolve_directional_ffi_call(callee.callee)
  when AST::Identifier
    if @sema_facts && (binding = @sema_facts.functions[callee.name]) && directional_ffi_binding?(binding)
      return { name: binding.name, params: binding.type.params }
    end

    if (declaration = @declared_directional_functions[callee.name])
      return { name: declaration.name, params: declaration.params }
    end

    nil
  when AST::MemberAccess
    return nil unless callee.receiver.is_a?(AST::Identifier)
    return nil unless @sema_facts

    imported_module = @sema_facts.imports[callee.receiver.name]
    return nil unless imported_module

    binding = imported_module.functions[callee.member]
    return nil unless directional_ffi_binding?(binding)

    { name: binding.name, params: binding.type.params }
  else
    nil
  end
end

#static_assert_false?(expression) ⇒ Boolean

Returns:

  • (Boolean)


116
117
118
119
120
121
122
# File 'lib/milk_tea/tooling/linter/rules.rb', line 116

def static_assert_false?(expression)
  return false unless expression.callee.is_a?(AST::Identifier)
  return false unless expression.callee.name == "static_assert"

  first_arg = expression.arguments.first
  first_arg.is_a?(AST::BooleanLiteral) && first_arg.value == false
end

#terminating_callee?(callee) ⇒ Boolean

Returns:

  • (Boolean)


124
125
126
127
128
129
130
131
132
133
# File 'lib/milk_tea/tooling/linter/rules.rb', line 124

def terminating_callee?(callee)
  case callee
  when AST::Identifier
    callee.name == "fatal"
  when AST::Specialization
    terminating_callee?(callee.callee)
  else
    false
  end
end

#terminating_expression?(expression) ⇒ Boolean

Returns:

  • (Boolean)


105
106
107
108
109
110
111
112
113
114
# File 'lib/milk_tea/tooling/linter/rules.rb', line 105

def terminating_expression?(expression)
  case expression
  when AST::Call
    terminating_callee?(expression.callee) || static_assert_false?(expression)
  when AST::Specialization
    terminating_callee?(expression.callee) || static_assert_false?(expression)
  else
    false
  end
end

#void_return_type?(return_type) ⇒ Boolean

Returns:

  • (Boolean)


44
45
46
47
48
49
50
51
52
53
54
# File 'lib/milk_tea/tooling/linter/rules.rb', line 44

def void_return_type?(return_type)
  case return_type
  when AST::TypeRef
    name = return_type.name
    name.is_a?(AST::QualifiedName) && name.parts == ["void"]
  when Types::Primitive
    return_type.name == "void"
  else
    false
  end
end