@vscode/telemetry-extractor 1.9.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/LICENSE +21 -0
- package/README.md +29 -0
- package/SECURITY.md +41 -0
- package/documentation/comment-code-annotations.md +197 -0
- package/documentation/typescript-code-annotations.md +187 -0
- package/documentation/using-the-tool.md +106 -0
- package/out/cli-help.js +30 -0
- package/out/cli-help.js.map +1 -0
- package/out/cli-options.js +52 -0
- package/out/cli-options.js.map +1 -0
- package/out/extractor.js +83 -0
- package/out/extractor.js.map +1 -0
- package/out/index.js +10 -0
- package/out/index.js.map +1 -0
- package/out/lib/common-properties.js +32 -0
- package/out/lib/common-properties.js.map +1 -0
- package/out/lib/debug-patch.js +17 -0
- package/out/lib/debug-patch.js.map +1 -0
- package/out/lib/declarations.js +121 -0
- package/out/lib/declarations.js.map +1 -0
- package/out/lib/events.js +55 -0
- package/out/lib/events.js.map +1 -0
- package/out/lib/file-writer.js +77 -0
- package/out/lib/file-writer.js.map +1 -0
- package/out/lib/fragments.js +21 -0
- package/out/lib/fragments.js.map +1 -0
- package/out/lib/keywords.js +11 -0
- package/out/lib/keywords.js.map +1 -0
- package/out/lib/logger.js +10 -0
- package/out/lib/logger.js.map +1 -0
- package/out/lib/object-converter.js +107 -0
- package/out/lib/object-converter.js.map +1 -0
- package/out/lib/operations.js +118 -0
- package/out/lib/operations.js.map +1 -0
- package/out/lib/parser.js +191 -0
- package/out/lib/parser.js.map +1 -0
- package/out/lib/save-declarations.js +94 -0
- package/out/lib/save-declarations.js.map +1 -0
- package/out/lib/source-spec.js +77 -0
- package/out/lib/source-spec.js.map +1 -0
- package/out/lib/telemetry-interfaces.js +3 -0
- package/out/lib/telemetry-interfaces.js.map +1 -0
- package/out/lib/ts-parser.js +219 -0
- package/out/lib/ts-parser.js.map +1 -0
- package/package.json +39 -0
- package/src/cli-help.ts +29 -0
- package/src/cli-options.ts +31 -0
- package/src/extractor.ts +57 -0
- package/src/index.ts +4 -0
- package/src/lib/common-properties.ts +41 -0
- package/src/lib/debug-patch.ts +13 -0
- package/src/lib/declarations.ts +131 -0
- package/src/lib/events.ts +56 -0
- package/src/lib/file-writer.ts +54 -0
- package/src/lib/fragments.ts +24 -0
- package/src/lib/keywords.ts +7 -0
- package/src/lib/logger.ts +5 -0
- package/src/lib/object-converter.ts +85 -0
- package/src/lib/operations.ts +89 -0
- package/src/lib/parser.ts +184 -0
- package/src/lib/save-declarations.ts +71 -0
- package/src/lib/source-spec.ts +67 -0
- package/src/lib/telemetry-interfaces.ts +40 -0
- package/src/lib/ts-parser.ts +206 -0
- package/tsconfig.json +67 -0
- package/vscode-telemetry-extractor.d.ts +72 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT license.
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} outputFile
|
|
8
|
+
* @param {string} contents
|
|
9
|
+
* @returns {Promise}
|
|
10
|
+
*/
|
|
11
|
+
export async function writeFile(outputFile: string, contents: string) {
|
|
12
|
+
const directory = path.dirname(outputFile);
|
|
13
|
+
return mkdirp(directory).then((dir: string) => {
|
|
14
|
+
return new Promise<void>((resolve, reject) => {
|
|
15
|
+
fs.writeFile(outputFile, contents, { encoding: 'utf8' }, (err: any) => {
|
|
16
|
+
if (err) {
|
|
17
|
+
reject(err);
|
|
18
|
+
} else {
|
|
19
|
+
resolve();
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// copied from https://github.com/Microsoft/vscode/blob/master/src/main.js#L139
|
|
27
|
+
async function mkdirp(dir: string): Promise<any> {
|
|
28
|
+
return mkdir(dir)
|
|
29
|
+
.then(null, function (err: any) {
|
|
30
|
+
if (err && err.code === 'ENOENT') {
|
|
31
|
+
var parent = path.dirname(dir);
|
|
32
|
+
if (parent !== dir) { // if not arrived at root
|
|
33
|
+
return mkdirp(parent)
|
|
34
|
+
.then(function () {
|
|
35
|
+
return mkdir(dir);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
throw err;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// copied from https://github.com/Microsoft/vscode/blob/master/src/main.js#L155
|
|
44
|
+
async function mkdir(dir: string): Promise<any> {
|
|
45
|
+
return new Promise(function (resolve, reject) {
|
|
46
|
+
fs.mkdir(dir, function (err: any) {
|
|
47
|
+
if (err && err.code !== 'EEXIST') {
|
|
48
|
+
reject(err);
|
|
49
|
+
} else {
|
|
50
|
+
resolve(dir);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT license.
|
|
3
|
+
import { ITelemetryData, ITelemetryDataPoint, IProperty, IWildcard } from './telemetry-interfaces';
|
|
4
|
+
import { Property } from './common-properties';
|
|
5
|
+
import { Include, Inline } from './events';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
// Fragments are retrieved as an object of objects of objects. So this just makes it easier to see the structure.
|
|
9
|
+
export class Fragments implements ITelemetryData{
|
|
10
|
+
public dataPoints: Array<Fragment>;
|
|
11
|
+
constructor () {
|
|
12
|
+
this.dataPoints = [];
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class Fragment implements ITelemetryDataPoint {
|
|
17
|
+
public name: string;
|
|
18
|
+
public properties: Array<Property | Include | Inline | IWildcard>;
|
|
19
|
+
constructor (name: string) {
|
|
20
|
+
this.name = name;
|
|
21
|
+
this.properties = [];
|
|
22
|
+
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT license.
|
|
3
|
+
export const wildcard = '${wildcard}';
|
|
4
|
+
export const inline = '${inline}';
|
|
5
|
+
export const include = '${include}';
|
|
6
|
+
export const prefix = '${prefix}';
|
|
7
|
+
export const classification = '${classification}';
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT license.
|
|
3
|
+
import { OutputtedDeclarations } from "./declarations";
|
|
4
|
+
import { Property } from "./common-properties";
|
|
5
|
+
import { Wildcard } from "./events";
|
|
6
|
+
import * as keywords from './keywords';
|
|
7
|
+
|
|
8
|
+
// Converts the declarations array to an object format for easy readability.
|
|
9
|
+
|
|
10
|
+
export async function transformOutput(output: OutputtedDeclarations): Promise<OutputtedDeclarations> {
|
|
11
|
+
const newEvents = Object.create(null);
|
|
12
|
+
const oldEvents = output.events.dataPoints;
|
|
13
|
+
for (const event of oldEvents) {
|
|
14
|
+
newEvents[event.name] = Object.create(null);
|
|
15
|
+
//We know there won't be anymore includes or inlines because we have resolved them
|
|
16
|
+
for (const property of event.properties as Array<Property | Wildcard>) {
|
|
17
|
+
if (property instanceof Wildcard) {
|
|
18
|
+
newEvents[event.name][keywords.wildcard] = newEvents[event.name][keywords.wildcard] ? newEvents[event.name][keywords.wildcard] : [];
|
|
19
|
+
for (const entry of property.entries) {
|
|
20
|
+
const found = newEvents[event.name][keywords.wildcard].find((e: any) => {
|
|
21
|
+
return e[keywords.prefix] === entry.prefix.toLowerCase();
|
|
22
|
+
});
|
|
23
|
+
if (found) continue;
|
|
24
|
+
const newEntry = Object.create(null);
|
|
25
|
+
newEntry[keywords.prefix] = entry.prefix.toLowerCase();
|
|
26
|
+
if (entry.endpoint) {
|
|
27
|
+
newEntry[keywords.classification] = { classification: entry.classification.classification, purpose: entry.classification.purpose, endPoint: "none" };
|
|
28
|
+
} else {
|
|
29
|
+
newEntry[keywords.classification] = entry.classification;
|
|
30
|
+
}
|
|
31
|
+
newEvents[event.name][keywords.wildcard].push(newEntry);
|
|
32
|
+
}
|
|
33
|
+
} else {
|
|
34
|
+
// Handles the case where the comments can be inconsistent
|
|
35
|
+
// We want to ensure that if isMeasurement is ever flagged it gets propogated
|
|
36
|
+
if (newEvents[event.name][propetyNameChanger(property.name)]) {
|
|
37
|
+
if (property.isMeasurement) newEvents[event.name][propetyNameChanger(property.name)]['isMeasurement'] = property.isMeasurement;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
newEvents[event.name][propetyNameChanger(property.name)] = { classification: property.classification, purpose: property.purpose };
|
|
41
|
+
if (property.expiration) {
|
|
42
|
+
newEvents[event.name][propetyNameChanger(property.name)]['expiration'] = property.expiration;
|
|
43
|
+
}
|
|
44
|
+
if (property.owner) {
|
|
45
|
+
newEvents[event.name][propetyNameChanger(property.name)]['owner'] = property.owner;
|
|
46
|
+
}
|
|
47
|
+
if (property.comment) {
|
|
48
|
+
newEvents[event.name][propetyNameChanger(property.name)]['comment'] = property.comment;
|
|
49
|
+
}
|
|
50
|
+
if (property.endPoint) {
|
|
51
|
+
newEvents[event.name][propetyNameChanger(property.name)]['endPoint'] = property.endPoint;
|
|
52
|
+
}
|
|
53
|
+
if (property.isMeasurement) {
|
|
54
|
+
newEvents[event.name][propetyNameChanger(property.name)]['isMeasurement'] = property.isMeasurement;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const newCommonProperties = Object.create(null);
|
|
60
|
+
const oldCommonProperties = output.commonProperties.properties;
|
|
61
|
+
for (const property of oldCommonProperties) {
|
|
62
|
+
// Handles the case where the comments can be incosistent
|
|
63
|
+
// We want to ensure that if isMeasurement is ever flagged it gets propogated
|
|
64
|
+
if (newCommonProperties[propetyNameChanger(property.name)]) {
|
|
65
|
+
if (property.isMeasurement) newCommonProperties[propetyNameChanger(property.name)]['isMeasurement'] = property.isMeasurement;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
newCommonProperties[propetyNameChanger(property.name)] = { classification: property.classification, purpose: property.purpose };
|
|
69
|
+
if (property.endPoint) {
|
|
70
|
+
newCommonProperties[propetyNameChanger(property.name)]['endPoint'] = property.endPoint;
|
|
71
|
+
}
|
|
72
|
+
if (property.isMeasurement) {
|
|
73
|
+
newCommonProperties[propetyNameChanger(property.name)]['isMeasurement'] = property.isMeasurement;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { events: newEvents, commonProperties: newCommonProperties };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function propetyNameChanger(name: string) {
|
|
80
|
+
name = name.toLowerCase();
|
|
81
|
+
if (name.includes('<number>')) {
|
|
82
|
+
name = name.replace('<number>', '<NUMBER>');
|
|
83
|
+
}
|
|
84
|
+
return name;
|
|
85
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT license.
|
|
3
|
+
import { Fragments, Fragment } from "./fragments";
|
|
4
|
+
import { Events, Event, Include, Inline, Wildcard, WildcardEntry } from "./events";
|
|
5
|
+
import { Property } from "./common-properties";
|
|
6
|
+
import * as keywords from './keywords';
|
|
7
|
+
|
|
8
|
+
export function merge(target: Fragments | Events, source: Fragments | Events) {
|
|
9
|
+
for (const item of source.dataPoints) {
|
|
10
|
+
const found = target.dataPoints.find((f) => {
|
|
11
|
+
return f.name == item.name;
|
|
12
|
+
});
|
|
13
|
+
// We combine their properties together if the event already exists
|
|
14
|
+
if (found) {
|
|
15
|
+
found.properties = found.properties.concat(item.properties);
|
|
16
|
+
} else {
|
|
17
|
+
target.dataPoints.push(item);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Searches the object for an event or fragment of the specific name
|
|
23
|
+
// If found returns, if not found creates it, places it in the array, and then returns
|
|
24
|
+
export function findOrCreate(searchTarget: Events | Fragments, name: string) {
|
|
25
|
+
let found = searchTarget.dataPoints.find((item) => {
|
|
26
|
+
return item.name === name;
|
|
27
|
+
});
|
|
28
|
+
if (!found) {
|
|
29
|
+
if (searchTarget instanceof Events) {
|
|
30
|
+
found = new Event(name);
|
|
31
|
+
} else {
|
|
32
|
+
found = new Fragment(name);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
searchTarget.dataPoints.push(found);
|
|
36
|
+
return found;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function mergeWildcards(wildcard: any[], target: Event | Fragment, applyEndpoints: boolean) {
|
|
40
|
+
let wildCard = target.properties.find((item) => {
|
|
41
|
+
return item instanceof Wildcard;
|
|
42
|
+
}) as Wildcard;
|
|
43
|
+
// if we don't have a wildcard yet we make one
|
|
44
|
+
if (!wildCard) {
|
|
45
|
+
wildCard = new Wildcard();
|
|
46
|
+
target.properties.push(wildCard);
|
|
47
|
+
}
|
|
48
|
+
wildcard.forEach(w => {
|
|
49
|
+
if (applyEndpoints) {
|
|
50
|
+
wildCard.entries.push(new WildcardEntry(w[keywords.prefix], w[keywords.classification], 'none'));
|
|
51
|
+
} else {
|
|
52
|
+
wildCard.entries.push(new WildcardEntry(w[keywords.prefix], w[keywords.classification]));
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function populateProperties(properties: any, target: Event | Fragment, applyEndpoints = false) {
|
|
58
|
+
for (const propertyName in properties) {
|
|
59
|
+
const currentProperty = properties[propertyName];
|
|
60
|
+
if (propertyName === keywords.include) {
|
|
61
|
+
target.properties.push(new Include(currentProperty));
|
|
62
|
+
} else if (currentProperty[keywords.inline]) {
|
|
63
|
+
// We consider the property name the inline name so when we resolve we can do prop.Inline for the new properties
|
|
64
|
+
target.properties.push(new Inline(propertyName, currentProperty[keywords.inline]));
|
|
65
|
+
} else if (propertyName === keywords.wildcard) {
|
|
66
|
+
mergeWildcards(currentProperty, target, applyEndpoints);
|
|
67
|
+
} else {
|
|
68
|
+
const prop = new Property(propertyName, currentProperty.classification, currentProperty.purpose, currentProperty.expiration, currentProperty.owner, currentProperty.comment);
|
|
69
|
+
if (applyEndpoints) {
|
|
70
|
+
const endpoint = currentProperty.endpoint ? currentProperty.endpoint : 'none';
|
|
71
|
+
prop.endPoint = endpoint;
|
|
72
|
+
}
|
|
73
|
+
if (currentProperty.isMeasurement) {
|
|
74
|
+
prop.isMeasurement = currentProperty.isMeasurement;
|
|
75
|
+
}
|
|
76
|
+
target.properties.push(prop);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function makeExclusionsRelativeToSource(sourceDir: string, excludedDirs: string[]) {
|
|
82
|
+
const relativeExclusions = [];
|
|
83
|
+
for (const excluded of excludedDirs) {
|
|
84
|
+
if (excluded.includes(sourceDir)) {
|
|
85
|
+
relativeExclusions.push(excluded.replace(sourceDir, ''));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return relativeExclusions;
|
|
89
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT license.
|
|
3
|
+
import { rgPath } from 'vscode-ripgrep';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as cp from 'child_process';
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import { Fragments } from './fragments';
|
|
8
|
+
import { Property, CommonProperties } from './common-properties';
|
|
9
|
+
import { Events } from './events';
|
|
10
|
+
import { Declarations } from './declarations';
|
|
11
|
+
import { merge, findOrCreate, populateProperties, makeExclusionsRelativeToSource } from './operations';
|
|
12
|
+
|
|
13
|
+
export class Parser {
|
|
14
|
+
|
|
15
|
+
private sourceDirs: string[];
|
|
16
|
+
private excludedDirs: string[];
|
|
17
|
+
private applyEndpoints: boolean;
|
|
18
|
+
|
|
19
|
+
private lowerCaseEvents: boolean;
|
|
20
|
+
|
|
21
|
+
constructor(sourceDirs: string[], excludedDirs: string[], applyEndpoints: boolean, lowerCaseEvents: boolean) {
|
|
22
|
+
this.sourceDirs = sourceDirs;
|
|
23
|
+
this.excludedDirs = excludedDirs;
|
|
24
|
+
this.applyEndpoints = applyEndpoints;
|
|
25
|
+
this.lowerCaseEvents = lowerCaseEvents;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private toRipGrepOption(dir: string) {
|
|
29
|
+
while (dir.startsWith('/')) {
|
|
30
|
+
dir = dir.substr(1);
|
|
31
|
+
}
|
|
32
|
+
return `--glob "!${dir}/**" `;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private extractComments(absoluteFilePaths: string[], commentMatcher: RegExp, collector: Function) {
|
|
36
|
+
absoluteFilePaths.forEach(absoluteFilePath => {
|
|
37
|
+
if (absoluteFilePath) {
|
|
38
|
+
const fileContents = fs.readFileSync(absoluteFilePath);
|
|
39
|
+
let match;
|
|
40
|
+
while (match = commentMatcher.exec(fileContents.toString())) {
|
|
41
|
+
collector(absoluteFilePath, match);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Converts relative paths to absolute utilizing the CWD
|
|
49
|
+
private asAbsoluteFilePaths(relativeFilePaths: string[]) {
|
|
50
|
+
return relativeFilePaths.map(r => path.resolve(r));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Finds all files containing common telemetry properties in the given directory
|
|
54
|
+
private findFilesWithCommonProperties(sourceDir: string) {
|
|
55
|
+
const ripgrepPattern = '//\\s*__GDPR__COMMON__';
|
|
56
|
+
return this.findFiles(ripgrepPattern, sourceDir);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private findCommonProperties(sourceDir: string) {
|
|
60
|
+
const filesWithCommonProperties = this.asAbsoluteFilePaths(this.findFilesWithCommonProperties(sourceDir));
|
|
61
|
+
|
|
62
|
+
const commonPropertyMatcher = /\/\/\s*__GDPR__COMMON__(.*)$/mg;
|
|
63
|
+
const commonPropertyDeclarations = new CommonProperties();
|
|
64
|
+
this.extractComments(filesWithCommonProperties, commonPropertyMatcher, (filePath: string, match: Array<string>) => {
|
|
65
|
+
try {
|
|
66
|
+
const commonPropertyDeclaration = JSON.parse(`{ ${match[1]} }`);
|
|
67
|
+
const propertyName = Object.keys(commonPropertyDeclaration)[0];
|
|
68
|
+
const properties = commonPropertyDeclaration[propertyName];
|
|
69
|
+
// Add all the common properties to the common property object
|
|
70
|
+
const prop = new Property(propertyName, properties.classification, properties.purpose);
|
|
71
|
+
if (this.applyEndpoints) {
|
|
72
|
+
const endpoint = properties.endPoint ? properties.endPoint : 'none';
|
|
73
|
+
prop.endPoint = endpoint;
|
|
74
|
+
}
|
|
75
|
+
if (properties.isMeasurement) {
|
|
76
|
+
prop.isMeasurement = properties.isMeasurement;
|
|
77
|
+
}
|
|
78
|
+
commonPropertyDeclarations.properties.push(prop);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error(`Common Property Declaration Error: ${error} in file ${filePath}`);
|
|
81
|
+
console.error(`Source comment:\n${match[0]}`);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
return commonPropertyDeclarations;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Finds all files containing event fragments in the given directory
|
|
88
|
+
private findFilesWithFragments(sourceDir: string) {
|
|
89
|
+
const ripgrepPattern = '/\*\\s*__GDPR__FRAGMENT__';
|
|
90
|
+
return this.findFiles(ripgrepPattern, sourceDir);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
///
|
|
94
|
+
private findFragments(sourceDir: string) {
|
|
95
|
+
const filesWithFragments = this.asAbsoluteFilePaths(this.findFilesWithFragments(sourceDir));
|
|
96
|
+
|
|
97
|
+
// Using [\s\S]* instead of .* since the latter does not match when using /m option
|
|
98
|
+
const fragmentMatcher = /\/\*\s*__GDPR__FRAGMENT__([\s\S]*?)\*\//mg;
|
|
99
|
+
const fragmentDeclarations = new Fragments();
|
|
100
|
+
this.extractComments(filesWithFragments, fragmentMatcher, (filePath: string, match: Array<string>) => {
|
|
101
|
+
try {
|
|
102
|
+
const fragmentDeclaration = JSON.parse(`{ ${match[1]} }`);
|
|
103
|
+
// There's only ever one key per match
|
|
104
|
+
const fragmentName = Object.keys(fragmentDeclaration)[0];
|
|
105
|
+
// Checks to see if we have a fragment of the given name, else creates.
|
|
106
|
+
const fragment = findOrCreate(fragmentDeclarations, fragmentName);
|
|
107
|
+
const fragmentProperties = fragmentDeclaration[fragmentName];
|
|
108
|
+
populateProperties(fragmentProperties, fragment, this.applyEndpoints);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error(`Fragment Declaration Error: ${error} in file ${filePath}`);
|
|
111
|
+
console.error(`Source comment:\n${match[0]}`);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
return fragmentDeclarations;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Find all files with complete events
|
|
118
|
+
private findFilesWithEvents(sourceDir: string) {
|
|
119
|
+
const ripgrepPattern = '/\*\\s*__GDPR__\\b';
|
|
120
|
+
return this.findFiles(ripgrepPattern, sourceDir);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private findEvents(sourceDir: string) {
|
|
124
|
+
const filesWithEvents = this.asAbsoluteFilePaths(this.findFilesWithEvents(sourceDir));
|
|
125
|
+
|
|
126
|
+
// Using [\s\S]* instead of .* since the latter does not match when using /m option
|
|
127
|
+
const eventMatcher = /\/\*\s*__GDPR__\b([\s\S]*?)\*\//mg;
|
|
128
|
+
const eventDeclarations = new Events();
|
|
129
|
+
this.extractComments(filesWithEvents, eventMatcher, (filePath: string, match: Array<string>) => {
|
|
130
|
+
try {
|
|
131
|
+
const eventDeclaration = JSON.parse(`{ ${match[1]} }`);
|
|
132
|
+
let eventName = Object.keys(eventDeclaration)[0];
|
|
133
|
+
eventName = this.lowerCaseEvents ? eventName.toLowerCase() : eventName;
|
|
134
|
+
const event = findOrCreate(eventDeclarations, eventName);
|
|
135
|
+
// Get the propeties which the event possesses
|
|
136
|
+
const eventProperties = eventDeclaration[Object.keys(eventDeclaration)[0]];
|
|
137
|
+
populateProperties(eventProperties, event, this.applyEndpoints);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
console.error(`Event Declaration Error: ${error} in file ${filePath}`);
|
|
140
|
+
console.error(`Source comment:\n${match[0]}`);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
return eventDeclarations;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Utilizes a regex to find the files containing the specific pattern
|
|
147
|
+
private findFiles(ripgrepPattern: string, sourceDir: string) {
|
|
148
|
+
const relativeExclusions = makeExclusionsRelativeToSource(sourceDir, this.excludedDirs);
|
|
149
|
+
const exclusions = relativeExclusions.length === 0 || relativeExclusions[0] === '' ? '' : relativeExclusions.map(this.toRipGrepOption).join('');
|
|
150
|
+
const cmd = `${rgPath} --files-with-matches --glob "*.ts" ${exclusions} --regexp "${ripgrepPattern}" -- ${sourceDir}`;
|
|
151
|
+
try {
|
|
152
|
+
let filePaths = cp.execSync(cmd, { encoding: 'ascii', cwd: `${sourceDir}` });
|
|
153
|
+
return filePaths.split(/(?:\r\n|\r|\n)/g).filter(path => path && path.length > 0);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
// ripgrep's return code != 0 if there are no matches
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private async parse(sourceDir: string): Promise<Declarations> {
|
|
161
|
+
const fragments = this.findFragments(sourceDir);
|
|
162
|
+
const events = this.findEvents(sourceDir);
|
|
163
|
+
const commonProperties = this.findCommonProperties(sourceDir);
|
|
164
|
+
return { fragments: fragments, events: events, commonProperties: commonProperties };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
public extractDeclarations(): Promise<Declarations> {
|
|
168
|
+
return new Promise((resolve, reject) => {
|
|
169
|
+
// Find all the properties for all files
|
|
170
|
+
const promises = this.sourceDirs.map(sd => this.parse(sd));
|
|
171
|
+
// Now we must merge them into one superset of all declarations
|
|
172
|
+
Promise.all(promises).then(parseResult => {
|
|
173
|
+
const declarations = { fragments: new Fragments(), events: new Events(), commonProperties: new CommonProperties() };
|
|
174
|
+
for (const currentResult of parseResult) {
|
|
175
|
+
merge(declarations.fragments, currentResult.fragments);
|
|
176
|
+
merge(declarations.events, currentResult.events);
|
|
177
|
+
// We just concatenate common properties
|
|
178
|
+
declarations.commonProperties.properties = declarations.commonProperties.properties.concat(currentResult.commonProperties.properties);
|
|
179
|
+
}
|
|
180
|
+
return resolve(declarations);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT license.
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { Parser } from './parser';
|
|
5
|
+
import * as fileWriter from './file-writer';
|
|
6
|
+
import { resolveDeclarations, OutputtedDeclarations } from './declarations';
|
|
7
|
+
import { transformOutput } from './object-converter';
|
|
8
|
+
import { Events } from './events';
|
|
9
|
+
import { CommonProperties } from './common-properties';
|
|
10
|
+
import { TsParser } from './ts-parser';
|
|
11
|
+
import { patchDebugEvents } from './debug-patch';
|
|
12
|
+
import { ParserOptions, SourceSpec } from './source-spec';
|
|
13
|
+
import { logMessage } from './logger';
|
|
14
|
+
|
|
15
|
+
export function writeToFile(outputDir: string, contents: object, fileName: string, emitProgressMessage: boolean) {
|
|
16
|
+
const json = JSON.stringify(contents);
|
|
17
|
+
const outputFile = path.resolve(outputDir, `${fileName}.json`);
|
|
18
|
+
logMessage(`...writing ${outputFile}`, !emitProgressMessage);
|
|
19
|
+
return fileWriter.writeFile(outputFile, json);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function getResolvedDeclaration(sourceDirs: Array<string>, excludedDirs: Array<string>, options: ParserOptions) {
|
|
23
|
+
logMessage('...extracting', options.silenceOutput);
|
|
24
|
+
const parser = new Parser(sourceDirs, excludedDirs, options.applyEndpoints, options.lowerCaseEvents);
|
|
25
|
+
let declarations = await parser.extractDeclarations();
|
|
26
|
+
declarations = resolveDeclarations(declarations, options.verbose);
|
|
27
|
+
return declarations;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function extractAndResolveDeclarations(sourceSpecs: Array<SourceSpec>): Promise<{ events: any, commonProperties: any }> {
|
|
31
|
+
try {
|
|
32
|
+
const allDeclarations: OutputtedDeclarations = { events: new Events(), commonProperties: new CommonProperties() };
|
|
33
|
+
const allTypeScriptDeclarations = Object.create(null);
|
|
34
|
+
for (const spec of sourceSpecs) {
|
|
35
|
+
const declarations = await getResolvedDeclaration(spec.sourceDirs, spec.excludedDirs, spec.parserOptions);
|
|
36
|
+
let typescriptDeclarations = Object.create(null);
|
|
37
|
+
// 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
|
|
38
|
+
spec.sourceDirs.forEach((dir) => {
|
|
39
|
+
Object.assign(typescriptDeclarations, new TsParser(dir, spec.excludedDirs, spec.parserOptions.applyEndpoints, spec.parserOptions.lowerCaseEvents).parseFiles());
|
|
40
|
+
});
|
|
41
|
+
if (spec.parserOptions.eventPrefix !== '') {
|
|
42
|
+
declarations.events.dataPoints = declarations.events.dataPoints.map((event) => {
|
|
43
|
+
event.name = spec.parserOptions.eventPrefix + event.name;
|
|
44
|
+
return event;
|
|
45
|
+
});
|
|
46
|
+
const modifiedDeclartions = Object.create(null);
|
|
47
|
+
// Modify the object keys to be prefixed with the specified prefix
|
|
48
|
+
for (const key in typescriptDeclarations) {
|
|
49
|
+
modifiedDeclartions[spec.parserOptions.eventPrefix + key] = typescriptDeclarations[key];
|
|
50
|
+
}
|
|
51
|
+
typescriptDeclarations = modifiedDeclartions;
|
|
52
|
+
}
|
|
53
|
+
if (spec.parserOptions.patchDebugEvents) {
|
|
54
|
+
patchDebugEvents(declarations.events, spec.parserOptions.eventPrefix);
|
|
55
|
+
}
|
|
56
|
+
// We concatenate each extensions properties into a central one
|
|
57
|
+
// Throwing out fragments as they have already been used to resolve that extensions declarations
|
|
58
|
+
allDeclarations.commonProperties.properties = allDeclarations.commonProperties.properties.concat(declarations.commonProperties.properties);
|
|
59
|
+
allDeclarations.events.dataPoints = allDeclarations.events.dataPoints.concat(declarations.events.dataPoints);
|
|
60
|
+
Object.assign(allTypeScriptDeclarations, typescriptDeclarations);
|
|
61
|
+
}
|
|
62
|
+
const formattedDeclarations: any = await transformOutput(allDeclarations);
|
|
63
|
+
for (const dec in allTypeScriptDeclarations) {
|
|
64
|
+
formattedDeclarations.events[dec] = allTypeScriptDeclarations[dec];
|
|
65
|
+
}
|
|
66
|
+
return Promise.resolve(formattedDeclarations);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error(`Error: ${error}`);
|
|
69
|
+
return Promise.reject(error);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT license.
|
|
3
|
+
import { PathLike, readFileSync } from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { cwd } from 'process';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
export interface ParserOptions {
|
|
9
|
+
eventPrefix: string;
|
|
10
|
+
applyEndpoints: boolean;
|
|
11
|
+
patchDebugEvents: boolean;
|
|
12
|
+
lowerCaseEvents: boolean;
|
|
13
|
+
silenceOutput: boolean;
|
|
14
|
+
verbose: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SourceSpec {
|
|
18
|
+
sourceDirs: string[],
|
|
19
|
+
excludedDirs: string[],
|
|
20
|
+
parserOptions: ParserOptions
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function convertConfigToSourceSpecs(file: PathLike): SourceSpec[] {
|
|
24
|
+
try {
|
|
25
|
+
const config = JSON.parse(readFileSync(file).toString());
|
|
26
|
+
const sourceSpecs: SourceSpec[] = [];
|
|
27
|
+
for (const key in config) {
|
|
28
|
+
const spec = config[key];
|
|
29
|
+
// Some defaults
|
|
30
|
+
spec.excludedDirs = spec.excludedDirs ? spec.excludedDirs : [];
|
|
31
|
+
spec.workingDir = spec.workingDir ? spec.workingDir : cwd();
|
|
32
|
+
spec.patchDebugEvents = spec.patchDebugEvents ? spec.patchDebugEvents : false;
|
|
33
|
+
spec.lowerCaseEvents = spec.lowerCaseEvents ? spec.lowerCaseEvents : false;
|
|
34
|
+
const parserOptions: ParserOptions = {
|
|
35
|
+
eventPrefix: spec.eventPrefix ? spec.eventPrefix : '',
|
|
36
|
+
applyEndpoints: spec.applyEndpoints,
|
|
37
|
+
patchDebugEvents: spec.patchDebugEvents,
|
|
38
|
+
lowerCaseEvents: spec.lowerCaseEvents,
|
|
39
|
+
silenceOutput: spec.silenceOuput,
|
|
40
|
+
verbose: spec.verbose
|
|
41
|
+
}
|
|
42
|
+
const sourceSpec: SourceSpec = {
|
|
43
|
+
sourceDirs: resolveDirectories(spec.sourceDirs, spec.workingDir),
|
|
44
|
+
excludedDirs: resolveDirectories(spec.excludedDirs, spec.workingDir),
|
|
45
|
+
parserOptions: parserOptions
|
|
46
|
+
}
|
|
47
|
+
sourceSpecs.push(sourceSpec);
|
|
48
|
+
}
|
|
49
|
+
return sourceSpecs;
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.error(err);
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Resolves an array of paths
|
|
57
|
+
function resolveDirectories(dirs: string[], workingDir?: string): string[] {
|
|
58
|
+
if (workingDir) {
|
|
59
|
+
if (path.isAbsolute(workingDir)) {
|
|
60
|
+
return dirs.map(s => path.resolve(workingDir, s));
|
|
61
|
+
} else {
|
|
62
|
+
return dirs.map(s => path.resolve(cwd(), workingDir, s));
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
return dirs.map(s => path.resolve(cwd(), s));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT license.
|
|
3
|
+
import { Wildcard } from "./events";
|
|
4
|
+
|
|
5
|
+
export interface ITelemetryData{
|
|
6
|
+
dataPoints: Array<ITelemetryDataPoint>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ITelemetryDataPoint {
|
|
10
|
+
name: string;
|
|
11
|
+
properties: Array<IProperty | IInclude | IInline | Wildcard>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface IProperty {
|
|
15
|
+
name: string;
|
|
16
|
+
purpose: string;
|
|
17
|
+
classification: string;
|
|
18
|
+
expiration?: string;
|
|
19
|
+
owner?: string;
|
|
20
|
+
comment?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface IInclude {
|
|
24
|
+
includeNames: Array<string>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface IInline {
|
|
28
|
+
inlineName: string;
|
|
29
|
+
inlines: Array<string>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface IWildcard {
|
|
33
|
+
entries: Array<IWildcardEntry>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface IWildcardEntry {
|
|
37
|
+
prefix: string;
|
|
38
|
+
classification: {classification: string, purpose: string};
|
|
39
|
+
endpoint: string | undefined;
|
|
40
|
+
}
|