1import * as ts from "./_namespaces/ts"; 2import * as Harness from "./_namespaces/Harness"; 3 4export function encodeString(s: string): string { 5 return ts.sys.bufferFrom!(s).toString("utf8"); 6} 7 8export function byteLength(s: string, encoding?: string): number { 9 // stub implementation if Buffer is not available (in-browser case) 10 return Buffer.byteLength(s, encoding as ts.BufferEncoding | undefined); 11} 12 13export function evalFile(fileContents: string, fileName: string, nodeContext?: any) { 14 const vm = require("vm"); 15 if (nodeContext) { 16 vm.runInNewContext(fileContents, nodeContext, fileName); 17 } 18 else { 19 vm.runInThisContext(fileContents, fileName); 20 } 21} 22 23/** Splits the given string on \r\n, or on only \n if that fails, or on only \r if *that* fails. */ 24export function splitContentByNewlines(content: string) { 25 // Split up the input file by line 26 // Note: IE JS engine incorrectly handles consecutive delimiters here when using RegExp split, so 27 // we have to use string-based splitting instead and try to figure out the delimiting chars 28 let lines = content.split("\r\n"); 29 if (lines.length === 1) { 30 lines = content.split("\n"); 31 32 if (lines.length === 1) { 33 lines = content.split("\r"); 34 } 35 } 36 return lines; 37} 38 39/** Reads a file under /tests */ 40export function readTestFile(path: string) { 41 if (path.indexOf("tests") < 0) { 42 path = "tests/" + path; 43 } 44 45 let content: string | undefined; 46 try { 47 content = Harness.IO.readFile(Harness.userSpecifiedRoot + path); 48 } 49 catch (err) { 50 return undefined; 51 } 52 53 return content; 54} 55 56export function memoize<T extends ts.AnyFunction>(f: T, memoKey: (...anything: any[]) => string): T { 57 const cache = new ts.Map<string, any>(); 58 59 return (function (this: any, ...args: any[]) { 60 const key = memoKey(...args); 61 if (cache.has(key)) { 62 return cache.get(key); 63 } 64 else { 65 const value = f.apply(this, args); 66 cache.set(key, value); 67 return value; 68 } 69 } as any); 70} 71 72export const canonicalizeForHarness = ts.createGetCanonicalFileName(/*caseSensitive*/ false); // This is done so tests work on windows _and_ linux 73 74export function assertInvariants(node: ts.Node | undefined, parent: ts.Node | undefined) { 75 const queue: [ts.Node | undefined, ts.Node | undefined][] = [[node, parent]]; 76 for (const [node, parent] of queue) { 77 assertInvariantsWorker(node, parent); 78 } 79 80 function assertInvariantsWorker(node: ts.Node | undefined, parent: ts.Node | undefined): void { 81 if (node) { 82 assert.isFalse(node.pos < 0, "node.pos < 0"); 83 assert.isFalse(node.end < 0, "node.end < 0"); 84 assert.isFalse(node.end < node.pos, "node.end < node.pos"); 85 assert.equal(node.parent, parent, "node.parent !== parent"); 86 87 if (parent) { 88 // Make sure each child is contained within the parent. 89 assert.isFalse(node.pos < parent.pos, "node.pos < parent.pos"); 90 assert.isFalse(node.end > parent.end, "node.end > parent.end"); 91 } 92 93 ts.forEachChild(node, child => { 94 queue.push([child, node]); 95 }); 96 97 // Make sure each of the children is in order. 98 let currentPos = 0; 99 ts.forEachChild(node, 100 child => { 101 assert.isFalse(child.pos < currentPos, "child.pos < currentPos"); 102 currentPos = child.end; 103 }, 104 array => { 105 assert.isFalse(array.pos < node.pos, "array.pos < node.pos"); 106 assert.isFalse(array.end > node.end, "array.end > node.end"); 107 assert.isFalse(array.pos < currentPos, "array.pos < currentPos"); 108 109 for (const item of array) { 110 assert.isFalse(item.pos < currentPos, "array[i].pos < currentPos"); 111 currentPos = item.end; 112 } 113 114 currentPos = array.end; 115 }); 116 117 const childNodesAndArrays: any[] = []; 118 ts.forEachChild(node, child => { 119 childNodesAndArrays.push(child); 120 }, array => { 121 childNodesAndArrays.push(array); 122 }); 123 124 for (const childName in node) { 125 if (childName === "parent" || 126 childName === "nextContainer" || 127 childName === "modifiers" || 128 childName === "externalModuleIndicator" || 129 // for now ignore jsdoc comments 130 childName === "jsDocComment" || 131 childName === "checkJsDirective" || 132 childName === "commonJsModuleIndicator" || 133 // ignore nodes added only to report grammar errors 134 childName === "illegalInitializer" || 135 childName === "illegalDecorators" || 136 childName === "illegalModifiers" || 137 childName === "illegalQuestionToken" || 138 childName === "illegalExclamationToken" || 139 childName === "illegalTypeParameters" || 140 childName === "illegalType") { 141 continue; 142 } 143 const child = (node as any)[childName]; 144 if (isNodeOrArray(child)) { 145 assert.isFalse(childNodesAndArrays.indexOf(child) < 0, 146 "Missing child when forEach'ing over node: " + ts.Debug.formatSyntaxKind(node.kind) + "-" + childName); 147 } 148 } 149 } 150 } 151} 152 153function isNodeOrArray(a: any): boolean { 154 return a !== undefined && typeof a.pos === "number"; 155} 156 157export function convertDiagnostics(diagnostics: readonly ts.Diagnostic[]) { 158 return diagnostics.map(convertDiagnostic); 159} 160 161function convertDiagnostic(diagnostic: ts.Diagnostic) { 162 return { 163 start: diagnostic.start, 164 length: diagnostic.length, 165 messageText: ts.flattenDiagnosticMessageText(diagnostic.messageText, Harness.IO.newLine()), 166 category: ts.diagnosticCategoryName(diagnostic, /*lowerCase*/ false), 167 code: diagnostic.code 168 }; 169} 170 171export function sourceFileToJSON(file: ts.Node): string { 172 return JSON.stringify(file, (_, v) => isNodeOrArray(v) ? serializeNode(v) : v, " "); 173 174 function getKindName(k: number | string | undefined): string | undefined { 175 if (k === undefined || ts.isString(k)) { 176 return k; 177 } 178 return ts.Debug.formatSyntaxKind(k); 179 } 180 181 function getNodeFlagName(f: number) { 182 return ts.Debug.formatNodeFlags(f); 183 } 184 185 function serializeNode(n: ts.Node): any { 186 const o: any = { kind: getKindName(n.kind) }; 187 if (ts.containsParseError(n)) { 188 o.containsParseError = true; 189 } 190 191 for (const propertyName of Object.getOwnPropertyNames(n) as readonly (keyof ts.SourceFile | keyof ts.Identifier)[]) { 192 switch (propertyName) { 193 case "parent": 194 case "symbol": 195 case "locals": 196 case "localSymbol": 197 case "kind": 198 case "id": 199 case "nodeCount": 200 case "symbolCount": 201 case "identifierCount": 202 case "scriptSnapshot": 203 // Blocklist of items we never put in the baseline file. 204 break; 205 206 case "originalKeywordKind": 207 o[propertyName] = getKindName((n as any)[propertyName]); 208 break; 209 210 case "flags": 211 // Clear the flags that are produced by aggregating child values. That is ephemeral 212 // data we don't care about in the dump. We only care what the parser set directly 213 // on the AST. 214 const flags = n.flags & ~(ts.NodeFlags.JavaScriptFile | ts.NodeFlags.HasAggregatedChildData); 215 if (flags) { 216 o[propertyName] = getNodeFlagName(flags); 217 } 218 break; 219 220 case "parseDiagnostics": 221 o[propertyName] = convertDiagnostics((n as any)[propertyName]); 222 break; 223 224 case "nextContainer": 225 if (n.nextContainer) { 226 o[propertyName] = { kind: n.nextContainer.kind, pos: n.nextContainer.pos, end: n.nextContainer.end }; 227 } 228 break; 229 230 case "text": 231 // Include 'text' field for identifiers/literals, but not for source files. 232 if (n.kind !== ts.SyntaxKind.SourceFile) { 233 o[propertyName] = (n as any)[propertyName]; 234 } 235 break; 236 237 default: 238 o[propertyName] = (n as any)[propertyName]; 239 } 240 } 241 242 return o; 243 } 244} 245 246export function assertDiagnosticsEquals(array1: readonly ts.Diagnostic[], array2: readonly ts.Diagnostic[]) { 247 if (array1 === array2) { 248 return; 249 } 250 251 assert(array1, "array1"); 252 assert(array2, "array2"); 253 254 assert.equal(array1.length, array2.length, "array1.length !== array2.length"); 255 256 for (let i = 0; i < array1.length; i++) { 257 const d1 = array1[i]; 258 const d2 = array2[i]; 259 260 assert.equal(d1.start, d2.start, "d1.start !== d2.start"); 261 assert.equal(d1.length, d2.length, "d1.length !== d2.length"); 262 assert.equal( 263 ts.flattenDiagnosticMessageText(d1.messageText, Harness.IO.newLine()), 264 ts.flattenDiagnosticMessageText(d2.messageText, Harness.IO.newLine()), "d1.messageText !== d2.messageText"); 265 assert.equal(d1.category, d2.category, "d1.category !== d2.category"); 266 assert.equal(d1.code, d2.code, "d1.code !== d2.code"); 267 } 268} 269 270export function assertStructuralEquals(node1: ts.Node, node2: ts.Node) { 271 if (node1 === node2) { 272 return; 273 } 274 275 assert(node1, "node1"); 276 assert(node2, "node2"); 277 assert.equal(node1.pos, node2.pos, "node1.pos !== node2.pos"); 278 assert.equal(node1.end, node2.end, "node1.end !== node2.end"); 279 assert.equal(node1.kind, node2.kind, "node1.kind !== node2.kind"); 280 281 // call this on both nodes to ensure all propagated flags have been set (and thus can be 282 // compared). 283 assert.equal(ts.containsParseError(node1), ts.containsParseError(node2)); 284 assert.equal(node1.flags & ~ts.NodeFlags.ReachabilityAndEmitFlags, node2.flags & ~ts.NodeFlags.ReachabilityAndEmitFlags, "node1.flags !== node2.flags"); 285 286 ts.forEachChild(node1, 287 child1 => { 288 const childName = findChildName(node1, child1); 289 const child2: ts.Node = (node2 as any)[childName]; 290 291 assertStructuralEquals(child1, child2); 292 }, 293 array1 => { 294 const childName = findChildName(node1, array1); 295 const array2: ts.NodeArray<ts.Node> = (node2 as any)[childName]; 296 297 assertArrayStructuralEquals(array1, array2); 298 }); 299} 300 301function assertArrayStructuralEquals(array1: ts.NodeArray<ts.Node>, array2: ts.NodeArray<ts.Node>) { 302 if (array1 === array2) { 303 return; 304 } 305 306 assert(array1, "array1"); 307 assert(array2, "array2"); 308 assert.equal(array1.pos, array2.pos, "array1.pos !== array2.pos"); 309 assert.equal(array1.end, array2.end, "array1.end !== array2.end"); 310 assert.equal(array1.length, array2.length, "array1.length !== array2.length"); 311 312 for (let i = 0; i < array1.length; i++) { 313 assertStructuralEquals(array1[i], array2[i]); 314 } 315} 316 317function findChildName(parent: any, child: any) { 318 for (const name in parent) { 319 if (ts.hasProperty(parent, name) && parent[name] === child) { 320 return name; 321 } 322 } 323 324 throw new Error("Could not find child in parent"); 325} 326 327const maxHarnessFrames = 1; 328 329export function filterStack(error: Error, stackTraceLimit = Infinity) { 330 const stack = (error as any).stack as string; 331 if (stack) { 332 const lines = stack.split(/\r\n?|\n/g); 333 const filtered: string[] = []; 334 let frameCount = 0; 335 let harnessFrameCount = 0; 336 for (let line of lines) { 337 if (isStackFrame(line)) { 338 if (frameCount >= stackTraceLimit 339 || isMocha(line) 340 || isNode(line)) { 341 continue; 342 } 343 344 if (isHarness(line)) { 345 if (harnessFrameCount >= maxHarnessFrames) { 346 continue; 347 } 348 349 harnessFrameCount++; 350 } 351 352 line = line.replace(/\bfile:\/\/\/(.*?)(?=(:\d+)*($|\)))/, (_, path) => ts.sys.resolvePath(path)); 353 frameCount++; 354 } 355 356 filtered.push(line); 357 } 358 359 (error as any).stack = filtered.join(Harness.IO.newLine()); 360 } 361 362 return error; 363} 364 365function isStackFrame(line: string) { 366 return /^\s+at\s/.test(line); 367} 368 369function isMocha(line: string) { 370 return /[\\/](node_modules|components)[\\/]mocha(js)?[\\/]|[\\/]mocha\.js/.test(line); 371} 372 373function isNode(line: string) { 374 return /\((timers|events|node|module)\.js:/.test(line); 375} 376 377function isHarness(line: string) { 378 return /[\\/]src[\\/]harness[\\/]|[\\/]run\.js/.test(line); 379} 380