@vscode/telemetry-extractor 1.20.2 → 1.20.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/.github/dependabot.yml +11 -0
  2. package/.github/workflows/node.js.yml +3 -2
  3. package/eslint.config.js +6 -1
  4. package/out/cli-options.js +2 -1
  5. package/out/cli-options.js.map +1 -1
  6. package/out/extractor.js +7 -1
  7. package/out/extractor.js.map +1 -1
  8. package/out/lib/common-properties.js +51 -1
  9. package/out/lib/common-properties.js.map +1 -1
  10. package/out/lib/declarations.js.map +1 -1
  11. package/out/lib/event-definition.js +5 -0
  12. package/out/lib/event-definition.js.map +1 -0
  13. package/out/lib/events.js +28 -1
  14. package/out/lib/events.js.map +1 -1
  15. package/out/lib/object-converter.js +58 -4
  16. package/out/lib/object-converter.js.map +1 -1
  17. package/out/lib/operations.js +83 -66
  18. package/out/lib/operations.js.map +1 -1
  19. package/out/lib/parser.js +18 -23
  20. package/out/lib/parser.js.map +1 -1
  21. package/out/lib/ripgrep.js +14 -0
  22. package/out/lib/ripgrep.js.map +1 -0
  23. package/out/lib/save-declarations.js +32 -10
  24. package/out/lib/save-declarations.js.map +1 -1
  25. package/out/lib/ts-parser-worker.js +39 -0
  26. package/out/lib/ts-parser-worker.js.map +1 -0
  27. package/out/lib/ts-parser.js +204 -37
  28. package/out/lib/ts-parser.js.map +1 -1
  29. package/package.json +3 -3
  30. package/src/cli-options.ts +2 -1
  31. package/src/extractor.ts +8 -1
  32. package/src/lib/common-properties.ts +71 -5
  33. package/src/lib/declarations.ts +2 -1
  34. package/src/lib/event-definition.ts +7 -0
  35. package/src/lib/events.ts +33 -1
  36. package/src/lib/object-converter.ts +73 -4
  37. package/src/lib/operations.ts +75 -72
  38. package/src/lib/parser.ts +22 -34
  39. package/src/lib/ripgrep.ts +10 -0
  40. package/src/lib/save-declarations.ts +37 -13
  41. package/src/lib/ts-parser-worker.ts +38 -0
  42. package/src/lib/ts-parser.ts +247 -41
  43. package/tsconfig.json +1 -0
  44. package/vscode-telemetry-extractor.d.ts +21 -1
@@ -1,25 +1,33 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT license.
3
3
  import { Declarations, OutputtedDeclarations } from "./declarations";
4
- import { Property } from "./common-properties";
5
- import { Metadata, Wildcard } from "./events";
4
+ import { ColumnType, Property, ColumnInfo } from "./common-properties";
5
+ import { Metadata, Wildcard, TableInfo as JsonTableInfo } from "./events";
6
6
  import * as keywords from './keywords';
7
7
 
8
8
  // Converts the declarations array to an object format for easy readability.
9
9
 
10
+ type TableInfo = OutputtedDeclarations['tableInfos'][string];
11
+ type BagInfo = TableInfo['columns'][number]['bag'];
12
+
10
13
  export async function transformOutput(output: Declarations): Promise<OutputtedDeclarations> {
11
14
  // If there's no events or common properties, we emit a null object
12
15
  if (output.events.dataPoints.length === 0 && output.commonProperties.properties.length === 0) {
13
- return { events: Object.create(null), commonProperties: Object.create(null) };
16
+ return { events: Object.create(null), commonProperties: Object.create(null), tableInfos: {} };
14
17
  }
15
18
  const newEvents = Object.create(null);
16
19
  const oldEvents = output.events.dataPoints;
20
+ const tableInfos: { [key: string]: TableInfo } = {};
17
21
  for (const event of oldEvents) {
18
22
  // Check if event.name ends with a number, if so throw an error because we don't support event names which end with numbers
19
23
  if (/\d$/.test(event.name)) {
20
24
  throw new Error(`Event name ${event.name} ends with a number. Event names cannot end with numbers.`);
21
25
  }
22
26
  newEvents[event.name] = Object.create(null);
27
+ const tableInfo: TableInfo | undefined = event.tableInfo ? { ...event.tableInfo, columns: [] } : undefined;
28
+ if (tableInfo) {
29
+ tableInfos[tableInfo.name] = tableInfo;
30
+ }
23
31
  //We know there won't be anymore includes or inlines because we have resolved them
24
32
  for (const property of event.properties as Array<Property | Wildcard | Metadata>) {
25
33
  if (property instanceof Wildcard) {
@@ -66,11 +74,18 @@ export async function transformOutput(output: Declarations): Promise<OutputtedDe
66
74
  if (property.isMeasurement) {
67
75
  newEvents[event.name][propetyNameChanger(property.name)]['isMeasurement'] = property.isMeasurement;
68
76
  }
77
+ if (tableInfo !== undefined && property.column) {
78
+ const name = property.column.name ?? property.name;
79
+ tableInfo.columns.push({ name, type: property.column.type, bag: { name: property.name.toLowerCase(), store: property.isMeasurement === true ? 'Measures' : 'Properties' } });
80
+ }
69
81
  } else {
70
82
  // Comments, expiration, and owner metadata are handled here
71
83
  newEvents[event.name][propetyNameChanger(property.name)] = property.value;
72
84
  }
73
85
  }
86
+ if (tableInfo !== undefined) {
87
+ tableInfos[tableInfo.name] = tableInfo;
88
+ }
74
89
  }
75
90
  const newCommonProperties = Object.create(null);
76
91
  const oldCommonProperties = output.commonProperties.properties;
@@ -89,8 +104,62 @@ export async function transformOutput(output: Declarations): Promise<OutputtedDe
89
104
  newCommonProperties[propetyNameChanger(property.name)]['isMeasurement'] = property.isMeasurement;
90
105
  }
91
106
  }
92
- return { events: newEvents, commonProperties: newCommonProperties };
107
+ return { events: newEvents, commonProperties: newCommonProperties, tableInfos };
108
+ }
109
+
110
+ type TypeScriptPropertyDeclaration = {
111
+ name?: string;
112
+ isMeasurement?: boolean;
113
+ type?: string;
114
+ column?: { name?: string; type?: string }
93
115
  }
116
+ type TypeScriptEventDeclaration = {
117
+ $tableInfo?: unknown;
118
+ [name: string]: TypeScriptPropertyDeclaration | unknown;
119
+ };
120
+
121
+ export function transformTypeScriptDeclaration(declaration: TypeScriptEventDeclaration): { declaration: object, tableInfo: TableInfo | undefined} {
122
+ // The property names in the TS events are already all lowercased.
123
+ const tableInfoProperty = declaration['$tableinfo'];
124
+ let tableInfo: TableInfo | undefined = undefined;
125
+ if (tableInfoProperty !== undefined) {
126
+ delete declaration['$tableinfo'];
127
+ const json = JsonTableInfo.fromObject(tableInfoProperty);
128
+ if (json !== undefined) {
129
+ tableInfo = { name: json.name, commonProperties: json.commonProperties, backfill: json.backfill, columns: [] };
130
+ }
131
+ }
132
+ for (const name of Object.keys(declaration)) {
133
+ if (name === '$tableinfo') {
134
+ continue;
135
+ }
136
+ const property = declaration[name] as TypeScriptPropertyDeclaration;
137
+ if (!property || typeof property !== 'object') {
138
+ continue;
139
+ }
140
+ if (tableInfo !== undefined) {
141
+ if (property.column !== undefined) {
142
+ if (ColumnInfo.is(property.column)) {
143
+ const type = ColumnType.fromString(property.column.type);
144
+ if (type !== undefined) {
145
+ const bag: BagInfo = { store: property.isMeasurement === true ? 'Measures' : 'Properties', name: name };
146
+ tableInfo.columns.push({ name: property.column.name ?? name, type: type, bag });
147
+ }
148
+ }
149
+ } else if (typeof property.type === 'string') {
150
+ const columnType = ColumnType.fromString(property.type);
151
+ if (columnType) {
152
+ const bag: BagInfo = { store: property.isMeasurement === true ? 'Measures' : 'Properties', name: name };
153
+ tableInfo.columns.push({ name: name, type: columnType, bag });
154
+ }
155
+ }
156
+ }
157
+ delete property.type;
158
+ delete property.column;
159
+ }
160
+ return { declaration, tableInfo };
161
+ }
162
+
94
163
 
95
164
  function propetyNameChanger(name: string) {
96
165
  name = name.toLowerCase();
@@ -2,7 +2,7 @@
2
2
  // Licensed under the MIT license.
3
3
  import { Fragments, Fragment } from "./fragments";
4
4
  import { Events, Event, Include, Inline, Wildcard, WildcardEntry, Metadata } from "./events";
5
- import { Property } from "./common-properties";
5
+ import { ColumnType, ColumnInfo, Property } from "./common-properties";
6
6
  import * as keywords from './keywords';
7
7
  import * as path from 'path';
8
8
 
@@ -14,9 +14,54 @@ export function merge(target: Fragments | Events, source: Fragments | Events) {
14
14
  // We combine their properties together if the event already exists
15
15
  if (found) {
16
16
  if (target instanceof Events && source instanceof Events && found instanceof Event && item instanceof Event) {
17
- if (!sameEventDefinition(found, item)) {
17
+ if (!eventsAreCompatible(found, item)) {
18
18
  continue;
19
19
  }
20
+ if (found.tableInfo === undefined) {
21
+ found.tableInfo = item.tableInfo;
22
+ }
23
+ // Merge unique properties from source into target
24
+ for (const prop of item.properties) {
25
+ if (prop instanceof Property) {
26
+ const exists = found.properties.some(p => p instanceof Property && p.name === prop.name);
27
+ if (!exists) {
28
+ found.properties.push(prop);
29
+ }
30
+ } else if (prop instanceof Metadata) {
31
+ const exists = found.properties.some(p => p instanceof Metadata && p.name === prop.name);
32
+ if (!exists) {
33
+ found.properties.push(prop);
34
+ }
35
+ } else if (prop instanceof Include) {
36
+ const existingInclude = found.properties.find(p => p instanceof Include) as Include | undefined;
37
+ if (existingInclude) {
38
+ for (const name of prop.includeNames) {
39
+ if (!existingInclude.includeNames.includes(name)) {
40
+ existingInclude.includeNames.push(name);
41
+ }
42
+ }
43
+ } else {
44
+ found.properties.push(new Include([...prop.includeNames]));
45
+ }
46
+ } else if (prop instanceof Inline) {
47
+ const exists = found.properties.some(p => p instanceof Inline && p.inlineName === prop.inlineName);
48
+ if (!exists) {
49
+ found.properties.push(prop);
50
+ }
51
+ } else if (prop instanceof Wildcard) {
52
+ const existingWildcard = found.properties.find(p => p instanceof Wildcard) as Wildcard | undefined;
53
+ if (existingWildcard) {
54
+ for (const entry of prop.entries) {
55
+ const entryExists = existingWildcard.entries.some(e => e.prefix === entry.prefix);
56
+ if (!entryExists) {
57
+ existingWildcard.entries.push(entry);
58
+ }
59
+ }
60
+ } else {
61
+ found.properties.push(prop);
62
+ }
63
+ }
64
+ }
20
65
  continue;
21
66
  }
22
67
  found.properties = found.properties.concat(item.properties);
@@ -26,79 +71,22 @@ export function merge(target: Fragments | Events, source: Fragments | Events) {
26
71
  }
27
72
  }
28
73
 
29
- function sameEventDefinition(left: Event, right: Event) {
30
- return stableSerialize(eventToComparable(left)) === stableSerialize(eventToComparable(right));
31
- }
32
-
33
- function eventToComparable(event: Event) {
34
- const properties = event.properties.map(propertyToComparable);
35
- properties.sort((a, b) => stableSerialize(a).localeCompare(stableSerialize(b)));
36
- return {
37
- name: event.name,
38
- properties
39
- };
40
- }
41
-
42
- function propertyToComparable(property: Property | Metadata | Include | Inline | Wildcard) {
43
- if (property instanceof Property) {
44
- return {
45
- type: 'property',
46
- name: property.name,
47
- classification: property.classification,
48
- purpose: property.purpose,
49
- expiration: property.expiration,
50
- owner: property.owner,
51
- comment: property.comment,
52
- endPoint: property.endPoint,
53
- isMeasurement: property.isMeasurement
54
- };
55
- }
56
- if (property instanceof Metadata) {
57
- return {
58
- type: 'metadata',
59
- name: property.name,
60
- value: property.value
61
- };
62
- }
63
- if (property instanceof Include) {
64
- return {
65
- type: 'include',
66
- includeNames: [...property.includeNames].sort()
67
- };
68
- }
69
- if (property instanceof Inline) {
70
- return {
71
- type: 'inline',
72
- inlineName: property.inlineName,
73
- inlines: [...property.inlines].sort()
74
- };
75
- }
76
- return {
77
- type: 'wildcard',
78
- entries: property.entries.map(entry => ({
79
- prefix: entry.prefix,
80
- classification: entry.classification,
81
- endpoint: entry.endpoint
82
- })).sort((a, b) => stableSerialize(a).localeCompare(stableSerialize(b)))
83
- };
84
- }
85
-
86
- function stableSerialize(value: unknown): string {
87
- if (Array.isArray(value)) {
88
- return `[${value.map(stableSerialize).join(',')}]`;
89
- }
90
-
91
- if (value && typeof value === 'object') {
92
- const entries = Object.entries(value as Record<string, unknown>)
93
- .sort(([left], [right]) => left.localeCompare(right))
94
- .map(([key, entryValue]) => `${JSON.stringify(key)}:${stableSerialize(entryValue)}`);
95
- return `{${entries.join(',')}}`;
74
+ function eventsAreCompatible(left: Event, right: Event): boolean {
75
+ for (const leftProp of left.properties) {
76
+ if (!(leftProp instanceof Property)) continue;
77
+ for (const rightProp of right.properties) {
78
+ if (!(rightProp instanceof Property)) continue;
79
+ if (leftProp.name === rightProp.name) {
80
+ if (leftProp.classification !== rightProp.classification || leftProp.purpose !== rightProp.purpose) {
81
+ return false;
82
+ }
83
+ }
84
+ }
96
85
  }
97
-
98
- return JSON.stringify(value);
86
+ return true;
99
87
  }
100
88
 
101
- // Searches the object for an event or fragment of the specific name
89
+ // Searches the object for an event or fragment of the specific name
102
90
  // If found returns, if not found creates it, places it in the array, and then returns
103
91
  export function findOrCreate(searchTarget: Events | Fragments, name: string) {
104
92
  let found = searchTarget.dataPoints.find((item) => {
@@ -137,6 +125,10 @@ export function mergeWildcards(wildcard: any[], target: Event | Fragment, applyE
137
125
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
138
126
  export function populateProperties(properties: any, target: Event | Fragment, applyEndpoints = false) {
139
127
  for (const propertyName in properties) {
128
+ // Skip the `$tableInfo` property since it is an artificial property for the flat table support.
129
+ if (propertyName === '$tableInfo') {
130
+ continue;
131
+ }
140
132
  const currentProperty = properties[propertyName];
141
133
  if (propertyName === keywords.include) {
142
134
  target.properties.push(new Include(currentProperty));
@@ -157,6 +149,17 @@ export function populateProperties(properties: any, target: Event | Fragment, ap
157
149
  if (currentProperty.isMeasurement) {
158
150
  prop.isMeasurement = currentProperty.isMeasurement;
159
151
  }
152
+ if (typeof currentProperty.type === 'string') {
153
+ const columnType = ColumnType.fromString(currentProperty.type);
154
+ if (columnType) {
155
+ prop.column = { type: columnType };
156
+ }
157
+ } else if (ColumnInfo.is(currentProperty.column)) {
158
+ const columnType = ColumnType.fromString(currentProperty.column.type);
159
+ if (columnType) {
160
+ prop.column = { name: currentProperty.column.name, type: columnType };
161
+ }
162
+ }
160
163
  target.properties.push(prop);
161
164
  }
162
165
  }
package/src/lib/parser.ts CHANGED
@@ -7,14 +7,11 @@ import * as fs from 'fs';
7
7
  // Not importing 'process' as tsc claims `process.exitCode` is read-only when it actually is not.
8
8
  import { Fragments } from './fragments';
9
9
  import { Property, CommonProperties } from './common-properties';
10
- import { Events } from './events';
10
+ import { Events, Event, TableInfo } from './events';
11
11
  import { Declarations } from './declarations';
12
12
  import { merge, findOrCreate, populateProperties, makeExclusionsRelativeToSource } from './operations';
13
-
14
- export interface EventDefinition {
15
- signature: string;
16
- location: string;
17
- }
13
+ import { parseRipgrepFilePaths } from './ripgrep';
14
+ import { EventDefinition } from './event-definition';
18
15
 
19
16
  export class Parser {
20
17
 
@@ -132,7 +129,6 @@ export class Parser {
132
129
 
133
130
  private findEvents(sourceDir: string) {
134
131
  const filesWithEvents = this.asAbsoluteFilePaths(this.findFilesWithEvents(sourceDir));
135
- const eventSignatures = new Map<string, string>();
136
132
 
137
133
  // Using [\s\S]* instead of .* since the latter does not match when using /m option
138
134
  const eventMatcher = /\/\*\s*__GDPR__\b([\s\S]*?)\*\//mg;
@@ -143,19 +139,22 @@ export class Parser {
143
139
  let eventName = Object.keys(eventDeclaration)[0];
144
140
  eventName = this.lowerCaseEvents ? eventName.toLowerCase() : eventName;
145
141
  const eventProperties = eventDeclaration[Object.keys(eventDeclaration)[0]];
146
- const currentSignature = this.stableSerialize(eventProperties);
142
+ const conflictProperties = this.extractConflictProperties(eventProperties);
147
143
  const lineNumber = this.getLineNumber(fileContents, match.index);
148
- this.addEventDefinition(eventName, currentSignature, `${filePath}:${lineNumber}`);
149
- const existingSignature = eventSignatures.get(eventName);
150
-
151
- if (existingSignature) {
152
- return;
144
+ this.addEventDefinition(eventName, conflictProperties, `${filePath}:${lineNumber}`);
145
+
146
+ const event = new Event(eventName);
147
+ if (event instanceof Event && eventProperties['$tableInfo'] !== undefined) {
148
+ const tableInfo: TableInfo | undefined = TableInfo.fromObject(eventProperties['$tableInfo']);
149
+ if (tableInfo) {
150
+ event.tableInfo = tableInfo;
151
+ }
153
152
  }
154
-
155
- eventSignatures.set(eventName, currentSignature);
156
- const event = findOrCreate(eventDeclarations, eventName);
157
153
  // Get the propeties which the event possesses
158
154
  populateProperties(eventProperties, event, this.applyEndpoints);
155
+ const currentDeclaration = new Events();
156
+ currentDeclaration.dataPoints.push(event);
157
+ merge(eventDeclarations, currentDeclaration);
159
158
  } catch (error) {
160
159
  console.error(`Event Declaration Error: ${error} in file ${filePath}`);
161
160
  console.error(`Source comment:\n${match[0]}`);
@@ -173,12 +172,16 @@ export class Parser {
173
172
  return definitions;
174
173
  }
175
174
 
176
- private addEventDefinition(eventName: string, signature: string, location: string) {
175
+ private addEventDefinition(eventName: string, properties: Record<string, unknown>, location: string) {
177
176
  const existing = this.eventDefinitions.get(eventName) ?? [];
178
- existing.push({ signature, location });
177
+ existing.push({ properties, location });
179
178
  this.eventDefinitions.set(eventName, existing);
180
179
  }
181
180
 
181
+ private extractConflictProperties(eventProperties: Record<string, unknown>): Record<string, unknown> {
182
+ return { ...eventProperties };
183
+ }
184
+
182
185
  private getLineNumber(fileContents: string, index: number | undefined) {
183
186
  if (index === undefined) {
184
187
  return 1;
@@ -186,21 +189,6 @@ export class Parser {
186
189
  return fileContents.slice(0, index).split(/\r\n|\r|\n/).length;
187
190
  }
188
191
 
189
- private stableSerialize(value: unknown): string {
190
- if (Array.isArray(value)) {
191
- return `[${value.map((entry) => this.stableSerialize(entry)).join(',')}]`;
192
- }
193
-
194
- if (value && typeof value === 'object') {
195
- const entries = Object.entries(value as Record<string, unknown>)
196
- .sort(([left], [right]) => left.localeCompare(right))
197
- .map(([key, entryValue]) => `${JSON.stringify(key)}:${this.stableSerialize(entryValue)}`);
198
- return `{${entries.join(',')}}`;
199
- }
200
-
201
- return JSON.stringify(value);
202
- }
203
-
204
192
  // Utilizes a regex to find the files containing the specific pattern
205
193
  private findFiles(ripgrepPattern: string, sourceDir: string) {
206
194
  const relativeExclusions = makeExclusionsRelativeToSource(sourceDir, this.excludedDirs);
@@ -208,7 +196,7 @@ export class Parser {
208
196
  const ripgrepArgs = ['--files-with-matches', '--glob', '*.ts', '--glob', '*.tsx', '--glob', '*.cs', ...exclusions, '--regexp', ripgrepPattern, '--', sourceDir];
209
197
  try {
210
198
  const filePaths = cp.execFileSync(rgPath, ripgrepArgs, { encoding: 'ascii', cwd: `${sourceDir}` });
211
- return filePaths.split(/(?:\r\n|\r|\n)/g).filter(path => path && path.length > 0);
199
+ return parseRipgrepFilePaths(filePaths);
212
200
  } catch {
213
201
  // ripgrep's return code != 0 if there are no matches
214
202
  return [];
@@ -0,0 +1,10 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT license.
3
+
4
+ export function parseRipgrepFilePaths(output: string): string[] {
5
+ // Sorting after ripgrep exits preserves its parallel filesystem traversal.
6
+ return output
7
+ .split(/(?:\r\n|\r|\n)/g)
8
+ .filter(filePath => filePath.length > 0)
9
+ .sort();
10
+ }
@@ -4,7 +4,7 @@ import * as path from 'path';
4
4
  import { Parser } from './parser';
5
5
  import * as fileWriter from './file-writer';
6
6
  import { resolveDeclarations, OutputtedDeclarations, Declarations } from './declarations';
7
- import { transformOutput } from './object-converter';
7
+ import { transformOutput, transformTypeScriptDeclaration } from './object-converter';
8
8
  import { Events } from './events';
9
9
  import { CommonProperties } from './common-properties';
10
10
  import { TsParser } from './ts-parser';
@@ -12,10 +12,10 @@ import { patchDebugEvents } from './debug-patch';
12
12
  import { ParserOptions, SourceSpec } from './source-spec';
13
13
  import { logMessage } from './logger';
14
14
  import { Fragments } from './fragments';
15
- import { EventDefinition } from './parser';
15
+ import { EventDefinition } from './event-definition';
16
16
 
17
17
  interface EventConflictEntry {
18
- signature: string;
18
+ properties: Record<string, unknown>;
19
19
  location: string;
20
20
  source: 'GDPR' | 'TS';
21
21
  }
@@ -92,15 +92,33 @@ function mergeEventDefinitions(
92
92
  function reportDuplicateEventConflicts(definitions: Map<string, EventConflictEntry[]>) {
93
93
  let hasConflicts = false;
94
94
  for (const [eventName, entries] of definitions.entries()) {
95
- const signatures = new Set(entries.map(entry => entry.signature));
96
- if (signatures.size <= 1) {
95
+ if (entries.length <= 1) {
97
96
  continue;
98
97
  }
99
98
 
100
- hasConflicts = true;
101
- const uniqueLocations = [...new Set(entries.map(entry => `${entry.location.replace(/\\/g, '/')} (${entry.source})`))];
102
- console.error(`Duplicate telemetry event declaration '${eventName}' has conflicting details at:`);
103
- uniqueLocations.forEach(location => console.error(` - ${location}`));
99
+ // Compatible declarations may add properties, but must agree on every overlapping output field.
100
+ let eventHasConflict = false;
101
+ for (let i = 0; i < entries.length && !eventHasConflict; i++) {
102
+ for (let j = i + 1; j < entries.length && !eventHasConflict; j++) {
103
+ for (const propName of Object.keys(entries[i].properties)) {
104
+ if (propName in entries[j].properties) {
105
+ const a = entries[i].properties[propName];
106
+ const b = entries[j].properties[propName];
107
+ if (JSON.stringify(deepSortKeys(a)) !== JSON.stringify(deepSortKeys(b))) {
108
+ eventHasConflict = true;
109
+ break;
110
+ }
111
+ }
112
+ }
113
+ }
114
+ }
115
+
116
+ if (eventHasConflict) {
117
+ hasConflicts = true;
118
+ const uniqueLocations = [...new Set(entries.map(entry => `${entry.location.replace(/\\/g, '/')} (${entry.source})`))];
119
+ console.error(`Duplicate telemetry event declaration '${eventName}' has conflicting details at:`);
120
+ uniqueLocations.forEach(location => console.error(` - ${location}`));
121
+ }
104
122
  }
105
123
  return hasConflicts;
106
124
  }
@@ -117,11 +135,11 @@ export async function extractAndResolveDeclarations(sourceSpecs: Array<SourceSpe
117
135
  declarations = resolveDeclarations(declarations, spec.parserOptions.verbose);
118
136
  let typescriptDeclarations = Object.create(null);
119
137
  // The parser does not know how to handle multiple source directories due to different TS configs, so we manually have to parse each source dir
120
- spec.sourceDirs.forEach((dir) => {
138
+ for (const dir of spec.sourceDirs) {
121
139
  const tsParser = new TsParser(dir, spec.excludedDirs, spec.parserOptions.applyEndpoints, spec.parserOptions.lowerCaseEvents);
122
- Object.assign(typescriptDeclarations, tsParser.parseFiles());
140
+ Object.assign(typescriptDeclarations, await tsParser.parseFiles());
123
141
  mergeEventDefinitions(allEventDefinitions, tsParser.getEventDefinitions(), 'TS', spec.parserOptions.eventPrefix);
124
- });
142
+ }
125
143
  if (spec.parserOptions.eventPrefix !== '') {
126
144
  declarations.events.dataPoints = declarations.events.dataPoints.map((event) => {
127
145
  event.name = spec.parserOptions.eventPrefix + event.name;
@@ -152,7 +170,12 @@ export async function extractAndResolveDeclarations(sourceSpecs: Array<SourceSpe
152
170
  if (formattedDeclarations.events === undefined) {
153
171
  formattedDeclarations.events = Object.create(null);
154
172
  }
155
- formattedDeclarations.events[dec] = allTypeScriptDeclarations[dec];
173
+ const transformed = transformTypeScriptDeclaration(allTypeScriptDeclarations[dec]);
174
+ formattedDeclarations.events[dec] = transformed.declaration as OutputtedDeclarations['events'][string];
175
+ if (transformed.tableInfo) {
176
+ const tableInfo = transformed.tableInfo;
177
+ formattedDeclarations.tableInfos[tableInfo.name] = tableInfo;
178
+ }
156
179
  }
157
180
  const hasPropertyValidationErrors = validateOutputtedDeclarations(formattedDeclarations);
158
181
  if (hasDuplicateEventConflicts || hasPropertyValidationErrors || process.exitCode === 1) {
@@ -164,3 +187,4 @@ export async function extractAndResolveDeclarations(sourceSpecs: Array<SourceSpe
164
187
  return Promise.reject(error);
165
188
  }
166
189
  }
190
+
@@ -0,0 +1,38 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT license.
3
+ import { parentPort, workerData } from 'worker_threads';
4
+ import { Project } from 'ts-morph';
5
+ import { TsProjectParser, ParserWorkerRequest, ParserWorkerResult } from './ts-parser';
6
+
7
+ if (!parentPort) {
8
+ throw new Error('Telemetry parser worker requires a parent');
9
+ }
10
+
11
+ const request: ParserWorkerRequest = workerData;
12
+ const project = new Project({ compilerOptions: request.compilerOptions });
13
+ let result: ParserWorkerResult;
14
+
15
+ if (request.kind === 'prepare') {
16
+ for (const file of request.sourceFiles) {
17
+ project.addSourceFileAtPathIfExists(file);
18
+ }
19
+ result = new TsProjectParser(project, false, false).prepare();
20
+ } else if (request.kind === 'parse') {
21
+ // Keep global declarations and module augmentations visible in every batch.
22
+ for (const file of request.sharedSourceFiles) {
23
+ project.addSourceFileAtPath(file);
24
+ }
25
+ for (const group of request.calls) {
26
+ project.addSourceFileAtPath(group.filePath);
27
+ }
28
+ const parser = new TsProjectParser(project, request.applyEndpoints, request.lowerCaseEvents, request.definitions);
29
+ result = {
30
+ kind: 'parsed',
31
+ events: parser.parseFiles(request.calls, request.events),
32
+ definitions: [...parser.getEventDefinitions()]
33
+ };
34
+ } else {
35
+ throw new Error('Unknown telemetry parser worker request');
36
+ }
37
+
38
+ parentPort.postMessage(result);