aboutsummaryrefslogtreecommitdiff
path: root/Zig.novaextension/Scripts/normalize-zig-issues.pl
blob: 17620a0de76c194e1eb488cb771ae9e550638963 (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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
use strict;
use warnings;
$| = 1;

my $base = $ENV{NOVA_ZIG_TASK_CWD} || $ENV{PWD} || "";
my %file_cache = ();
my @collected_issues = ();

sub normalize_path {
  my ($path) = @_;
  return $path if $path =~ m{^/} || $path =~ m{^[A-Za-z]:[\/\\]};

  # The Zig compiler occasionally emits paths without a leading slash (e.g.
  # "Users/..." instead of "/Users/...") when traversing relative directories.
  # Detect known top-level directory names and restore the leading slash.
  if ($path =~ m{^(?:Users|private|opt|Library|System|usr|var|tmp|etc|home)/}) {
    return "/$path";
  }

  return $base eq "" ? $path : "$base/$path";
}

sub source_line_for_path {
  my ($path, $line_number) = @_;
  return undef if $line_number < 1;

  if (!exists $file_cache{$path}) {
    if (open my $fh, "<", $path) {
      my @lines = <$fh>;
      close $fh;
      $file_cache{$path} = \@lines;
    } else {
      $file_cache{$path} = undef;
    }
  }

  my $lines = $file_cache{$path};
  return undef if !defined $lines;
  return undef if $line_number > scalar(@$lines);

  my $line = $lines->[$line_number - 1];
  $line =~ s/\r?\n$//;
  return $line;
}

# Zig reports argument-count errors at the column of the opening parenthesis,
# which causes Nova to underline the entire argument list. For method-call
# expressions (receiver.method(...)), shift the column back to the start of
# the callee identifier so only the call site itself is highlighted.
sub adjusted_call_column {
  my ($source_line, $column) = @_;
  return $column if !defined $source_line || $column < 1;

  my $column_index = $column - 1;
  my $open_paren = index($source_line, "(", $column_index);
  return $column if $open_paren < 0;

  my $prefix = substr($source_line, 0, $open_paren);
  return $column if $prefix !~ /([A-Za-z_][A-Za-z0-9_]*)\s*$/;

  my $callee = $1;
  my $callee_index = rindex($prefix, $callee);
  return $column if $callee_index < 0;

  my $target_column = $callee_index + 1;
  return $column if $target_column <= $column;

  my $between = substr($source_line, $column_index, $target_column - $column);
  return $column if $between !~ /\./;

  return $target_column;
}

while (my $line = <STDIN>) {
  my $newline = $line =~ s/\r?\n$// ? "\n" : "";

  if ($line =~ m{^([^:\n]+\.zig):(\d+):(\d+):\s*(error|warning|note):\s*(.*)$}) {
    my ($path, $line_number, $column, $severity, $message) = ($1, $2, $3, $4, $5);
    my $normalized = normalize_path($path);

    if ($message =~ /\bexpected\b.*argument\(s\).*\bfound\b/i) {
      my $source_line = source_line_for_path($normalized, $line_number);
      $column = adjusted_call_column($source_line, $column);
    }

    $line = "$normalized:$line_number:$column: $severity: $message";

    if ($severity eq "error" || $severity eq "warning" || $severity eq "note") {
      push @collected_issues, {
        file     => $normalized,
        line     => int($line_number),
        column   => int($column),
        severity => $severity,
        message  => $message,
      };
    }
  } elsif ($line =~ m{^([^:\n]+\.zig):(\d+):(\d+):}) {
    my ($path, $line_number, $column) = ($1, $2, $3);
    my $normalized = normalize_path($path);
    $line =~ s{^[^:\n]+\.zig:\d+:\d+:}{$normalized:$line_number:$column:};
  }

  print $line, $newline;
}

END {
  my $issue_file = $ENV{NOVA_ZIG_ISSUE_FILE} or return;
  sub _json_str {
    my ($s) = @_;
    $s =~ s/\\/\\\\/g;
    $s =~ s/"/\\"/g;
    $s =~ s/\n/\\n/g;
    $s =~ s/\r/\\r/g;
    $s =~ s/\t/\\t/g;
    return qq("$s");
  }
  if (open my $fh, ">", $issue_file) {
    print $fh "[";
    my $first = 1;
    for my $issue (@collected_issues) {
      print $fh "," unless $first;
      $first = 0;
      printf $fh '{%s,%s,%s,%s,%s}',
        '"file":'     . _json_str($issue->{file}),
        '"line":'     . $issue->{line},
        '"column":'   . $issue->{column},
        '"severity":' . _json_str($issue->{severity}),
        '"message":'  . _json_str($issue->{message});
    }
    print $fh "]";
    close $fh;
  }
}