1import { 2 AbsolutePositionAndLineText, ConfiguredProject, Errors, ExternalProject, InferredProject, isConfiguredProject, 3 isExternalProject, isInferredProject, maxFileSize, NormalizedPath, Project, ProjectKind, protocol, 4 ScriptVersionCache, ServerHost, 5} from "./_namespaces/ts.server"; 6import { 7 assign, clear, closeFileWatcherOf, computeLineAndCharacterOfPosition, computeLineStarts, 8 computePositionOfLineAndCharacter, contains, createTextSpanFromBounds, Debug, directorySeparator, 9 DocumentPositionMapper, DocumentRegistryBucketKeyWithMode, emptyOptions, FileWatcher, FileWatcherEventKind, forEach, 10 FormatCodeSettings, getBaseFileName, getDefaultFormatCodeSettings, getLineInfo, getScriptKindFromFileName, 11 getSnapshotText, hasTSFileExtension, IScriptSnapshot, isString, LineInfo, Path, ScriptKind, ScriptSnapshot, Set, 12 some, SourceFile, SourceFileLike, stringContains, TextSpan, unorderedRemoveItem, 13} from "./_namespaces/ts"; 14 15export interface ScriptInfoVersion { 16 svc: number; 17 text: number; 18} 19 20/** @internal */ 21export class TextStorage { 22 version: ScriptInfoVersion; 23 24 /** 25 * Generated only on demand (based on edits, or information requested) 26 * The property text is set to undefined when edits happen on the cache 27 */ 28 private svc: ScriptVersionCache | undefined; 29 30 /** 31 * Stores the text when there are no changes to the script version cache 32 * The script version cache is generated on demand and text is still retained. 33 * Only on edits to the script version cache, the text will be set to undefined 34 */ 35 private text: string | undefined; 36 /** 37 * Line map for the text when there is no script version cache present 38 */ 39 private lineMap: number[] | undefined; 40 41 /** 42 * When a large file is loaded, text will artificially be set to "". 43 * In order to be able to report correct telemetry, we store the actual 44 * file size in this case. (In other cases where text === "", e.g. 45 * for mixed content or dynamic files, fileSize will be undefined.) 46 */ 47 private fileSize: number | undefined; 48 49 /** 50 * True if the text is for the file thats open in the editor 51 */ 52 public isOpen = false; 53 /** 54 * True if the text present is the text from the file on the disk 55 */ 56 private ownFileText = false; 57 /** 58 * True when reloading contents of file from the disk is pending 59 */ 60 private pendingReloadFromDisk = false; 61 62 constructor(private readonly host: ServerHost, private readonly info: ScriptInfo, initialVersion?: ScriptInfoVersion) { 63 this.version = initialVersion || { svc: 0, text: 0 }; 64 } 65 66 public getVersion() { 67 return this.svc 68 ? `SVC-${this.version.svc}-${this.svc.getSnapshotVersion()}` 69 : `Text-${this.version.text}`; 70 } 71 72 public hasScriptVersionCache_TestOnly() { 73 return this.svc !== undefined; 74 } 75 76 public useScriptVersionCache_TestOnly() { 77 this.switchToScriptVersionCache(); 78 } 79 80 private resetSourceMapInfo() { 81 this.info.sourceFileLike = undefined; 82 this.info.closeSourceMapFileWatcher(); 83 this.info.sourceMapFilePath = undefined; 84 this.info.declarationInfoPath = undefined; 85 this.info.sourceInfos = undefined; 86 this.info.documentPositionMapper = undefined; 87 } 88 89 /** Public for testing */ 90 public useText(newText?: string) { 91 this.svc = undefined; 92 this.text = newText; 93 this.lineMap = undefined; 94 this.fileSize = undefined; 95 this.resetSourceMapInfo(); 96 this.version.text++; 97 } 98 99 public edit(start: number, end: number, newText: string) { 100 this.switchToScriptVersionCache().edit(start, end - start, newText); 101 this.ownFileText = false; 102 this.text = undefined; 103 this.lineMap = undefined; 104 this.fileSize = undefined; 105 this.resetSourceMapInfo(); 106 } 107 108 /** 109 * Set the contents as newText 110 * returns true if text changed 111 */ 112 public reload(newText: string): boolean { 113 Debug.assert(newText !== undefined); 114 115 // Reload always has fresh content 116 this.pendingReloadFromDisk = false; 117 118 // If text changed set the text 119 // This also ensures that if we had switched to version cache, 120 // we are switching back to text. 121 // The change to version cache will happen when needed 122 // Thus avoiding the computation if there are no changes 123 if (this.text !== newText) { 124 this.useText(newText); 125 // We cant guarantee new text is own file text 126 this.ownFileText = false; 127 return true; 128 } 129 130 return false; 131 } 132 133 /** 134 * Reads the contents from tempFile(if supplied) or own file and sets it as contents 135 * returns true if text changed 136 */ 137 public reloadWithFileText(tempFileName?: string) { 138 const { text: newText, fileSize } = this.getFileTextAndSize(tempFileName); 139 const reloaded = this.reload(newText); 140 this.fileSize = fileSize; // NB: after reload since reload clears it 141 this.ownFileText = !tempFileName || tempFileName === this.info.fileName; 142 return reloaded; 143 } 144 145 /** 146 * Reloads the contents from the file if there is no pending reload from disk or the contents of file are same as file text 147 * returns true if text changed 148 */ 149 public reloadFromDisk() { 150 if (!this.pendingReloadFromDisk && !this.ownFileText) { 151 return this.reloadWithFileText(); 152 } 153 return false; 154 } 155 156 public delayReloadFromFileIntoText() { 157 this.pendingReloadFromDisk = true; 158 } 159 160 /** 161 * For telemetry purposes, we would like to be able to report the size of the file. 162 * However, we do not want telemetry to require extra file I/O so we report a size 163 * that may be stale (e.g. may not reflect change made on disk since the last reload). 164 * NB: Will read from disk if the file contents have never been loaded because 165 * telemetry falsely indicating size 0 would be counter-productive. 166 */ 167 public getTelemetryFileSize(): number { 168 return !!this.fileSize 169 ? this.fileSize 170 : !!this.text // Check text before svc because its length is cheaper 171 ? this.text.length // Could be wrong if this.pendingReloadFromDisk 172 : !!this.svc 173 ? this.svc.getSnapshot().getLength() // Could be wrong if this.pendingReloadFromDisk 174 : this.getSnapshot().getLength(); // Should be strictly correct 175 } 176 177 public getSnapshot(): IScriptSnapshot { 178 return this.useScriptVersionCacheIfValidOrOpen() 179 ? this.svc!.getSnapshot() 180 : ScriptSnapshot.fromString(this.getOrLoadText()); 181 } 182 183 public getAbsolutePositionAndLineText(line: number): AbsolutePositionAndLineText { 184 return this.switchToScriptVersionCache().getAbsolutePositionAndLineText(line); 185 } 186 /** 187 * @param line 0 based index 188 */ 189 lineToTextSpan(line: number): TextSpan { 190 if (!this.useScriptVersionCacheIfValidOrOpen()) { 191 const lineMap = this.getLineMap(); 192 const start = lineMap[line]; // -1 since line is 1-based 193 const end = line + 1 < lineMap.length ? lineMap[line + 1] : this.text!.length; 194 return createTextSpanFromBounds(start, end); 195 } 196 return this.svc!.lineToTextSpan(line); 197 } 198 199 /** 200 * @param line 1 based index 201 * @param offset 1 based index 202 */ 203 lineOffsetToPosition(line: number, offset: number, allowEdits?: true): number { 204 if (!this.useScriptVersionCacheIfValidOrOpen()) { 205 return computePositionOfLineAndCharacter(this.getLineMap(), line - 1, offset - 1, this.text, allowEdits); 206 } 207 208 // TODO: assert this offset is actually on the line 209 return this.svc!.lineOffsetToPosition(line, offset); 210 } 211 212 positionToLineOffset(position: number): protocol.Location { 213 if (!this.useScriptVersionCacheIfValidOrOpen()) { 214 const { line, character } = computeLineAndCharacterOfPosition(this.getLineMap(), position); 215 return { line: line + 1, offset: character + 1 }; 216 } 217 return this.svc!.positionToLineOffset(position); 218 } 219 220 private getFileTextAndSize(tempFileName?: string): { text: string, fileSize?: number } { 221 let text: string; 222 const fileName = tempFileName || this.info.fileName; 223 const getText = () => text === undefined ? (text = this.host.readFile(fileName) || "") : text; 224 // Only non typescript files have size limitation 225 if (!hasTSFileExtension(this.info.fileName)) { 226 const fileSize = this.host.getFileSize ? this.host.getFileSize(fileName) : getText().length; 227 if (fileSize > maxFileSize) { 228 Debug.assert(!!this.info.containingProjects.length); 229 const service = this.info.containingProjects[0].projectService; 230 service.logger.info(`Skipped loading contents of large file ${fileName} for info ${this.info.fileName}: fileSize: ${fileSize}`); 231 this.info.containingProjects[0].projectService.sendLargeFileReferencedEvent(fileName, fileSize); 232 return { text: "", fileSize }; 233 } 234 } 235 return { text: getText() }; 236 } 237 238 private switchToScriptVersionCache(): ScriptVersionCache { 239 if (!this.svc || this.pendingReloadFromDisk) { 240 this.svc = ScriptVersionCache.fromString(this.getOrLoadText()); 241 this.version.svc++; 242 } 243 return this.svc; 244 } 245 246 private useScriptVersionCacheIfValidOrOpen(): ScriptVersionCache | undefined { 247 // If this is open script, use the cache 248 if (this.isOpen) { 249 return this.switchToScriptVersionCache(); 250 } 251 252 // If there is pending reload from the disk then, reload the text 253 if (this.pendingReloadFromDisk) { 254 this.reloadWithFileText(); 255 } 256 257 // At this point if svc is present it's valid 258 return this.svc; 259 } 260 261 private getOrLoadText() { 262 if (this.text === undefined || this.pendingReloadFromDisk) { 263 Debug.assert(!this.svc || this.pendingReloadFromDisk, "ScriptVersionCache should not be set when reloading from disk"); 264 this.reloadWithFileText(); 265 } 266 return this.text!; 267 } 268 269 private getLineMap() { 270 Debug.assert(!this.svc, "ScriptVersionCache should not be set"); 271 return this.lineMap || (this.lineMap = computeLineStarts(this.getOrLoadText())); 272 } 273 274 getLineInfo(): LineInfo { 275 if (this.svc) { 276 return { 277 getLineCount: () => this.svc!.getLineCount(), 278 getLineText: line => this.svc!.getAbsolutePositionAndLineText(line + 1).lineText! 279 }; 280 } 281 const lineMap = this.getLineMap(); 282 return getLineInfo(this.text!, lineMap); 283 } 284} 285 286export function isDynamicFileName(fileName: NormalizedPath) { 287 return fileName[0] === "^" || 288 ((stringContains(fileName, "walkThroughSnippet:/") || stringContains(fileName, "untitled:/")) && 289 getBaseFileName(fileName)[0] === "^") || 290 (stringContains(fileName, ":^") && !stringContains(fileName, directorySeparator)); 291} 292 293/** @internal */ 294export interface DocumentRegistrySourceFileCache { 295 key: DocumentRegistryBucketKeyWithMode; 296 sourceFile: SourceFile; 297} 298 299/** @internal */ 300export interface SourceMapFileWatcher { 301 watcher: FileWatcher; 302 sourceInfos?: Set<Path>; 303} 304 305export class ScriptInfo { 306 /** 307 * All projects that include this file 308 */ 309 readonly containingProjects: Project[] = []; 310 private formatSettings: FormatCodeSettings | undefined; 311 private preferences: protocol.UserPreferences | undefined; 312 313 /** @internal */ 314 fileWatcher: FileWatcher | undefined; 315 private textStorage: TextStorage; 316 317 /** @internal */ 318 readonly isDynamic: boolean; 319 320 /** 321 * Set to real path if path is different from info.path 322 * 323 * @internal 324 */ 325 private realpath: Path | undefined; 326 327 /** @internal */ 328 cacheSourceFile: DocumentRegistrySourceFileCache | undefined; 329 330 /** @internal */ 331 mTime?: number; 332 333 /** @internal */ 334 sourceFileLike?: SourceFileLike; 335 336 /** @internal */ 337 sourceMapFilePath?: Path | SourceMapFileWatcher | false; 338 339 // Present on sourceMapFile info 340 /** @internal */ 341 declarationInfoPath?: Path; 342 /** @internal */ 343 sourceInfos?: Set<Path>; 344 /** @internal */ 345 documentPositionMapper?: DocumentPositionMapper | false; 346 347 constructor( 348 private readonly host: ServerHost, 349 readonly fileName: NormalizedPath, 350 readonly scriptKind: ScriptKind, 351 public readonly hasMixedContent: boolean, 352 readonly path: Path, 353 initialVersion?: ScriptInfoVersion) { 354 this.isDynamic = isDynamicFileName(fileName); 355 356 this.textStorage = new TextStorage(host, this, initialVersion); 357 if (hasMixedContent || this.isDynamic) { 358 this.textStorage.reload(""); 359 this.realpath = this.path; 360 } 361 this.scriptKind = scriptKind 362 ? scriptKind 363 : getScriptKindFromFileName(fileName); 364 } 365 366 /** @internal */ 367 getVersion() { 368 return this.textStorage.version; 369 } 370 371 /** @internal */ 372 getTelemetryFileSize() { 373 return this.textStorage.getTelemetryFileSize(); 374 } 375 376 /** @internal */ 377 public isDynamicOrHasMixedContent() { 378 return this.hasMixedContent || this.isDynamic; 379 } 380 381 public isScriptOpen() { 382 return this.textStorage.isOpen; 383 } 384 385 public open(newText: string) { 386 this.textStorage.isOpen = true; 387 if (newText !== undefined && 388 this.textStorage.reload(newText)) { 389 // reload new contents only if the existing contents changed 390 this.markContainingProjectsAsDirty(); 391 } 392 } 393 394 public close(fileExists = true) { 395 this.textStorage.isOpen = false; 396 if (this.isDynamicOrHasMixedContent() || !fileExists) { 397 if (this.textStorage.reload("")) { 398 this.markContainingProjectsAsDirty(); 399 } 400 } 401 else if (this.textStorage.reloadFromDisk()) { 402 this.markContainingProjectsAsDirty(); 403 } 404 } 405 406 public getSnapshot() { 407 return this.textStorage.getSnapshot(); 408 } 409 410 private ensureRealPath() { 411 if (this.realpath === undefined) { 412 // Default is just the path 413 this.realpath = this.path; 414 if (this.host.realpath) { 415 Debug.assert(!!this.containingProjects.length); 416 const project = this.containingProjects[0]; 417 const realpath = this.host.realpath(this.path); 418 if (realpath) { 419 this.realpath = project.toPath(realpath); 420 // If it is different from this.path, add to the map 421 if (this.realpath !== this.path) { 422 project.projectService.realpathToScriptInfos!.add(this.realpath, this); // TODO: GH#18217 423 } 424 } 425 } 426 } 427 } 428 429 /** @internal */ 430 getRealpathIfDifferent(): Path | undefined { 431 return this.realpath && this.realpath !== this.path ? this.realpath : undefined; 432 } 433 434 /** 435 * @internal 436 * Does not compute realpath; uses precomputed result. Use `ensureRealPath` 437 * first if a definite result is needed. 438 */ 439 isSymlink(): boolean | undefined { 440 return this.realpath && this.realpath !== this.path; 441 } 442 443 getFormatCodeSettings(): FormatCodeSettings | undefined { return this.formatSettings; } 444 getPreferences(): protocol.UserPreferences | undefined { return this.preferences; } 445 446 attachToProject(project: Project): boolean { 447 const isNew = !this.isAttached(project); 448 if (isNew) { 449 this.containingProjects.push(project); 450 if (!project.getCompilerOptions().preserveSymlinks) { 451 this.ensureRealPath(); 452 } 453 project.onFileAddedOrRemoved(this.isSymlink()); 454 } 455 return isNew; 456 } 457 458 isAttached(project: Project) { 459 // unrolled for common cases 460 switch (this.containingProjects.length) { 461 case 0: return false; 462 case 1: return this.containingProjects[0] === project; 463 case 2: return this.containingProjects[0] === project || this.containingProjects[1] === project; 464 default: return contains(this.containingProjects, project); 465 } 466 } 467 468 detachFromProject(project: Project) { 469 // unrolled for common cases 470 switch (this.containingProjects.length) { 471 case 0: 472 return; 473 case 1: 474 if (this.containingProjects[0] === project) { 475 project.onFileAddedOrRemoved(this.isSymlink()); 476 this.containingProjects.pop(); 477 } 478 break; 479 case 2: 480 if (this.containingProjects[0] === project) { 481 project.onFileAddedOrRemoved(this.isSymlink()); 482 this.containingProjects[0] = this.containingProjects.pop()!; 483 } 484 else if (this.containingProjects[1] === project) { 485 project.onFileAddedOrRemoved(this.isSymlink()); 486 this.containingProjects.pop(); 487 } 488 break; 489 default: 490 if (unorderedRemoveItem(this.containingProjects, project)) { 491 project.onFileAddedOrRemoved(this.isSymlink()); 492 } 493 break; 494 } 495 } 496 497 detachAllProjects() { 498 for (const p of this.containingProjects) { 499 if (isConfiguredProject(p)) { 500 p.getCachedDirectoryStructureHost().addOrDeleteFile(this.fileName, this.path, FileWatcherEventKind.Deleted); 501 } 502 const existingRoot = p.getRootFilesMap().get(this.path); 503 // detach is unnecessary since we'll clean the list of containing projects anyways 504 p.removeFile(this, /*fileExists*/ false, /*detachFromProjects*/ false); 505 p.onFileAddedOrRemoved(this.isSymlink()); 506 // If the info was for the external or configured project's root, 507 // add missing file as the root 508 if (existingRoot && !isInferredProject(p)) { 509 p.addMissingFileRoot(existingRoot.fileName); 510 } 511 } 512 clear(this.containingProjects); 513 } 514 515 getDefaultProject() { 516 switch (this.containingProjects.length) { 517 case 0: 518 return Errors.ThrowNoProject(); 519 case 1: 520 return ensurePrimaryProjectKind(this.containingProjects[0]); 521 default: 522 // If this file belongs to multiple projects, below is the order in which default project is used 523 // - for open script info, its default configured project during opening is default if info is part of it 524 // - first configured project of which script info is not a source of project reference redirect 525 // - first configured project 526 // - first external project 527 // - first inferred project 528 let firstExternalProject: ExternalProject | undefined; 529 let firstConfiguredProject: ConfiguredProject | undefined; 530 let firstInferredProject: InferredProject | undefined; 531 let firstNonSourceOfProjectReferenceRedirect: ConfiguredProject | undefined; 532 let defaultConfiguredProject: ConfiguredProject | false | undefined; 533 for (let index = 0; index < this.containingProjects.length; index++) { 534 const project = this.containingProjects[index]; 535 if (isConfiguredProject(project)) { 536 if (!project.isSourceOfProjectReferenceRedirect(this.fileName)) { 537 // If we havent found default configuredProject and 538 // its not the last one, find it and use that one if there 539 if (defaultConfiguredProject === undefined && 540 index !== this.containingProjects.length - 1) { 541 defaultConfiguredProject = project.projectService.findDefaultConfiguredProject(this) || false; 542 } 543 if (defaultConfiguredProject === project) return project; 544 if (!firstNonSourceOfProjectReferenceRedirect) firstNonSourceOfProjectReferenceRedirect = project; 545 } 546 if (!firstConfiguredProject) firstConfiguredProject = project; 547 } 548 else if (!firstExternalProject && isExternalProject(project)) { 549 firstExternalProject = project; 550 } 551 else if (!firstInferredProject && isInferredProject(project)) { 552 firstInferredProject = project; 553 } 554 } 555 return ensurePrimaryProjectKind(defaultConfiguredProject || 556 firstNonSourceOfProjectReferenceRedirect || 557 firstConfiguredProject || 558 firstExternalProject || 559 firstInferredProject); 560 } 561 } 562 563 registerFileUpdate(): void { 564 for (const p of this.containingProjects) { 565 p.registerFileUpdate(this.path); 566 } 567 } 568 569 setOptions(formatSettings: FormatCodeSettings, preferences: protocol.UserPreferences | undefined): void { 570 if (formatSettings) { 571 if (!this.formatSettings) { 572 this.formatSettings = getDefaultFormatCodeSettings(this.host.newLine); 573 assign(this.formatSettings, formatSettings); 574 } 575 else { 576 this.formatSettings = { ...this.formatSettings, ...formatSettings }; 577 } 578 } 579 580 if (preferences) { 581 if (!this.preferences) { 582 this.preferences = emptyOptions; 583 } 584 this.preferences = { ...this.preferences, ...preferences }; 585 } 586 } 587 588 getLatestVersion(): string { 589 // Ensure we have updated snapshot to give back latest version 590 this.textStorage.getSnapshot(); 591 return this.textStorage.getVersion(); 592 } 593 594 saveTo(fileName: string) { 595 this.host.writeFile(fileName, getSnapshotText(this.textStorage.getSnapshot())); 596 } 597 598 /** @internal */ 599 delayReloadNonMixedContentFile() { 600 Debug.assert(!this.isDynamicOrHasMixedContent()); 601 this.textStorage.delayReloadFromFileIntoText(); 602 this.markContainingProjectsAsDirty(); 603 } 604 605 reloadFromFile(tempFileName?: NormalizedPath) { 606 if (this.isDynamicOrHasMixedContent()) { 607 this.textStorage.reload(""); 608 this.markContainingProjectsAsDirty(); 609 return true; 610 } 611 else { 612 if (this.textStorage.reloadWithFileText(tempFileName)) { 613 this.markContainingProjectsAsDirty(); 614 return true; 615 } 616 } 617 return false; 618 } 619 620 /** @internal */ 621 getAbsolutePositionAndLineText(line: number): AbsolutePositionAndLineText { 622 return this.textStorage.getAbsolutePositionAndLineText(line); 623 } 624 625 editContent(start: number, end: number, newText: string): void { 626 this.textStorage.edit(start, end, newText); 627 this.markContainingProjectsAsDirty(); 628 } 629 630 markContainingProjectsAsDirty() { 631 for (const p of this.containingProjects) { 632 p.markFileAsDirty(this.path); 633 } 634 } 635 636 isOrphan() { 637 return !forEach(this.containingProjects, p => !p.isOrphan()); 638 } 639 640 /** @internal */ 641 isContainedByBackgroundProject() { 642 return some( 643 this.containingProjects, 644 p => p.projectKind === ProjectKind.AutoImportProvider || p.projectKind === ProjectKind.Auxiliary); 645 } 646 647 /** 648 * @param line 1 based index 649 */ 650 lineToTextSpan(line: number) { 651 return this.textStorage.lineToTextSpan(line); 652 } 653 654 /** 655 * @param line 1 based index 656 * @param offset 1 based index 657 */ 658 lineOffsetToPosition(line: number, offset: number): number; 659 /** @internal */ 660 lineOffsetToPosition(line: number, offset: number, allowEdits?: true): number; // eslint-disable-line @typescript-eslint/unified-signatures 661 lineOffsetToPosition(line: number, offset: number, allowEdits?: true): number { 662 return this.textStorage.lineOffsetToPosition(line, offset, allowEdits); 663 } 664 665 positionToLineOffset(position: number): protocol.Location { 666 failIfInvalidPosition(position); 667 const location = this.textStorage.positionToLineOffset(position); 668 failIfInvalidLocation(location); 669 return location; 670 } 671 672 public isJavaScript() { 673 return this.scriptKind === ScriptKind.JS || this.scriptKind === ScriptKind.JSX; 674 } 675 676 /** @internal */ 677 getLineInfo(): LineInfo { 678 return this.textStorage.getLineInfo(); 679 } 680 681 /** @internal */ 682 closeSourceMapFileWatcher() { 683 if (this.sourceMapFilePath && !isString(this.sourceMapFilePath)) { 684 closeFileWatcherOf(this.sourceMapFilePath); 685 this.sourceMapFilePath = undefined; 686 } 687 } 688} 689 690/** 691 * Throws an error if `project` is an AutoImportProvider or AuxiliaryProject, 692 * which are used in the background by other Projects and should never be 693 * reported as the default project for a ScriptInfo. 694 */ 695function ensurePrimaryProjectKind(project: Project | undefined) { 696 if (!project || project.projectKind === ProjectKind.AutoImportProvider || project.projectKind === ProjectKind.Auxiliary) { 697 return Errors.ThrowNoProject(); 698 } 699 return project; 700} 701 702function failIfInvalidPosition(position: number) { 703 Debug.assert(typeof position === "number", `Expected position ${position} to be a number.`); 704 Debug.assert(position >= 0, `Expected position to be non-negative.`); 705} 706 707function failIfInvalidLocation(location: protocol.Location) { 708 Debug.assert(typeof location.line === "number", `Expected line ${location.line} to be a number.`); 709 Debug.assert(typeof location.offset === "number", `Expected offset ${location.offset} to be a number.`); 710 711 Debug.assert(location.line > 0, `Expected line to be non-${location.line === 0 ? "zero" : "negative"}`); 712 Debug.assert(location.offset > 0, `Expected offset to be non-${location.offset === 0 ? "zero" : "negative"}`); 713} 714