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_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).



594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
# File 'lib/milk_tea/tooling/linter/rules.rb', line 594

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



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