aboutsummaryrefslogtreecommitdiffhomepage
path: root/scripts/linters/no_banned_use.rb
blob: 341a2b93927192edc235cba6f12ee93bf8827c7b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
BANNED_USE_PATHS = %w[
  anyhow::Result
].freeze

def no_banned_use(root_dir, excludes = [])
  pattern = root_dir.join('crates', '**', '*.rs').to_s
  errors = Dir.glob(pattern).sort.flat_map do |path|
    relative = Pathname.new(path).relative_path_from(root_dir).to_s
    next [] if excludes.include?(relative)

    find_banned_uses(path, relative)
  end

  return true if errors.empty?

  puts 'Found banned `use` imports.'
  puts 'These items must always be referenced by their fully-qualified path:'
  errors.each do |err|
    puts "  #{err}"
  end
  false
end

BANNED_USE_START_RE = /\A(?:pub(?:\([^)]*\))?\s+)?use\b/

def find_banned_uses(path, relative)
  errors = []
  lines = File.readlines(path)
  buffer = nil
  start_idx = nil

  lines.each_with_index do |raw, idx|
    code = raw.split('//', 2).first || raw
    stripped = code.strip

    if buffer.nil?
      next unless stripped =~ BANNED_USE_START_RE

      buffer = +''
      start_idx = idx
    end

    buffer << ' ' << stripped
    next unless buffer.include?(';')

    tree = buffer[/\buse\s+(.*?);/m, 1]
    expand_use_tree(tree).each do |full|
      next unless BANNED_USE_PATHS.include?(full)

      errors << "#{relative}:#{start_idx + 1}: `use #{full}` is banned (fully qualify as `#{full}` instead)"
    end

    buffer = nil
  end

  errors.uniq
end

def expand_use_tree(tree)
  return [] if tree.nil?

  tree = tree.strip
  brace = tree.index('{')

  if brace.nil?
    return [strip_use_alias(tree)].reject(&:empty?)
  end

  prefix = tree[0...brace].sub(/::\s*\z/, '').strip
  inner = tree[(brace + 1)..].sub(/\}\s*\z/, '')

  split_top_level(inner).flat_map do |child|
    expand_use_tree(child).map do |sub|
      if sub.empty? || sub == 'self'
        prefix
      elsif prefix.empty?
        sub
      else
        "#{prefix}::#{sub}"
      end
    end
  end
end

def strip_use_alias(segment)
  segment.sub(/\s+as\s+\S+\s*\z/, '').strip
end

def split_top_level(str)
  parts = []
  current = +''
  depth = 0

  str.each_char do |ch|
    case ch
    when '{' then depth += 1; current << ch
    when '}' then depth -= 1; current << ch
    when ','
      if depth.zero?
        parts << current
        current = +''
      else
        current << ch
      end
    else
      current << ch
    end
  end
  parts << current

  parts.map(&:strip).reject(&:empty?)
end