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

#assertion_false?(expression) ⇒ Boolean

Returns:

  • (Boolean)


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

def assertion_false?(expression)
  return false unless expression.is_a?(AST::Call)

  callee = expression.callee
  return false unless callee.is_a?(AST::Identifier)
  return false unless %w[assert expect].include?(callee.name)

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

#binding_type_for_declaration(statement) ⇒ Object



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

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 ──────────────────────────────────────────────



515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
# File 'lib/milk_tea/tooling/linter/rules.rb', line 515

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



308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
# File 'lib/milk_tea/tooling/linter/rules.rb', line 308

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



361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
# File 'lib/milk_tea/tooling/linter/rules.rb', line 361

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



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

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.



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/milk_tea/tooling/linter/rules.rb', line 148

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 ────────────────────────────────────────────────────



219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/milk_tea/tooling/linter/rules.rb', line 219

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 ────────────────────────────────────────────────────



238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
# File 'lib/milk_tea/tooling/linter/rules.rb', line 238

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



176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/milk_tea/tooling/linter/rules.rb', line 176

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)


202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
# File 'lib/milk_tea/tooling/linter/rules.rb', line 202

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)


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

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



410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
# File 'lib/milk_tea/tooling/linter/rules.rb', line 410

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_inline_methods_warnings(source_file) ⇒ Object

── prefer-inline-methods ────────────────────────────────────────────── Flag extending X: blocks whose receiver struct is declared in the same file, suggesting the methods be written inline inside the struct body (which desugars to the identical extending block).



605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
# File 'lib/milk_tea/tooling/linter/rules.rb', line 605

def emit_prefer_inline_methods_warnings(source_file)
  struct_names = source_file.declarations.filter_map { |decl| decl.name if decl.is_a?(AST::StructDecl) }.to_set
  return if struct_names.empty?

  source_file.declarations.each do |declaration|
    next unless declaration.is_a?(AST::ExtendingBlock)
    next if declaration.inline

    parts = declaration.type_name.name.parts
    next unless parts.length == 1
    next if declaration.type_name.arguments.any?

    name = parts.first
    next unless struct_names.include?(name)

    @warnings << Warning.new(
      path: @path,
      line: declaration.line,
      column: declaration.column,
      length: name.length,
      code: "prefer-inline-methods",
      message: "methods on '#{name}' can be written inline inside the struct declaration",
      severity: :hint,
      symbol_name: name,
    )
  end
end

#emit_prefer_let_else_warnings(stmts) ⇒ Object

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



402
403
404
# File 'lib/milk_tea/tooling/linter/rules.rb', line 402

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



406
407
408
# File 'lib/milk_tea/tooling/linter/rules.rb', line 406

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



334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
# File 'lib/milk_tea/tooling/linter/rules.rb', line 334

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)


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

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

#legacy_directional_argument_expression?(expression) ⇒ Boolean

Returns:

  • (Boolean)


584
585
586
587
588
589
590
591
592
593
594
595
# File 'lib/milk_tea/tooling/linter/rules.rb', line 584

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)


597
598
599
# File 'lib/milk_tea/tooling/linter/rules.rb', line 597

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



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

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



461
462
463
464
465
466
467
468
469
# File 'lib/milk_tea/tooling/linter/rules.rb', line 461

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)


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

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)


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

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)


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

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



580
581
582
# File 'lib/milk_tea/tooling/linter/rules.rb', line 580

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

#parameter_passing_mode(parameter) ⇒ Object



576
577
578
# File 'lib/milk_tea/tooling/linter/rules.rb', line 576

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

#pointer_like_cast_expression(expression) ⇒ Object



498
499
500
501
502
503
504
505
506
# File 'lib/milk_tea/tooling/linter/rules.rb', line 498

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)


508
509
510
511
512
# File 'lib/milk_tea/tooling/linter/rules.rb', line 508

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



435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
# File 'lib/milk_tea/tooling/linter/rules.rb', line 435

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)


456
457
458
459
# File 'lib/milk_tea/tooling/linter/rules.rb', line 456

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

#redundant_bool_compare_length(expr) ⇒ Object



285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
# File 'lib/milk_tea/tooling/linter/rules.rb', line 285

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



539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
# File 'lib/milk_tea/tooling/linter/rules.rb', line 539

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)


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

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)


135
136
137
138
139
140
141
142
143
144
# File 'lib/milk_tea/tooling/linter/rules.rb', line 135

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) || assertion_false?(expression)
  when AST::Specialization
    terminating_callee?(expression.callee) || static_assert_false?(expression) || assertion_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