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

luttje / glua-api-snippets / 13087830380

01 Feb 2025 10:38AM UTC coverage: 79.295% (-0.06%) from 79.358%
13087830380

Pull #75

github

web-flow
Merge f3b9ea031 into 404b59584
Pull Request #75: Plugin. WIP 1

322 of 402 branches covered (80.1%)

Branch coverage included in aggregate %.

0 of 2 new or added lines in 1 file covered. (0.0%)

1704 of 2153 relevant lines covered (79.15%)

456.3 hits per line

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

86.05
/src/api-writer/glua-api-writer.ts
1
import { ClassFunction, Enum, Function, HookFunction, LibraryFunction, TypePage, Panel, PanelFunction, Realm, Struct, WikiPage, isPanel, FunctionArgument, FunctionCallback } from '../scrapers/wiki-page-markup-scraper.js';
1✔
2
import { escapeSingleQuotes, indentText, putCommentBeforeEachLine, removeNewlines, safeFileName, toLowerCamelCase } from '../utils/string.js';
1✔
3
import {
1✔
4
  isClassFunction,
1✔
5
  isHookFunction,
1✔
6
  isLibraryFunction,
1✔
7
  isLibrary,
1✔
8
  isClass,
1✔
9
  isPanelFunction,
1✔
10
  isStruct,
1✔
11
  isEnum,
1✔
12
} from '../scrapers/wiki-page-markup-scraper.js';
1✔
13
import fs from 'fs';
1✔
14

1✔
15
export const RESERVERD_KEYWORDS = new Set([
1✔
16
  'and',
1✔
17
  'break',
1✔
18
  'continue',
1✔
19
  'do',
1✔
20
  'else',
1✔
21
  'elseif',
1✔
22
  'end',
1✔
23
  'false',
1✔
24
  'for',
1✔
25
  'function',
1✔
26
  'goto',
1✔
27
  'if',
1✔
28
  'in',
1✔
29
  'local',
1✔
30
  'nil',
1✔
31
  'not',
1✔
32
  'or',
1✔
33
  'repeat',
1✔
34
  'return',
1✔
35
  'then',
1✔
36
  'true',
1✔
37
  'until',
1✔
38
  'while'
1✔
39
]);
1✔
40

1✔
41
type IndexedWikiPage = {
1✔
42
  index: number;
1✔
43
  page: WikiPage;
1✔
44
};
1✔
45

1✔
46
export class GluaApiWriter {
1✔
47
  private readonly writtenClasses: Set<string> = new Set();
1✔
48
  private readonly writtenLibraryGlobals: Set<string> = new Set();
1✔
49
  private readonly pageOverrides: Map<string, string> = new Map();
1✔
50

1✔
51
  private readonly files: Map<string, IndexedWikiPage[]> = new Map();
1✔
52

1✔
53
  constructor() { }
1✔
54

1✔
55
  public static safeName(name: string) {
1✔
56
    if (name.includes('/'))
107✔
57
      name = name.replace(/\//g, ' or ');
107✔
58

107✔
59
    if (name.includes('='))
107✔
60
      name = name.split('=')[0];
107✔
61

107✔
62
    if (name.includes(' '))
107✔
63
      name = toLowerCamelCase(name);
107✔
64

107✔
65
    // Remove any remaining characters not valid in a Lua variable/function name.
107✔
66
    name = name.replace(/[^A-Za-z\d_.]/g, '');
107✔
67

107✔
68
    if (RESERVERD_KEYWORDS.has(name))
107✔
69
      return `_${name}`;
107✔
70

84✔
71
    return name;
84✔
72
  }
84✔
73

1✔
74
  public addOverride(pageAddress: string, override: string) {
1✔
75
    this.pageOverrides.set(safeFileName(pageAddress, '.'), override);
2✔
76
  }
2✔
77

1✔
78
  public writePage(page: WikiPage) {
1✔
79
    const fileSafeAddress = safeFileName(page.address, '.');
20✔
80
    if (this.pageOverrides.has(fileSafeAddress)) {
20✔
81
      let api = '';
1✔
82

1✔
83
      if (isClassFunction(page))
1✔
84
        api += this.writeClassStart(page.parent, undefined, undefined, undefined, page.deprecated);
1!
85
      else if (isLibraryFunction(page))
1✔
86
        api += this.writeLibraryGlobalFallback(page);
1!
87

1✔
88
      api += this.pageOverrides.get(fileSafeAddress);
1✔
89

1✔
90
      return `${api}\n\n`;
1✔
91
    } else if (isClassFunction(page))
20✔
92
      return this.writeClassFunction(page);
19!
93
    else if (isLibraryFunction(page))
19✔
94
      return this.writeLibraryFunction(page);
19✔
95
    else if (isHookFunction(page))
6✔
96
      return this.writeHookFunction(page);
6✔
97
    else if (isPanel(page))
5✔
98
      return this.writePanel(page);
5✔
99
    else if (isPanelFunction(page))
4✔
100
      return this.writePanelFunction(page);
4!
101
    else if (isEnum(page))
4✔
102
      return this.writeEnum(page);
4✔
103
    else if (isStruct(page))
2✔
104
      return this.writeStruct(page);
2✔
105
    else if (isLibrary(page))
×
106
      return this.writeLibraryGlobal(page);
×
107
    else if (isClass(page))
×
108
      return this.writeClassGlobal(page);
×
109
  }
20✔
110

1✔
111
  private writeClassStart(className: string, realm?: Realm, url?: string, parent?: string, deprecated?: string, description?: string) {
1✔
112
    let api: string = '';
4✔
113

4✔
114
    if (!this.writtenClasses.has(className)) {
4✔
115
      const classOverride = `class.${className}`;
4✔
116
      if (this.pageOverrides.has(classOverride)) {
4✔
117
        api += this.pageOverrides.get(classOverride)!.replace(/\n$/g, '') + '\n\n';
1✔
118
      } else {
4✔
119
        if (realm) {
3!
120
          api += `---${this.formatRealm(realm)} ${description ? `${putCommentBeforeEachLine(description)}\n` : ''}\n`;
×
121
        } else {
3✔
122
          api += description ? `${putCommentBeforeEachLine(description, false)}\n` : '';
3✔
123
        }
3✔
124

3✔
125
        if (url) {
3✔
126
          api += `---\n---[View wiki](${url})\n`;
1✔
127
        }
1✔
128

3✔
129
        if (deprecated)
3✔
130
          api += `---@deprecated ${removeNewlines(deprecated)}\n`;
3✔
131

3✔
132
        api += `---@class ${className}`;
3✔
133

3✔
134
        if (parent)
3✔
135
          api += ` : ${parent}`;
3✔
136

3✔
137
        api += '\n';
3✔
138

3✔
139
        // for PLAYER, WEAPON, etc. we want to define globals
3✔
140
        if (className !== className.toUpperCase()) api += 'local ';
3✔
141
        api += `${className} = {}\n\n`;
3✔
142
      }
3✔
143

4✔
144
      this.writtenClasses.add(className);
4✔
145
    }
4✔
146

4✔
147
    return api;
4✔
148
  }
4✔
149

1✔
150
  private writeLibraryGlobalFallback(func: LibraryFunction) {
1✔
151
    if (!func.dontDefineParent && !this.writtenLibraryGlobals.has(func.parent)) {
13✔
152
      let api = '';
2✔
153

2✔
154
      api += `--- Missing description.\n`;
2✔
155
      api += `${func.parent} = {}\n\n`;
2✔
156

2✔
157
      this.writtenLibraryGlobals.add(func.parent);
2✔
158

2✔
159
      return api;
2✔
160
    }
2✔
161

11✔
162
    return '';
11✔
163
  }
11✔
164

1✔
165
  private writeLibraryGlobal(page: TypePage) {
1✔
166
    if (!this.writtenLibraryGlobals.has(page.name)) {
×
167
      let api = '';
×
168

×
169
      api += page.description ? `${putCommentBeforeEachLine(page.description.trim(), false)}\n` : '';
×
170

×
171
      if (page.deprecated)
×
172
        api += `---@deprecated ${removeNewlines(page.deprecated)}\n`;
×
173

×
174
      api += `${page.name} = {}\n\n`;
×
175

×
176
      this.writtenLibraryGlobals.add(page.name);
×
177

×
178
      return api;
×
179
    }
×
180

×
181
    return '';
×
182
  }
×
183

1✔
184
  private writeClassGlobal(page: TypePage) {
1✔
185
    return this.writeClassStart(page.name, page.realm, page.url, page.parent, page.deprecated, page.description);
×
186
  }
×
187

1✔
188
  private writeClassFunction(func: ClassFunction) {
1✔
189
    let api: string = this.writeClassStart(func.parent, undefined, undefined, undefined, func.deprecated);
1✔
190

1✔
191
    if (!func.arguments || func.arguments.length === 0) func.arguments = [{}];
1!
192
    for (const argSet of func.arguments) {
1✔
193
      api += this.writeFunctionLuaDocComment(func, argSet.args, func.realm);
1✔
194
      api += this.writeFunctionDeclaration(func, argSet.args, ':');
1✔
195
    }
1✔
196

1✔
197
    return api;
1✔
198
  }
1✔
199

1✔
200
  private writeLibraryFunction(func: LibraryFunction) {
1✔
201
    let api: string = this.writeLibraryGlobalFallback(func);
13✔
202

13✔
203
    if (!func.arguments || func.arguments.length === 0) func.arguments = [{}];
13✔
204
    for (const argSet of func.arguments) {
13✔
205
      api += this.writeFunctionLuaDocComment(func, argSet.args, func.realm);
13✔
206
      api += this.writeFunctionDeclaration(func, argSet.args);
13✔
207
    }
13✔
208

12✔
209
    return api;
12✔
210
  }
12✔
211

1✔
212
  private writeHookFunction(func: HookFunction) {
1✔
213
    return this.writeClassFunction(func);
1✔
214
  }
1✔
215

1✔
216
  private writePanel(panel: Panel) {
1✔
217
    let api: string = this.writeClassStart(panel.name, undefined, undefined, panel.parent, panel.deprecated, panel.description);
1✔
218

1✔
219
    return api;
1✔
220
  }
1✔
221

1✔
222
  private writePanelFunction(func: PanelFunction) {
1✔
223
    let api: string = '';
×
224

×
225
    if (!func.arguments || func.arguments.length === 0) func.arguments = [{}];
×
226
    for (const argSet of func.arguments) {
×
227
      api += this.writeFunctionLuaDocComment(func, argSet.args, func.realm);
×
228
      api += this.writeFunctionDeclaration(func, argSet.args, ':');
×
229
    }
×
230

×
231
    return api;
×
232
  }
×
233

1✔
234
  private writeEnum(_enum: Enum) {
1✔
235
    let api: string = '';
2✔
236

2✔
237
    // If the first key is empty (like SCREENFADE has), check the second key
2✔
238
    const isContainedInTable =
2✔
239
      _enum.items[0]?.key === ''
2!
240
        ? _enum.items[1]?.key.includes('.')
2!
241
        : _enum.items[0]?.key.includes('.');
2!
242

2✔
243
    if (_enum.deprecated)
2✔
244
      api += `---@deprecated ${removeNewlines(_enum.deprecated)}\n`;
2!
245

2✔
246
    if (isContainedInTable) {
2✔
247
      api += `---${this.formatRealm(_enum.realm)} ${_enum.description ? `${putCommentBeforeEachLine(_enum.description.trim())}` : ''}\n`;
1!
248
      api += `---@enum ${_enum.name}\n`;
1✔
249
      api += `${_enum.name} = {\n`;
1✔
250
    }
1✔
251

2✔
252
    const writeItem = (key: string, item: typeof _enum.items[0]) => {
2✔
253
      if (key === '') {
10✔
254
        // Happens for SCREENFADE which has a blank key to describe what 0 does.
1✔
255
        return;
1✔
256
      }
1✔
257

9✔
258
      if (isNaN(Number(item.value.trim()))) {
10✔
259
        // Happens for TODO value in NAV_MESH_BLOCKED_LUA in https://wiki.facepunch.com/gmod/Enums/NAV_MESH
1✔
260
        console.warn(`Enum ${_enum.name} has a TODO value for key ${key}. Skipping.`);
1✔
261
        return;
1✔
262
      }
1✔
263

8✔
264
      if (isContainedInTable) {
10✔
265
        key = key.split('.')[1];
5✔
266

5✔
267
        if (item.description?.trim()) {
5✔
268
          api += `${indentText(putCommentBeforeEachLine(item.description.trim(), false), 2)}\n`;
4✔
269
        }
4✔
270

5✔
271
        api += `  ${key} = ${item.value},\n`;
5✔
272
      } else {
10✔
273
        api += item.description ? `${putCommentBeforeEachLine(item.description.trim(), false)}\n` : '';
3✔
274
        if (item.deprecated)
3✔
275
          api += `---@deprecated ${removeNewlines(item.deprecated)}\n`;
3!
276
        api += `${key} = ${item.value}\n`;
3✔
277
      }
3✔
278
    };
2✔
279

2✔
280
    for (const item of _enum.items)
2✔
281
      writeItem(item.key, item);
2✔
282

2✔
283
    if (isContainedInTable) {
2✔
284
      api += '}';
1✔
285
    } else {
1✔
286
      // TODO: Clean up this workaround when LuaLS supports global enumerations.
1✔
287
      // Until LuaLS supports global enumerations (https://github.com/LuaLS/lua-language-server/issues/2721) we
1✔
288
      // will use @alias as a workaround.
1✔
289
      // LuaLS doesn't nicely display annotations for aliasses, hence this is commented
1✔
290
      //api += `\n---${this.formatRealm(_enum.realm)} ${_enum.description ? `${putCommentBeforeEachLine(_enum.description.trim())}` : ''}\n`;
1✔
291
      api += `\n---@alias ${_enum.name}\n`;
1✔
292

1✔
293
      for (const item of _enum.items) {
1✔
294
        if (item.key !== '' && !isNaN(Number(item.value.trim()))) {
4✔
295
          api += `---| \`${item.key}\`\n`;
3✔
296
        }
3✔
297
      }
4✔
298
    }
1✔
299

2✔
300
    api += `\n\n`;
2✔
301

2✔
302
    return api;
2✔
303
  }
2✔
304

1✔
305
  private writeType(type: string, value: any) {
1✔
306
    if (type === 'string')
×
307
      return `'${escapeSingleQuotes(value)}'`;
×
308

×
309
    if (type === 'Vector')
×
310
      return `Vector${value}`;
×
311

×
312
    return value;
×
313
  }
×
314

1✔
315
  private writeStruct(struct: Struct) {
1✔
316
    let api: string = this.writeClassStart(struct.name, struct.realm, struct.url, undefined, struct.deprecated, struct.description);
2✔
317

2✔
318
    for (const field of struct.fields) {
2✔
319
      if (field.deprecated)
34✔
320
        api += `---@deprecated ${removeNewlines(field.deprecated)}\n`;
34!
321

34✔
322
      api += `---${putCommentBeforeEachLine(field.description.trim())}\n`;
34✔
323

34✔
324
      const type = this.transformType(field.type, field.callback);
34✔
325
      api += `---@type ${type}\n`;
34✔
326
      api += `${struct.name}.${GluaApiWriter.safeName(field.name)} = ${field.default ? this.writeType(type, field.default) : 'nil'}\n\n`;
34!
327
    }
34✔
328

2✔
329
    return api;
2✔
330
  }
2✔
331

1✔
332
  public writePages(pages: WikiPage[], filePath: string, index: number = 0) {
1✔
333
    if (!this.files.has(filePath)) this.files.set(filePath, []);
7✔
334

7✔
335
    pages.forEach(page => {
7✔
336
      this.files.get(filePath)!.push({ index: index, page: page });
7✔
337
    });
7✔
338
  }
7✔
339

1✔
340
  public getPages(filePath: string) {
1✔
341
    return this.files.get(filePath) ?? [];
7!
342
  }
7✔
343

1✔
344
  public makeApiFromPages(pages: IndexedWikiPage[]) {
1✔
345
    let api = '';
7✔
346

7✔
347
    pages.sort((a, b) => a.index - b.index);
7✔
348

7✔
349
    // First we write the "header" types
7✔
350
    for (const page of pages.filter(x => isClass(x.page) || isLibrary(x.page) || isPanel(x.page))) {
7✔
351
      try {
1✔
352
        api += this.writePage(page.page);
1✔
353
      } catch (e) {
1!
354
        console.error(`Failed to write 'header' page ${page.page.address}: ${e}`);
×
355
      }
×
356
    }
1✔
357

7✔
358
    for (const page of pages.filter(x => !isClass(x.page) && !isLibrary(x.page) && !isPanel(x.page))) {
7✔
359
      try {
6✔
360
        api += this.writePage(page.page);
6✔
361
      } catch (e) {
6!
362
        console.error(`Failed to write page ${page.page.address}: ${e}`);
×
363
      }
×
364
    }
6✔
365

7✔
366
    return api;
7✔
367
  }
7✔
368

1✔
369
  public writeToDisk() {
1✔
370
    this.files.forEach((pages: IndexedWikiPage[], filePath: string) => {
×
371
      let api = this.makeApiFromPages(pages);
×
372

×
373
      if (api.length > 0) {
×
374
        fs.appendFileSync(filePath, '---@meta\n\n' + api);
×
375
      }
×
376
    });
×
377
  }
×
378

1✔
379
  private transformType(type: string, callback?: FunctionCallback) {
1✔
380
    if (type === 'vararg')
65✔
381
      return 'any';
65✔
382

64✔
383
    // Convert `function` type to `fun(cmd: string, args: string):(returnValueName: string[]?)`
64✔
384
    if (type === 'function' && callback) {
65✔
385
      let callbackString = `fun(`;
1✔
386

1✔
387
      for (const arg of callback.arguments || []) {
1!
388
        if (!arg.name) arg.name = arg.type;
1!
389
        if (arg.type === 'vararg') arg.name = '...';
1!
390

1✔
391
        callbackString += `${GluaApiWriter.safeName(arg.name)}: ${this.transformType(arg.type)}${arg.default !== undefined ? `?` : ''}, `;
1!
392
      }
1✔
393

1✔
394
      // Remove trailing comma and space
1✔
395
      if (callbackString.endsWith(', '))
1✔
396
        callbackString = callbackString.substring(0, callbackString.length - 2);
1✔
397

1✔
398
      callbackString += ')';
1✔
399

1✔
400
      if (callback.returns?.length) {
1!
401
        callbackString += ':(';
1✔
402
      }
1✔
403

1✔
404
      for (const ret of callback.returns || []) {
1!
405
        if (!ret.name) ret.name = ret.type;
2!
406
        if (ret.type === 'vararg') ret.name = '...';
2!
407

2✔
408
        callbackString += `${ret.name}: ${this.transformType(ret.type)}${ret.default !== undefined ? `?` : ''}, `;
2!
409
      }
2✔
410

1✔
411
      // Remove trailing comma and space
1✔
412
      if (callbackString.endsWith(', '))
1✔
413
        callbackString = callbackString.substring(0, callbackString.length - 2);
1✔
414

1✔
415
      if (callback.returns?.length) {
1!
416
        callbackString += ')';
1✔
417
      }
1✔
418

1✔
419
      return callbackString;
1✔
420
    } else if (type.startsWith('table<') && !type.includes(',')) {
65✔
421
      // Convert `table<Player>` to `Player[]` for LuaLS (but leave table<x, y> untouched)
2✔
422
      let innerType = type.match(/<([^>]+)>/)?.[1];
2✔
423

2✔
424
      if (!innerType) throw new Error(`Invalid table type: ${type}`);
2✔
425

1✔
426
      return `${innerType}[]`;
1✔
427
    } else if (type.startsWith('table{')) {
63✔
428
      // Convert `table{ToScreenData}` structures to `ToScreenData` class for LuaLS
1✔
429
      let innerType = type.match(/{([^}]+)}/)?.[1];
1!
430

1✔
431
      if (!innerType) throw new Error(`Invalid table type: ${type}`);
1!
432

1✔
433
      return innerType;
1✔
434
    } else if (type.startsWith('number{')) {
61✔
435
      // Convert `number{MATERIAL_FOG}` to `MATERIAL_FOG` enum for LuaLS
1✔
436
      let innerType = type.match(/{([^}]+)}/)?.[1];
1!
437

1✔
438
      if (!innerType) throw new Error(`Invalid number type: ${type}`);
1!
439

1✔
440
      return innerType;
1✔
441
    }
1✔
442

59✔
443
    return type;
59✔
444
  }
59✔
445

1✔
446
  private formatRealm(realm: Realm) {
1✔
447
    // Formats to show the image, with the realm as the alt text
15✔
448
    switch (realm) {
15✔
449
      case 'menu':
15!
450
        return '![(Menu)](https://github.com/user-attachments/assets/62703d98-767e-4cf2-89b3-390b1c2c5cd9)';
×
451
      case 'client':
15✔
452
        return '![(Client)](https://github.com/user-attachments/assets/a5f6ba64-374d-42f0-b2f4-50e5c964e808)';
4✔
453
      case 'server':
15✔
454
        return '![(Server)](https://github.com/user-attachments/assets/d8fbe13a-6305-4e16-8698-5be874721ca1)';
1✔
455
      case 'shared':
15✔
456
        return '![(Shared)](https://github.com/user-attachments/assets/a356f942-57d7-4915-a8cc-559870a980fc)';
8✔
457
      case 'client and menu':
15!
458
        return '![(Client and menu)](https://github.com/user-attachments/assets/25d1a1c8-4288-4a51-9867-5e3bb51b9981)';
×
459
      case 'shared and menu':
15✔
460
        return '![(Shared and Menu)](https://github.com/user-attachments/assets/8f5230ff-38f7-493b-b9fc-cc70ffd5b3f4)';
2✔
461
      default:
15!
462
        throw new Error(`Unknown realm: ${realm}`);
×
463
    }
15✔
464
  }
15✔
465

1✔
466
  private writeFunctionLuaDocComment(func: Function, args: FunctionArgument[] | undefined, realm: Realm) {
1✔
467
    let luaDocComment = `---${this.formatRealm(realm)} ${putCommentBeforeEachLine(func.description!.trim())}\n`;
14✔
468
    luaDocComment += `---\n---[View wiki](${func.url})\n`;
14✔
469

14✔
470
    if (args) {
14✔
471
      args.forEach((arg, index) => {
10✔
472
        if (!arg.name)
16✔
473
          arg.name = arg.type;
16!
474

16✔
475
        if (arg.type === 'vararg')
16✔
476
          arg.name = '...';
16✔
477

16✔
478
        // TODO: This splitting will fail in complicated cases like `table<string|number>|string`.
16✔
479
        // TODO: I'm assuming for now that there is no such case in the GMod API.
16✔
480
        // Split any existing types, append the (deprecated) alt and join them back together
16✔
481
        // while transforming each type to a LuaLS compatible type.
16✔
482
        let types = arg.type.split('|');
16✔
483

16✔
484
        if (arg.altType) {
16✔
485
          types.push(arg.altType);
3✔
486
        }
3✔
487

16✔
488
        let typesString = types.map(type => this.transformType(type, arg.callback))
16✔
489
          .join('|');
16✔
490

16✔
491
        luaDocComment += `---@param ${GluaApiWriter.safeName(arg.name)}${arg.default !== undefined ? `?` : ''} ${typesString} ${putCommentBeforeEachLine(arg.description!.trim())}\n`;
16✔
492
      });
10✔
493
    }
10✔
494

13✔
495
    if (func.returns) {
14✔
496
      func.returns.forEach(ret => {
8✔
497
        const description = putCommentBeforeEachLine(ret.description!.trim());
8✔
498

8✔
499
        luaDocComment += `---@return `;
8✔
500

8✔
501
        if (ret.type === 'vararg')
8✔
502
          luaDocComment += 'any ...';
8✔
503
        else
7✔
504
          luaDocComment += `${this.transformType(ret.type, ret.callback)}`;
7✔
505

8✔
506
        luaDocComment += ` # ${description}\n`;
8✔
507
      });
8✔
508
    }
8✔
509

13✔
510
    if (func.deprecated)
13✔
511
      luaDocComment += `---@deprecated ${removeNewlines(func.deprecated)}\n`;
14!
512

13✔
513
    return luaDocComment;
13✔
514
  }
13✔
515

1✔
516
  private writeFunctionDeclaration(func: Function, args: FunctionArgument[] | undefined, indexer: string = '.') {
1✔
517
    let declaration = `function ${func.parent ? `${func.parent}${indexer}` : ''}${GluaApiWriter.safeName(func.name)}(`;
13!
518

13✔
519
    if (args) {
13✔
520
      declaration += args.map(arg => {
9✔
521
        if (arg.type === 'vararg')
15✔
522
          return '...';
15✔
523

14✔
524
        return GluaApiWriter.safeName(arg.name!);
14✔
525
      }).join(', ');
9✔
526
    }
9✔
527

13✔
528
    declaration += ') end\n\n';
13✔
529

13✔
530
    return declaration;
13✔
531
  }
13✔
532
}
1✔
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