@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.
- package/.github/dependabot.yml +11 -0
- package/.github/workflows/node.js.yml +3 -2
- package/eslint.config.js +6 -1
- package/out/cli-options.js +2 -1
- package/out/cli-options.js.map +1 -1
- package/out/extractor.js +7 -1
- package/out/extractor.js.map +1 -1
- package/out/lib/common-properties.js +51 -1
- package/out/lib/common-properties.js.map +1 -1
- package/out/lib/declarations.js.map +1 -1
- package/out/lib/event-definition.js +5 -0
- package/out/lib/event-definition.js.map +1 -0
- package/out/lib/events.js +28 -1
- package/out/lib/events.js.map +1 -1
- package/out/lib/object-converter.js +58 -4
- package/out/lib/object-converter.js.map +1 -1
- package/out/lib/operations.js +83 -66
- package/out/lib/operations.js.map +1 -1
- package/out/lib/parser.js +18 -23
- package/out/lib/parser.js.map +1 -1
- package/out/lib/ripgrep.js +14 -0
- package/out/lib/ripgrep.js.map +1 -0
- package/out/lib/save-declarations.js +32 -10
- package/out/lib/save-declarations.js.map +1 -1
- package/out/lib/ts-parser-worker.js +39 -0
- package/out/lib/ts-parser-worker.js.map +1 -0
- package/out/lib/ts-parser.js +204 -37
- package/out/lib/ts-parser.js.map +1 -1
- package/package.json +3 -3
- package/src/cli-options.ts +2 -1
- package/src/extractor.ts +8 -1
- package/src/lib/common-properties.ts +71 -5
- package/src/lib/declarations.ts +2 -1
- package/src/lib/event-definition.ts +7 -0
- package/src/lib/events.ts +33 -1
- package/src/lib/object-converter.ts +73 -4
- package/src/lib/operations.ts +75 -72
- package/src/lib/parser.ts +22 -34
- package/src/lib/ripgrep.ts +10 -0
- package/src/lib/save-declarations.ts +37 -13
- package/src/lib/ts-parser-worker.ts +38 -0
- package/src/lib/ts-parser.ts +247 -41
- package/tsconfig.json +1 -0
- 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();
|
package/src/lib/operations.ts
CHANGED
|
@@ -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 (!
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
|
142
|
+
const conflictProperties = this.extractConflictProperties(eventProperties);
|
|
147
143
|
const lineNumber = this.getLineNumber(fileContents, match.index);
|
|
148
|
-
this.addEventDefinition(eventName,
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (
|
|
152
|
-
|
|
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,
|
|
175
|
+
private addEventDefinition(eventName: string, properties: Record<string, unknown>, location: string) {
|
|
177
176
|
const existing = this.eventDefinitions.get(eventName) ?? [];
|
|
178
|
-
existing.push({
|
|
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
|
|
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 './
|
|
15
|
+
import { EventDefinition } from './event-definition';
|
|
16
16
|
|
|
17
17
|
interface EventConflictEntry {
|
|
18
|
-
|
|
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
|
-
|
|
96
|
-
if (signatures.size <= 1) {
|
|
95
|
+
if (entries.length <= 1) {
|
|
97
96
|
continue;
|
|
98
97
|
}
|
|
99
98
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
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
|
-
|
|
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);
|