|
| 1 | +// @ts-check |
| 2 | +import * as ts from 'typescript'; |
| 3 | +import * as fs from 'fs'; |
| 4 | +import * as path from 'path'; |
| 5 | +import { fileURLToPath } from 'url'; |
| 6 | + |
| 7 | +const __filename = fileURLToPath(import.meta.url); |
| 8 | +const __dirname = path.join(path.dirname(__filename), '..'); |
| 9 | + |
| 10 | +const filePath = path.join(__dirname, 'src/constants.telemetry.ts'); |
| 11 | + |
| 12 | +const program = ts.createProgram([filePath], {}); |
| 13 | +const sourceFile = program.getSourceFile(filePath); |
| 14 | +const typeChecker = program.getTypeChecker(); |
| 15 | + |
| 16 | +if (!sourceFile) { |
| 17 | + throw new Error(`Could not find source file: ${filePath}`); |
| 18 | +} |
| 19 | + |
| 20 | +let telemetryEventsType; |
| 21 | +let telemetryGlobalContext; |
| 22 | + |
| 23 | +// Find the types |
| 24 | +ts.forEachChild(sourceFile, node => { |
| 25 | + if (ts.isTypeAliasDeclaration(node)) { |
| 26 | + switch (node.name.text) { |
| 27 | + case 'TelemetryEvents': |
| 28 | + telemetryEventsType = typeChecker.getTypeAtLocation(node); |
| 29 | + break; |
| 30 | + case 'TelemetryGlobalContext': |
| 31 | + telemetryGlobalContext = typeChecker.getTypeAtLocation(node); |
| 32 | + break; |
| 33 | + } |
| 34 | + } |
| 35 | +}); |
| 36 | + |
| 37 | +if (!telemetryEventsType || !telemetryGlobalContext) { |
| 38 | + throw new Error('Could not find the telemetry types'); |
| 39 | +} |
| 40 | + |
| 41 | +// Generate markdown |
| 42 | +let markdown = '# GitLens Telemetry\n\n'; |
| 43 | +markdown += '> This is a generated file. Do not edit.\n\n'; |
| 44 | + |
| 45 | +markdown += '## Global Attributes\n\n'; |
| 46 | +markdown += '> Global attributes are sent (if available) with every telemetry event\n\n'; |
| 47 | + |
| 48 | +markdown += `${expandType(telemetryGlobalContext, '', true, 'global.')}\n\n`; |
| 49 | + |
| 50 | +markdown += '## Events\n\n'; |
| 51 | + |
| 52 | +const properties = typeChecker.getPropertiesOfType(telemetryEventsType); |
| 53 | +for (const prop of properties) { |
| 54 | + const propType = typeChecker.getTypeOfSymbolAtLocation(prop, sourceFile); |
| 55 | + |
| 56 | + markdown += `### ${prop.name}\n\n`; |
| 57 | + |
| 58 | + // Add property documentation if available |
| 59 | + const propDocs = prop.getDocumentationComment(typeChecker); |
| 60 | + if (propDocs.length > 0) { |
| 61 | + markdown += `> ${propDocs.map(doc => doc.text).join('\n> ')}\n\n`; |
| 62 | + } |
| 63 | + |
| 64 | + // Check for deprecated tag |
| 65 | + const jsDocTags = getJSDocTags(prop); |
| 66 | + if (jsDocTags.deprecated) { |
| 67 | + markdown += `> **Deprecated:** ${ |
| 68 | + jsDocTags.deprecated === true ? 'This property is deprecated.' : jsDocTags.deprecated |
| 69 | + }\n\n`; |
| 70 | + } |
| 71 | + |
| 72 | + markdown += `${expandType(propType, '')}\n\n`; |
| 73 | +} |
| 74 | + |
| 75 | +const outputPath = path.join(__dirname, 'docs/telemetry-events.md'); |
| 76 | +fs.writeFileSync(outputPath, markdown); |
| 77 | + |
| 78 | +function expandType(type, indent = '', isRoot = true, prefix = '') { |
| 79 | + let result = ''; |
| 80 | + |
| 81 | + if (type.isUnion()) { |
| 82 | + if (isRoot) { |
| 83 | + return type.types |
| 84 | + .map(t => `\`\`\`typescript\n${expandType(t, '', false, prefix)}\n\`\`\``) |
| 85 | + .join('\n\nor\n\n'); |
| 86 | + } else { |
| 87 | + const types = type.types.map(t => expandType(t, indent, false, prefix)).join(' | '); |
| 88 | + result = types.includes('\n') ? `(${types})` : types; |
| 89 | + } |
| 90 | + } else if (type.isIntersection()) { |
| 91 | + const combinedProperties = new Map(); |
| 92 | + type.types.forEach(t => { |
| 93 | + if (t.symbol && t.symbol.flags & ts.SymbolFlags.TypeLiteral) { |
| 94 | + typeChecker.getPropertiesOfType(t).forEach(prop => { |
| 95 | + combinedProperties.set(prop.name, prop); |
| 96 | + }); |
| 97 | + } |
| 98 | + }); |
| 99 | + |
| 100 | + if (combinedProperties.size > 0) { |
| 101 | + const expandedProps = Array.from(combinedProperties).map(([name, prop]) => { |
| 102 | + const propType = typeChecker.getTypeOfSymbolAtLocation(prop, sourceFile); |
| 103 | + const jsDocTags = getJSDocTags(prop); |
| 104 | + let propString = ''; |
| 105 | + if (jsDocTags.deprecated) { |
| 106 | + propString += `${indent} // @deprecated: ${ |
| 107 | + jsDocTags.deprecated === true ? '' : jsDocTags.deprecated |
| 108 | + }\n`; |
| 109 | + } |
| 110 | + propString += `${indent} '${prefix}${name}': ${expandType(propType, indent + ' ', false, prefix)}`; |
| 111 | + return propString; |
| 112 | + }); |
| 113 | + result = `{\n${expandedProps.join(',\n')}\n${indent}}`; |
| 114 | + } else { |
| 115 | + const types = type.types.map(t => expandType(t, indent, false, prefix)).join(' & '); |
| 116 | + result = types.includes('\n') ? `(${types})` : types; |
| 117 | + } |
| 118 | + } else if (type.isStringLiteral()) { |
| 119 | + result = `'${type.value}'`; |
| 120 | + } else if (type.isNumberLiteral()) { |
| 121 | + result = type.value.toString(); |
| 122 | + } else if (type.symbol && type.symbol.flags & ts.SymbolFlags.Method) { |
| 123 | + const signatures = type.getCallSignatures(); |
| 124 | + if (signatures.length) { |
| 125 | + const params = signatures[0] |
| 126 | + .getParameters() |
| 127 | + .map( |
| 128 | + p => |
| 129 | + `'${prefix}${p.name}': ${expandType( |
| 130 | + typeChecker.getTypeOfSymbolAtLocation(p, sourceFile), |
| 131 | + indent, |
| 132 | + false, |
| 133 | + prefix, |
| 134 | + )}`, |
| 135 | + ) |
| 136 | + .join(', '); |
| 137 | + const returnType = expandType(signatures[0].getReturnType(), indent, false, prefix); |
| 138 | + result = `(${params}) => ${returnType}`; |
| 139 | + } |
| 140 | + } else if (type.symbol && type.symbol.flags & ts.SymbolFlags.TypeLiteral) { |
| 141 | + const properties = typeChecker.getPropertiesOfType(type); |
| 142 | + if (properties.length === 0) { |
| 143 | + result = '{}'; |
| 144 | + } else { |
| 145 | + const expandedProps = properties.map(prop => { |
| 146 | + const propType = typeChecker.getTypeOfSymbolAtLocation(prop, sourceFile); |
| 147 | + const jsDocTags = getJSDocTags(prop); |
| 148 | + let propString = ''; |
| 149 | + if (jsDocTags.deprecated) { |
| 150 | + propString += `${indent} // @deprecated: ${ |
| 151 | + jsDocTags.deprecated === true ? '' : jsDocTags.deprecated |
| 152 | + }\n`; |
| 153 | + } |
| 154 | + propString += `${indent} '${prefix}${prop.name}': ${expandType( |
| 155 | + propType, |
| 156 | + indent + ' ', |
| 157 | + false, |
| 158 | + prefix, |
| 159 | + )}`; |
| 160 | + return propString; |
| 161 | + }); |
| 162 | + result = `{\n${expandedProps.join(',\n')}\n${indent}}`; |
| 163 | + } |
| 164 | + } else { |
| 165 | + result = typeChecker.typeToString(type); |
| 166 | + } |
| 167 | + |
| 168 | + if (isRoot && !type.isUnion()) { |
| 169 | + return `\`\`\`typescript\n${result}\n\`\`\``; |
| 170 | + } |
| 171 | + return result; |
| 172 | +} |
| 173 | + |
| 174 | +function getJSDocTags(symbol) { |
| 175 | + const tags = {}; |
| 176 | + const jsDocTags = symbol.getJsDocTags(); |
| 177 | + for (const tag of jsDocTags) { |
| 178 | + tags[tag.name] = tag.text ? tag.text.map(t => t.text).join(' ') : true; |
| 179 | + } |
| 180 | + return tags; |
| 181 | +} |
0 commit comments