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

rokucommunity / brighterscript / #13664

28 Jan 2025 02:09PM UTC coverage: 86.757%. Remained the same
#13664

push

web-flow
Merge 62bbd6b91 into aa9aba86f

12553 of 15295 branches covered (82.07%)

Branch coverage included in aggregate %.

94 of 100 new or added lines in 13 files covered. (94.0%)

108 existing lines in 10 files now uncovered.

13423 of 14646 relevant lines covered (91.65%)

34160.83 hits per line

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

78.63
/src/PluginInterface.ts
1
import type { AnnotationDeclaration, CompilerPlugin } from './interfaces';
2
import { LogLevel, createLogger, type Logger } from './logging';
1✔
3
import type { TypedFunctionType } from './types/TypedFunctionType';
4
/*
5
 * we use `Required` everywhere here because we expect that the methods on plugin objects will
6
 * be optional, and we don't want to deal with `undefined`.
7
 * `extends (...args: any[]) => any` determines whether the thing we're dealing with is a function.
8
 * Returning `never` in the `as` clause of the `[key in object]` step deletes that key from the
9
 * resultant object.
10
 * on the right-hand side of the mapped type we are forced to use a conditional type a second time,
11
 * in order to be able to use the `Parameters` utility type on `Required<T>[K]`. This will always
12
 * be true because of the filtering done by the `[key in object]` clause, but TS requires the duplication.
13
 *
14
 * so put together: we iterate over all of the fields in T, deleting ones which are not (potentially
15
 * optional) functions. For the ones that are, we replace them with their parameters.
16
 *
17
 * this returns the type of an object whose keys are the names of the methods of T and whose values
18
 * are tuples containing the arguments that each method accepts.
19
 */
20
export type PluginEventArgs<T> = {
21
    [K in keyof Required<T> as Required<T>[K] extends (...args: any[]) => any ? K : never]:
22
    Required<T>[K] extends (...args: any[]) => any ? Parameters<Required<T>[K]> : never
23
};
24

25
export default class PluginInterface<T extends CompilerPlugin = CompilerPlugin> {
1✔
26
    constructor(
27
        plugins?: CompilerPlugin[],
28
        options?: {
29
            logger?: Logger;
30
            suppressErrors?: boolean;
31
        }
32
    ) {
33
        this.logger = options?.logger;
1,902✔
34
        this.suppressErrors = (options as any)?.suppressErrors === false ? false : true;
1,902!
35
        for (const plugin of plugins ?? []) {
1,902✔
UNCOV
36
            this.add(plugin);
×
37
        }
38
        if (!this.logger) {
1,902✔
39
            this.logger = createLogger();
1✔
40
        }
41
        if (!this.logger) {
1,902!
UNCOV
42
            this.logger = createLogger();
×
43
        }
44
    }
45

46
    private plugins: CompilerPlugin[] = [];
1,902✔
47
    private logger: Logger;
48

49
    /**
50
     * Should plugin errors cause the program to fail, or should they be caught and simply logged
51
     */
52
    private suppressErrors: boolean | undefined;
53

54
    /**
55
     * Call `event` on plugins
56
     */
57
    public emit<K extends keyof PluginEventArgs<T> & string>(event: K, ...args: PluginEventArgs<T>[K]) {
58
        this.logger.debug(`Emitting plugin event: ${event}`);
39,492✔
59
        for (let plugin of this.plugins) {
39,492✔
60
            if ((plugin as any)[event]) {
40,999✔
61
                try {
12,221✔
62
                    this.logger?.time(LogLevel.debug, [plugin.name, event], () => {
12,221!
63
                        (plugin as any)[event](...args);
12,221✔
64
                    });
65
                } catch (err) {
66
                    this.logger?.error(`Error when calling plugin ${plugin.name}.${event}:`, (err as Error).stack);
4!
67
                    if (!this.suppressErrors) {
4!
UNCOV
68
                        throw err;
×
69
                    }
70
                }
71
            }
72
        }
73
        return args[0];
39,492✔
74
    }
75

76
    /**
77
     * Call `event` on plugins, but allow the plugins to return promises that will be awaited before the next plugin is notified
78
     */
79
    public async emitAsync<K extends keyof PluginEventArgs<T> & string>(event: K, ...args: PluginEventArgs<T>[K]): Promise<PluginEventArgs<T>[K][0]> {
80
        this.logger.debug(`Emitting async plugin event: ${event}`);
10,920✔
81
        for (let plugin of this.plugins) {
10,920✔
82
            if ((plugin as any)[event]) {
20,850✔
83
                try {
3,930✔
84
                    await this.logger?.time(LogLevel.debug, [plugin.name, event], async () => {
3,930!
85
                        await Promise.resolve(
3,930✔
86
                            (plugin as any)[event](...args)
87
                        );
88
                    });
89
                } catch (err) {
90
                    this.logger?.error(`Error when calling plugin ${plugin.name}.${event}:`, err);
2!
91
                }
92
            }
93
        }
94
        return args[0];
10,920✔
95
    }
96

97
    /**
98
     * Add a plugin to the beginning of the list of plugins
99
     */
100
    public addFirst<T extends CompilerPlugin = CompilerPlugin>(plugin: T) {
101
        if (!this.has(plugin)) {
2,200!
102
            this.plugins.unshift(plugin);
2,200✔
103
        }
104
        return plugin;
2,200✔
105
    }
106

107
    /**
108
     * Add a plugin to the end of the list of plugins
109
     */
110
    public add<T extends CompilerPlugin = CompilerPlugin>(plugin: T) {
111
        if (!this.has(plugin)) {
80✔
112
            this.sanitizePlugin(plugin);
79✔
113
            this.plugins.push(plugin);
79✔
114
        }
115
        return plugin;
80✔
116
    }
117

118
    /**
119
     * Find deprecated or removed historic plugin hooks, and warn about them.
120
     * Some events can be forwards-converted
121
     */
122
    private sanitizePlugin(plugin: CompilerPlugin) {
123
        const removedHooks = [
79✔
124
            'beforePrepublish',
125
            'afterPrepublish'
126
        ];
127
        for (const removedHook of removedHooks) {
79✔
128
            if (plugin[removedHook]) {
158!
UNCOV
129
                this.logger?.error(`Plugin "${plugin.name}": event ${removedHook} is no longer supported and will never be called`);
×
130
            }
131
        }
132

133
        const upgradeWithWarn = {
79✔
134
            beforePublish: 'beforeSerializeProgram',
135
            afterPublish: 'afterSerializeProgram',
136
            beforeProgramTranspile: 'beforeBuildProgram',
137
            afterProgramTranspile: 'afterBuildProgram',
138
            beforeFileParse: 'beforeProvideFile',
139
            afterFileParse: 'afterProvideFile',
140
            beforeFileTranspile: 'beforePrepareFile',
141
            afterFileTranspile: 'afterPrepareFile',
142
            beforeFileDispose: 'beforeFileRemove',
143
            afterFileDispose: 'afterFileRemove'
144
        };
145

146
        for (const [oldEvent, newEvent] of Object.entries(upgradeWithWarn)) {
79✔
147
            if (plugin[oldEvent]) {
790✔
148
                if (!plugin[newEvent]) {
7!
149
                    plugin[newEvent] = plugin[oldEvent];
7✔
150
                    this.logger?.warn(`Plugin '${plugin.name}': event '${oldEvent}' is no longer supported. It has been converted to '${newEvent}' but you may encounter issues as their signatures do not match.`);
7!
151
                } else {
UNCOV
152
                    this.logger?.warn(`Plugin "${plugin.name}": event '${oldEvent}' is no longer supported and will never be called`);
×
153
                }
154
            }
155
        }
156
    }
157

158
    public has(plugin: CompilerPlugin) {
159
        return this.plugins.includes(plugin);
2,604✔
160
    }
161

162
    public remove<T extends CompilerPlugin = CompilerPlugin>(plugin: T) {
163
        if (this.has(plugin)) {
321!
164
            this.plugins.splice(this.plugins.indexOf(plugin), 1);
321✔
165
        }
166
        return plugin;
321✔
167
    }
168

169
    /**
170
     * Remove all plugins
171
     */
172
    public clear() {
UNCOV
173
        this.plugins = [];
×
174
    }
175

176

177
    private annotationMap: Map<string, Array<string | TypedFunctionType | AnnotationDeclaration>>;
178

179
    public getAnnotationMap() {
180
        if (this.annotationMap) {
1,881!
NEW
181
            return this.annotationMap;
×
182
        }
183
        this.annotationMap = new Map<string, Array<string | TypedFunctionType | AnnotationDeclaration>>();
1,881✔
184
        for (let plugin of this.plugins) {
1,881✔
185
            if (plugin.annotations?.length > 0) {
1,921✔
186
                this.annotationMap.set(plugin.name, plugin.annotations);
1✔
187
            }
188
        }
189
        return this.annotationMap;
1,881✔
190
    }
191
}
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