• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ota-meshi / astro-eslint-parser / 27524083055

15 Jun 2026 04:29AM UTC coverage: 81.263% (-0.5%) from 81.805%
27524083055

Pull #435

github

web-flow
Merge fac292131 into ce311c59d
Pull Request #435: feat!: use `@astrojs/compiler-rs` instead of `@astrojs/compiler`

647 of 869 branches covered (74.45%)

Branch coverage included in aggregate %.

349 of 397 new or added lines in 9 files covered. (87.91%)

9 existing lines in 4 files now uncovered.

1257 of 1474 relevant lines covered (85.28%)

52184.39 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.85
/src/parser/astro-parser/parse.ts
1
import type { ParseResult } from "../../astro/types";
600!
2
import type { Context } from "../../context";
3
import { parse as parseAstro } from "@astrojs/compiler-rs";
1✔
4
import { ParseError } from "../../errors";
1✔
5

6
/**
7
 * Parse code by `@astrojs/compiler-rs`.
8
 *
9
 * The compiler returns locations in its own offset format. Normalize them here
10
 * before any later parser phase reads the AST, so the rest of the parser can
11
 * treat every `start`/`end` and diagnostic label as ESLint-compatible source
12
 * indexes.
13
 */
14
export function parse(code: string, ctx: Context): ParseResult {
1✔
15
  // @astrojs/compiler-rs currently types `ast` as `Record<string, any>`,
16
  // although the synchronous parser returns an AstroRoot tree. Keep the
17
  // assertion at this boundary so the rest of the parser can use the detailed
18
  // compiler node types from `types.ts`.
19
  const result = parseAstro(code) as ParseResult;
288✔
20
  normalizeLocations(result, code, ctx);
288✔
21

22
  for (const diagnostic of result.diagnostics || []) {
288!
23
    if (diagnostic.severity !== "error") {
1!
NEW
24
      continue;
×
25
    }
26
    ctx.originalAST = result.ast;
1✔
27
    const location = diagnostic.labels?.[0]?.start ?? 0;
1!
28
    throw new ParseError(diagnostic.text, location, ctx);
1✔
29
  }
30
  return result;
287✔
31
}
32

33
/**
34
 * Normalize compiler byte offsets to JavaScript string indices.
35
 *
36
 * `@astrojs/compiler-rs` reports `start`/`end` as UTF-8 byte offsets. ESLint
37
 * `range`, `Context`, and JavaScript string slicing all use UTF-16 code-unit
38
 * indexes. Those values are the same for ASCII, but diverge as soon as a
39
 * multibyte character appears before or inside a node. For example, the raw
40
 * compiler offset after `"あ"` is 3, while the JavaScript index is 1.
41
 *
42
 * Keep this conversion in the parse phase so downstream code does not need to
43
 * know which compiler produced the AST or remember to remap every lookup.
44
 */
45
function normalizeLocations(
1✔
46
  result: ParseResult,
47
  code: string,
48
  ctx: Context,
49
): void {
50
  const byteOffsetToIndex = buildByteOffsetToIndexMap(code);
288✔
51
  remapNodeLocations(result.ast, byteOffsetToIndex);
288✔
52
  for (const diagnostic of result.diagnostics || []) {
288!
53
    for (const label of diagnostic.labels || []) {
1!
54
      label.start = byteOffsetToIndex(label.start);
1✔
55
      label.end = byteOffsetToIndex(label.end);
1✔
56
      // Compiler diagnostics expose line/column too, but the column follows
57
      // the compiler's character counting. Recompute it from the normalized
58
      // index so errors and AST ranges use the same coordinate system.
59
      const loc = ctx.getLocFromIndex(label.start);
1✔
60
      label.line = loc.line;
1✔
61
      label.column = loc.column;
1✔
62
    }
63
  }
64
}
65

66
/**
67
 * Remap start/end properties on a compiler AST subtree.
68
 *
69
 * The compiler AST contains ESTree-compatible JavaScript nodes nested inside
70
 * Astro nodes. Walking generically keeps the location fix independent from the
71
 * exact node shape and prevents future compiler node additions from bypassing
72
 * the normalization.
73
 */
74
function remapNodeLocations(
1✔
75
  value: unknown,
76
  byteOffsetToIndex: (offset: number) => number,
77
  seen = new Set<object>(),
288✔
78
): void {
79
  if (!value || typeof value !== "object") {
793,462✔
80
    return;
629,562✔
81
  }
82
  if (seen.has(value)) {
163,900!
UNCOV
83
    return;
×
84
  }
85
  seen.add(value);
163,900✔
86

87
  if (Array.isArray(value)) {
163,900✔
88
    for (const child of value) {
45,558✔
89
      remapNodeLocations(child, byteOffsetToIndex, seen);
40,120✔
90
    }
91
    return;
45,558✔
92
  }
93

94
  const node = value as Record<string, unknown>;
118,342✔
95
  if (typeof node.start === "number") {
118,342✔
96
    node.start = byteOffsetToIndex(node.start);
117,904✔
97
  }
98
  if (typeof node.end === "number") {
118,342✔
99
    node.end = byteOffsetToIndex(node.end);
117,904✔
100
  }
101

102
  for (const child of Object.values(node)) {
118,342✔
103
    remapNodeLocations(child, byteOffsetToIndex, seen);
753,054✔
104
  }
105
}
106

107
/**
108
 * Build a mapper from compiler byte offsets to JavaScript string indices.
109
 *
110
 * The table records both coordinates at each source character boundary. The
111
 * returned function can then translate any compiler offset with a binary
112
 * search. If the offset falls between boundaries, it returns the previous
113
 * JavaScript index; this keeps error locations stable even if the compiler
114
 * points into a multibyte character boundary.
115
 */
116
function buildByteOffsetToIndexMap(source: string): (offset: number) => number {
1✔
117
  const byteOffsets: number[] = [0];
288✔
118
  const codeUnitOffsets: number[] = [0];
288✔
119
  let byteOffset = 0;
288✔
120

121
  for (let index = 0; index < source.length; ) {
288✔
122
    const codePoint = source.codePointAt(index)!;
2,416,180✔
123
    const codeUnitLength = codePoint > 0xffff ? 2 : 1;
2,416,180✔
124
    const nextIndex = index + codeUnitLength;
2,416,180✔
125
    byteOffset += getUTF8ByteLength(codePoint);
2,416,180✔
126
    byteOffsets.push(byteOffset);
2,416,180✔
127
    codeUnitOffsets.push(nextIndex);
2,416,180✔
128
    index = nextIndex;
2,416,180✔
129
  }
130

131
  return (offset: number) => {
288✔
132
    let low = 0;
235,810✔
133
    let high = byteOffsets.length - 1;
235,810✔
134
    while (low <= high) {
235,810✔
135
      const mid = Math.floor((low + high) / 2);
4,115,327✔
136
      const value = byteOffsets[mid];
4,115,327✔
137
      if (value === offset) {
4,115,327✔
138
        return codeUnitOffsets[mid];
235,810✔
139
      }
140
      if (value < offset) {
3,879,517✔
141
        low = mid + 1;
2,151,640✔
142
      } else {
143
        high = mid - 1;
1,727,877✔
144
      }
145
    }
NEW
146
    return codeUnitOffsets[Math.max(high, 0)] ?? 0;
×
147
  };
148
}
149

150
/** Get the UTF-8 byte length for a Unicode code point. */
151
function getUTF8ByteLength(codePoint: number): number {
2!
152
  if (codePoint <= 0x7f) {
2,416,180✔
153
    return 1;
2,415,931✔
154
  }
155
  if (codePoint <= 0x7ff) {
249✔
156
    return 2;
108✔
157
  }
158
  if (codePoint <= 0xffff) {
141✔
159
    return 3;
135✔
160
  }
161
  return 4;
6✔
162
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc