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

streetsidesoftware / cspell / 23402256720

22 Mar 2026 11:39AM UTC coverage: 93.023% (+0.1%) from 92.904%
23402256720

Pull #8680

github

web-flow
Merge d765d4168 into fc7b60b0f
Pull Request #8680: fix: make flatpack diff friendly

9587 of 11401 branches covered (84.09%)

1272 of 1309 new or added lines in 17 files covered. (97.17%)

8 existing lines in 3 files now uncovered.

19094 of 20526 relevant lines covered (93.02%)

30241.67 hits per line

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

92.11
/packages/flatpack-json/src/stringTable.mts
1
import assert from 'node:assert';
2

3
import { ElementType, type StringTableElement, type StringTableEntry } from './types.mjs';
4

5
export class StringTable {
6
    constructor(readonly stringTableElement: StringTableElement) {}
1,820✔
7

8
    get(index: number): string | undefined {
9
        if (!index) return '';
133,668!
10
        index = index < 0 ? -index : index;
133,668!
11
        if (index >= this.stringTableElement.length) return undefined;
133,668!
12
        return this.#getCompoundString(index);
133,668✔
13
    }
14

15
    *entries(): Iterable<[number, string]> {
16
        for (let i = 1; i < this.stringTableElement.length; i++) {
1,451✔
17
            yield [i, this.#getCompoundString(i)];
25,280✔
18
        }
19
    }
20

21
    *values(): Iterable<string> {
22
        for (const entry of this.entries()) {
4✔
23
            yield entry[1];
26✔
24
        }
25
    }
26

27
    get size(): number {
NEW
28
        return this.stringTableElement.length;
×
29
    }
30

31
    #getCompoundString(index: number, visited = new Set<number>()): string {
158,948✔
32
        if (visited.has(index)) {
159,003✔
33
            throw new Error(`Circular reference in string table at index ${index}`);
1✔
34
        }
35
        const entry = this.stringTableElement[index];
159,002✔
36
        if (typeof entry === 'string') {
159,002✔
37
            return entry;
158,984✔
38
        }
39
        if (Array.isArray(entry)) {
18!
40
            visited.add(index);
18✔
41
            const value = entry.map((i) => this.#getCompoundString(i, visited)).join('');
55✔
42
            visited.delete(index);
17✔
43
            return value;
17✔
44
        }
NEW
45
        throw new Error(`Invalid string table entry at index ${index}`);
×
46
    }
47
}
48

49
interface BuilderEntry {
50
    value: string;
51
    entry: StringTableEntry;
52
    refCount: number;
53
}
54

55
const tokenRegex = /\w+/g;
7✔
56

57
export class StringTableBuilder {
58
    splitStrings: boolean = false;
1,468✔
59
    #stringToIndex = new Map<string, number>();
1,468✔
60
    #entries: BuilderEntry[] = [{ value: '', entry: '', refCount: 0 }];
1,468✔
61
    #availableIndexes: number[] = [];
1,468✔
62
    tokenRegex: RegExp = tokenRegex;
1,468✔
63
    #splitIntoTokens: boolean = false;
1,468✔
64

65
    constructor(stringTableElement?: StringTableElement) {
66
        if (!stringTableElement) return;
1,468✔
67

68
        const st = new StringTable(stringTableElement);
1,445✔
69
        for (const [idx, value] of st.entries()) {
1,445✔
70
            if (!idx) continue;
25,250!
71
            const entry = stringTableElement[idx] as StringTableEntry;
25,250✔
72
            this.#entries[idx] = { value, entry, refCount: 0 };
25,250✔
73
            if (Array.isArray(entry) && !entry.length) {
25,250!
NEW
74
                this.#availableIndexes.push(idx);
×
NEW
75
                continue;
×
76
            }
77
            if (!this.#stringToIndex.has(value)) {
25,250!
78
                this.#stringToIndex.set(value, idx);
25,250✔
79
            }
80
        }
81
    }
82

83
    set splitIntoTokensWhenAdding(value: boolean) {
NEW
84
        this.#splitIntoTokens = value;
×
85
    }
86

87
    get splitIntoTokensWhenAdding(): boolean {
NEW
88
        return this.#splitIntoTokens;
×
89
    }
90

91
    add(str: string): number {
92
        const found = this.#stringToIndex.get(str);
159,403✔
93
        if (found !== undefined) {
159,403✔
94
            const entry = this.#entries[found];
124,758✔
95
            entry.refCount++;
124,758✔
96
            return found;
124,758✔
97
        }
98
        str ||= '';
34,645✔
99
        return this.#append(str);
34,645✔
100
    }
101

102
    getIndex(str: string): number | undefined {
103
        return this.#stringToIndex.get(str);
2✔
104
    }
105

106
    get(index: number): string | undefined {
107
        const entry = this.#getEntry(index);
4✔
108
        return entry?.value;
4✔
109
    }
110

111
    /**
112
     * Increments the reference count for the given index.
113
     * @param index - The index of the string in the string table. The absolute value is used.
114
     * @returns the new reference count for the string at the given index.
115
     */
116
    addRef(index: number): number {
117
        const entry = this.#getEntryCheckBounds(index);
65,748✔
118
        const count = ++entry.refCount;
65,748✔
119
        if (count === 1 && Array.isArray(entry.entry)) {
65,748✔
120
            entry.entry.forEach((i) => this.addRef(i));
3✔
121
        }
122
        return count;
65,748✔
123
    }
124

125
    getRefCount(index: number): number {
126
        const entry = this.#getEntryCheckBounds(index);
2✔
127
        return entry.refCount;
2✔
128
    }
129

130
    #getEntry(index: number): BuilderEntry | undefined {
131
        index = index < 0 ? -index : index;
65,754!
132
        return this.#entries[index];
65,754✔
133
    }
134

135
    #getEntryCheckBounds(index: number): BuilderEntry {
136
        const entry = this.#getEntry(index);
65,750✔
137
        if (!entry) {
65,750!
NEW
138
            throw new Error(`Invalid string table index: ${index}`);
×
139
        }
140
        return entry;
65,750✔
141
    }
142

143
    #append(str: string): number {
144
        const found = this.#stringToIndex.get(str);
34,645✔
145
        if (found !== undefined) {
34,645!
NEW
146
            return found;
×
147
        }
148
        const entry: BuilderEntry = { value: str, entry: str, refCount: 1 };
34,645✔
149
        const idx = this.#availableIndexes.shift() ?? this.#entries.length;
34,645✔
150
        this.#entries[idx] = entry;
34,645✔
151
        this.#stringToIndex.set(str, idx);
34,645✔
152
        if (this.#splitIntoTokens) {
34,645!
NEW
153
            this.#splitEntryIntoTokens(entry);
×
154
        }
155
        return idx;
34,645✔
156
    }
157

158
    #splitEntryIntoTokens(entry: BuilderEntry): void {
159
        if (Array.isArray(entry.entry)) return;
12✔
160
        if (!entry.value) return;
11✔
161
        const regex = new RegExp(this.tokenRegex);
10✔
162
        const indexes: number[] = [...entry.value.matchAll(regex)].flatMap((match) => [
12✔
163
            match.index,
164
            match.index + match[0].length,
165
        ]);
166
        if (!indexes.length) return;
10✔
167
        if (indexes.length === 2 && indexes[0] === 0 && indexes[1] === entry.value.length) {
9✔
168
            return;
6✔
169
        }
170
        const subEntries: number[] = [];
3✔
171
        entry.entry = subEntries;
3✔
172
        indexes.push(entry.value.length);
3✔
173
        let lastIndex = 0;
3✔
174
        for (const index of indexes) {
3✔
175
            if (index === lastIndex) continue;
15✔
176
            const value = entry.value.slice(lastIndex, index);
9✔
177
            subEntries.push(this.add(value));
9✔
178
            lastIndex = index;
9✔
179
        }
180
    }
181

182
    clearUnusedEntries(): void {
183
        for (let i = 1; i < this.#entries.length; i++) {
2✔
184
            const entry = this.#entries[i];
10✔
185
            if (entry.refCount > 0) continue;
10✔
186
            if (this.#stringToIndex.get(entry.value) === i) {
4!
187
                this.#stringToIndex.delete(entry.value);
4✔
188
            }
189
            this.#entries[i] = { value: '', entry: [], refCount: 0 };
4✔
190
            this.#availableIndexes.push(i);
4✔
191
        }
192
    }
193

194
    tokenizeAllEntries(): void {
195
        for (const entry of this.#entries) {
1✔
196
            this.#splitEntryIntoTokens(entry);
12✔
197
        }
198
    }
199

200
    /**
201
     * Sorts the entries in the string table by reference count, with the most referenced strings first.
202
     * This can help reduce the size of the string table when serialized, as more frequently used strings
203
     * will have smaller indexes.
204
     * @returns a map of old indexes to new indexes after sorting. The index 0 is always mapped to itself.
205
     */
206
    sortEntriesByRefCount(): Map<number, number> {
207
        const mapEntryToOldIndex = new Map<BuilderEntry, number>(this.#entries.map((entry, index) => [entry, index]));
16,591✔
208

209
        const entry0 = this.#entries[0];
58✔
210
        const sorted = this.#entries.sort((a, b) =>
58✔
211
            a === entry0 ? -1 : b === entry0 ? 1 : b.refCount - a.refCount || getOldIndex(a) - getOldIndex(b),
90,289!
212
        );
213

214
        const oldIndexToNew = new Map<number, number>(sorted.map((entry, index) => [getOldIndex(entry), index]));
16,591✔
215

216
        for (const entry of this.#entries) {
58✔
217
            if (!Array.isArray(entry.entry)) continue;
16,591✔
218
            entry.entry = entry.entry.map((i) => oldIndexToNew.get(i) ?? i);
15!
219
        }
220

221
        for (const [str, oldIdx] of this.#stringToIndex.entries()) {
58✔
222
            this.#stringToIndex.set(str, oldIndexToNew.get(oldIdx) ?? oldIdx);
16,533!
223
        }
224

225
        return oldIndexToNew;
58✔
226

227
        function getOldIndex(entry: BuilderEntry): number {
228
            const oldIndex = mapEntryToOldIndex.get(entry);
108,741✔
229
            assert(oldIndex !== undefined, 'Entry not found in map');
108,741✔
230
            return oldIndex;
108,741✔
231
        }
232
    }
233

234
    build(): StringTableElement {
235
        return [ElementType.StringTable, ...this.#entries.slice(1).map((e) => e.entry)];
58,336✔
236
    }
237
}
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