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

streetsidesoftware / cspell / 23445217120

23 Mar 2026 03:25PM UTC coverage: 93.06%. First build
23445217120

Pull #8680

github

web-flow
Merge f867167a4 into 72ff88b0a
Pull Request #8680: fix: make flatpack diff friendly

9631 of 11447 branches covered (84.14%)

1340 of 1373 new or added lines in 18 files covered. (97.6%)

19161 of 20590 relevant lines covered (93.06%)

29921.69 hits per line

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

93.97
/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) {}
2,287✔
7

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

15
    *entries(): Iterable<[number, string]> {
16
        for (let i = 1; i < this.stringTableElement.length; i++) {
1,769✔
17
            yield [i, this.#getCompoundString(i)];
28,556✔
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 {
166,844✔
32
        if (visited.has(index)) {
166,899✔
33
            throw new Error(`Circular reference in string table at index ${index}`);
1✔
34
        }
35
        const entry = this.stringTableElement[index];
166,898✔
36
        if (typeof entry === 'string') {
166,898✔
37
            return entry;
166,832✔
38
        }
39
        if (Array.isArray(entry)) {
66!
40
            visited.add(index);
66✔
41
            const value = entry.map((i) => this.#getCompoundString(i, visited)).join('');
66✔
42
            visited.delete(index);
65✔
43
            return value;
65✔
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,785✔
59
    #stringToIndex = new Map<string, number>();
1,785✔
60
    #entries: BuilderEntry[] = [{ value: '', entry: '', refCount: 0 }];
1,785✔
61
    #availableIndexes: number[] = [];
1,785✔
62
    tokenRegex: RegExp = tokenRegex;
1,785✔
63
    #splitIntoTokens: boolean = false;
1,785✔
64

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

68
        const st = new StringTable(stringTableElement);
1,763✔
69
        for (const [idx, value] of st.entries()) {
1,763✔
70
            if (!idx) continue;
28,526!
71
            const entry = stringTableElement[idx] as StringTableEntry;
28,526✔
72
            this.#entries[idx] = { value, entry, refCount: 0 };
28,526✔
73
            if (Array.isArray(entry) && !entry.length) {
28,526✔
74
                this.#availableIndexes.push(idx);
48✔
75
                continue;
48✔
76
            }
77
            if (!this.#stringToIndex.has(value)) {
28,478!
78
                this.#stringToIndex.set(value, idx);
28,478✔
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);
197,893✔
93
        if (found !== undefined) {
197,893✔
94
            const entry = this.#entries[found];
155,644✔
95
            entry.refCount++;
155,644✔
96
            return found;
155,644✔
97
        }
98
        str ||= '';
42,249✔
99
        return this.#append(str);
42,249✔
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);
69,115✔
118
        const count = ++entry.refCount;
69,115✔
119
        if (count === 1 && Array.isArray(entry.entry)) {
69,115✔
120
            entry.entry.forEach((i) => this.addRef(i));
3✔
121
        }
122
        return count;
69,115✔
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;
69,121!
132
        return this.#entries[index];
69,121✔
133
    }
134

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

143
    #append(str: string): number {
144
        const found = this.#stringToIndex.get(str);
42,249✔
145
        if (found !== undefined) {
42,249!
NEW
146
            return found;
×
147
        }
148
        const entry: BuilderEntry = { value: str, entry: str, refCount: 1 };
42,249✔
149
        const idx = this.#availableIndexes.shift() ?? this.#entries.length;
42,249✔
150
        this.#entries[idx] = entry;
42,249✔
151
        this.#stringToIndex.set(str, idx);
42,249✔
152
        if (this.#splitIntoTokens) {
42,249!
NEW
153
            this.#splitEntryIntoTokens(entry);
×
154
        }
155
        return idx;
42,249✔
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(): this {
183
        for (let i = 1; i < this.#entries.length; i++) {
597✔
184
            const entry = this.#entries[i];
44,130✔
185
            if (entry.refCount > 0) continue;
44,130✔
186
            if (this.#stringToIndex.get(entry.value) === i) {
36!
187
                this.#stringToIndex.delete(entry.value);
36✔
188
            }
189
            this.#entries[i] = { value: '', entry: [], refCount: 0 };
36✔
190
            this.#availableIndexes.push(i);
36✔
191
        }
192
        return this;
597✔
193
    }
194

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

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

211
        const entry0 = this.#entries[0];
59✔
212
        const sorted = this.#entries.sort((a, b) =>
59✔
213
            a === entry0 ? -1 : b === entry0 ? 1 : b.refCount - a.refCount || getOldIndex(a) - getOldIndex(b),
95,517!
214
        );
215

216
        const oldIndexToNew = new Map<number, number>(sorted.map((entry, index) => [getOldIndex(entry), index]));
19,838✔
217

218
        for (const entry of this.#entries) {
59✔
219
            if (!Array.isArray(entry.entry)) continue;
19,838✔
220
            entry.entry = entry.entry.map((i) => oldIndexToNew.get(i) ?? i);
15!
221
        }
222

223
        for (const [str, oldIdx] of this.#stringToIndex.entries()) {
59✔
224
            this.#stringToIndex.set(str, oldIndexToNew.get(oldIdx) ?? oldIdx);
19,779!
225
        }
226

227
        return oldIndexToNew;
59✔
228

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

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