Repository: danvk/literate-ts (ts)

Issue:
Switch to filename:line:col output format
Here's an example current error message:

```
ch02-the_type_system-40: Unexpected TypeScript error: 0:0-5: BigInt literals are not available when targeting lower than ES2020.
```

In VS Code, you can Ctrl+Click on file specifiers that include line and column. It's very useful. 

Proposal: switch the id printed for a file & line to reference the offending snippet:

```
./ch02-the_type_system.asciidoc:40:0: Unexpected TypeScript error: 0:0-5: BigInt literals are not available when targeting lower than ES2020.
```


Pull request description:
Print failures as filename:line (updated)
Updated version of @JoshuaKGoldberg's #37 to resolve merge conflicts.

Closes #37.
Fixes #31.

Here's Josh's neat video for posterity:

https://user-images.githubusercontent.com/3335181/164457313-8c1600c1-d299-4a8f-a335-ca3caf8d2bc8.mp4


Patch:
diff --git a/src/asciidoc.ts b/src/asciidoc.ts
index e2f58de..1cd1ab1 100644
--- a/src/asciidoc.ts
+++ b/src/asciidoc.ts
@@ -1,5 +1,6 @@
 import {matchAndExtract} from './utils';
 import {Processor} from './code-sample';
+import {generateIdMetadata} from './metadata';
 
 const EXTRACT_BACKTICKS = /```(tsx|ts)/;
 const EXTRACT_ID = /\[\[([^\]]*)\]\]/;
@@ -7,7 +8,7 @@ const EXTRACT_SOURCE = /\[source,(ts|js)\]/;
 const EXTRACT_DIRECTIVE = /^\/\/ verifier:(.*)$/;
 const TOP_HEADER = /^={1,3} (.*)$/;
 
-export function extractAsciidocSamples(text: string, p: Processor) {
+export function extractAsciidocSamples(sourceFile: string, text: string, p: Processor) {
   let isIgnoringFencedCodeBlocks = false;
   const lines = text.split('\n');
 
@@ -32,7 +33,7 @@ export function extractAsciidocSamples(text: string, p: Processor) {
     const directive = matchAndExtract(EXTRACT_DIRECTIVE, line);
 
     if (id) {
-      p.setNextId(id);
+      p.setNextId(generateIdMetadata(id, sourceFile, i));
     } else if (language) {
       p.setNextLanguage(language);
     } else if (header) {
diff --git a/src/code-sample.ts b/src/code-sample.ts
index 851f4ea..9d6ddf4 100644
--- a/src/code-sample.ts
+++ b/src/code-sample.ts
@@ -1,16 +1,17 @@
 import _ from 'lodash';
 
-import {CodeSample, PrefixedCodeSample, Prefix} from './types';
+import {CodeSample, PrefixedCodeSample, Prefix, IdMetadata} from './types';
 import {log} from './logger';
 import {fail} from './test-tracker';
 import {extractAsciidocSamples} from './asciidoc';
 import {extractMarkdownSamples} from './markdown';
+import {generateIdMetadata} from './metadata';
 
 export interface Processor {
   setLineNum(line: number): void;
   setHeader(header: string): void;
   setDirective(directive: string): void;
-  setNextId(id: string): void;
+  setNextId(idMetadata: IdMetadata): void;
   setNextLanguage(lang: string | null): void;
   addSample(code: string): void;
   resetWithNormalLine(): void;
@@ -20,14 +21,13 @@ function process(
   text: string,
   slug: string,
   sourceFile: string,
-  processor: (text: string, processor: Processor) => void,
+  processor: (filename: string, text: string, processor: Processor) => void,
 ): PrefixedCodeSample[] {
   const samples: PrefixedCodeSample[] = [];
 
   let lineNum = 0;
-  let lastSectionId: string | null = null;
   let lastSectionHeader: string | null = null;
-  let lastId: string | null = null;
+  let lastMetadata: IdMetadata | null = null;
   let lastLanguage: string | null = null;
   let prefixes: readonly Prefix[] = [];
   let skipNext = false;
@@ -44,9 +44,8 @@ function process(
     },
     setHeader(header) {
       p.setDirective('reset');
-      lastSectionId = lastId;
       lastSectionHeader = header;
-      lastId = null;
+      lastMetadata = null;
     },
     setDirective(directive) {
       if (directive === 'reset') {
@@ -93,21 +92,23 @@ function process(
       }
     },
     setNextId(id) {
-      lastId = id;
+      lastMetadata = id;
     },
     setNextLanguage(lang) {
       lastLanguage = lang;
     },
     addSample(content) {
-      if (!lastId && (lastLanguage === 'ts' || (lastLanguage === 'js' && nextShouldCheckJs))) {
+      if (
+        !lastMetadata &&
+        (lastLanguage === 'ts' || (lastLanguage === 'js' && nextShouldCheckJs))
+      ) {
         // TS samples get checked even without IDs.
-        lastId = slug + '-' + lineNum;
+        lastMetadata = generateIdMetadata(slug + '-' + lineNum, sourceFile, lineNum);
       }
-      if (lastId) {
+      if (lastMetadata) {
         if (!skipNext) {
           samples.push({
-            id: lastId,
-            sectionId: lastSectionId,
+            ...lastMetadata,
             sectionHeader: lastSectionHeader,
             language: lastLanguage as CodeSample['language'],
             content,
@@ -122,7 +123,7 @@ function process(
         if (prependNext) {
           prefixes = prefixes.concat([
             {
-              id: lastId,
+              id: lastMetadata.id,
               ...(prependLines ? {lines: prependLines} : {}),
             },
           ]);
@@ -132,7 +133,7 @@ function process(
       }
     },
     resetWithNormalLine() {
-      lastId = null;
+      lastMetadata = null;
       lastLanguage = null;
       skipNext = false;
       nextIsTSX = false;
@@ -140,7 +141,7 @@ function process(
     },
   };
 
-  processor(text, p);
+  processor(sourceFile, text, p);
 
   return samples;
 }
@@ -166,7 +167,7 @@ export function checkSource(sample: CodeSample, source: string) {
   // Strip out code behind HIDE..END markers
   const strippedSource = stripSource(source);
   if (sample.content.trim() !== strippedSource.trim()) {
-    fail('Inline sample does not match sample in source file', sample.id);
+    fail('Inline sample does not match sample in source file', sample);
     log('Inline sample:');
     log(sample.content.trim());
     log('----');
@@ -196,8 +197,8 @@ export function applyPrefixes(
       .concat([sources[sample.id] || sample.content])
       .join('\n');
     return {
+      descriptor: sample.descriptor,
       id: sample.id,
-      sectionId: sample.sectionId,
       sectionHeader: sample.sectionHeader,
       language: sample.language,
       tsOptions: sample.tsOptions,
diff --git a/src/main.ts b/src/main.ts
index c172959..9fd40e6 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -119,7 +119,7 @@ function checkOutput(expectedOutput: string, input: CodeSample) {
 async function checkSample(sample: CodeSample, idToSample: {[id: string]: CodeSample}) {
   const {id, language} = sample;
   const {content} = sample;
-  startSample(id);
+  startSample(sample);
 
   if (language === 'ts' || (language === 'js' && sample.checkJS)) {
     await checkTs(sample, id + '-output' in idToSample, typeScriptBundle);
@@ -156,7 +156,7 @@ function setStatus(status: string) {
 }
 
 async function processSourceFile(path: string, fileNum: number, outOf: number) {
-  const fileStatus = `${fileNum}/${outOf}: ${path}`;
+  const fileStatus = `${fileNum}/${outOf}: ./${path}`;
   setStatus(fileStatus);
   startFile(path);
 
@@ -180,7 +180,7 @@ async function processSourceFile(path: string, fileNum: number, outOf: number) {
     if (argv.filter && !sample.id.startsWith(argv.filter)) {
       continue;
     }
-    setStatus(`${fileStatus}: ${n}/${samples.length} ${sample.id}`);
+    setStatus(`${fileStatus}: ${n}/${samples.length} ${sample.descriptor}`);
     await checkSample(sample, outputs);
   }
 
@@ -200,7 +200,7 @@ export function main() {
 
     for (const [file, fileResults] of Object.entries(getTestResults())) {
       const numPassed = _.sum(_.map(fileResults, n => (n === 0 ? 1 : 0)));
-      console.log(file, `${numPassed}/${_.size(fileResults)} passed`);
+      console.log(`./${file}`, `${numPassed}/${_.size(fileResults)} passed`);
       for (const [id, failures] of Object.entries(fileResults)) {
         numTotal += 1;
         if (failures > 0) {
diff --git a/src/markdown.ts b/src/markdown.ts
index 8ba4177..88e88ff 100644
--- a/src/markdown.ts
+++ b/src/markdown.ts
@@ -1,5 +1,6 @@
 import {matchAndExtract} from './utils';
 import {Processor} from './code-sample';
+import {generateIdMetadata} from './metadata';
 
 const EXTRACT_ID = /<!-- #([^ ]+) -->/;
 const EXTRACT_DIRECTIVE = /<!-- verifier:(.*) -->/;
@@ -7,7 +8,7 @@ const ALT_EXTRACT_ID = /^\/\/ #([^ ]+)$/;
 const ALT_EXTRACT_DIRECTIVE = /^\/\/ verifier:(.*)$/;
 const TOP_HEADER = /^#{1,3} (.*)$/;
 
-export function extractMarkdownSamples(text: string, p: Processor) {
+export function extractMarkdownSamples(sourceFile: string, text: string, p: Processor) {
   const lines = text.split('\n');
 
   for (let i = 0; i < lines.length; i++) {
@@ -18,7 +19,7 @@ export function extractMarkdownSamples(text: string, p: Processor) {
       matchAndExtract(EXTRACT_DIRECTIVE, line) || matchAndExtract(ALT_EXTRACT_DIRECTIVE, line);
 
     if (id) {
-      p.setNextId(id);
+      p.setNextId(generateIdMetadata(id, sourceFile, i));
     } else if (header) {
       p.setHeader(header);
     } else if (directive) {
diff --git a/src/metadata.ts b/src/metadata.ts
new file mode 100644
index 0000000..6076e2e
--- /dev/null
+++ b/src/metadata.ts
@@ -0,0 +1,8 @@
+import {IdMetadata} from './types';
+
+export function generateIdMetadata(id: string, sourceFile: string, line: number): IdMetadata {
+  return {
+    descriptor: `./${sourceFile}:${line}`,
+    id,
+  };
+}
diff --git a/src/ts-checker.ts b/src/ts-checker.ts
index 4ef64fb..29faae9 100644
--- a/src/ts-checker.ts
+++ b/src/ts-checker.ts
@@ -290,13 +290,8 @@ export function checkTypeAssertions(
 }
 
 /** Verify that a TypeScript sample has the expected errors and no others. */
-export async function checkTs(
-  content: string,
-  sample: CodeSample,
-  runCode: boolean,
-  config: ConfigBundle,
-) {
-  const {id} = sample;
+export async function checkTs(sample: CodeSample, runCode: boolean, config: ConfigBundle) {
+  const {id, content} = sample;
   const fileName = id + (sample.isTSX ? '.tsx' : `.${sample.language}`);
   const tsFile = writeTempFile(fileName, content);
   const sampleDir = getTempDir();
diff --git a/src/types.ts b/src/types.ts
index 9b649df..64d0185 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,9 +1,14 @@
 import {CompilerOptions} from 'typescript';
 
+export interface IdMetadata {
+  descriptor: string;
+  id: string;
+}
+
 export interface CodeSample {
+  descriptor: string;
   language: 'js' | 'ts' | null;
   id: string;
-  sectionId: string | null;
   sectionHeader: string | null;
   content: string;
   isTSX: boolean;


Test patch:
diff --git a/src/test-tracker.ts b/src/test-tracker.ts
index 6fea9c2..12e9ce2 100644
--- a/src/test-tracker.ts
+++ b/src/test-tracker.ts
@@ -1,9 +1,10 @@
 import {log, isLoggingToStderr} from './logger';
+import {CodeSample} from './types';
 
 let currentFile: string;
-let currentSampleId: string;
+let currentSample: CodeSample | undefined;
 let sampleStartMs: number;
-const results: {[file: string]: {[id: string]: number}} = {};
+const results: {[file: string]: {[descriptor: string]: number}} = {};
 
 export function startFile(file: string) {
   log(`---- BEGIN FILE ${file}\n`);
@@ -14,34 +15,34 @@ export function startFile(file: string) {
 export function finishFile() {
   log(`---- END FILE ${currentFile}\n`);
   currentFile = '';
-  currentSampleId = '';
+  currentSample = undefined;
 }
 
-export function startSample(sampleId: string) {
-  currentSampleId = sampleId;
-  results[currentFile][currentSampleId] = 0;
+export function startSample(sample: CodeSample) {
+  currentSample = sample;
+  results[currentFile][sample.descriptor] = 0;
   sampleStartMs = Date.now();
-  log(`BEGIN #${sampleId}\n`);
+  log(`BEGIN #${sample.descriptor}\n`);
 }
 
 export function finishSample() {
   const elapsedMs = Date.now() - sampleStartMs;
-  log(`\nEND #${currentSampleId} (${elapsedMs} ms)\n`);
-  currentSampleId = '';
+  log(`\nEND #${currentSample!.descriptor} (${elapsedMs} ms)\n`);
+  currentSample = undefined;
 }
 
-export function fail(message: string, sampleId?: string) {
-  if (sampleId === undefined) {
-    sampleId = currentSampleId;
+export function fail(message: string, sample?: CodeSample) {
+  if (sample === undefined) {
+    sample = currentSample;
   }
 
-  const fullMessage = `${currentSampleId}: ${message}`;
+  const fullMessage = `${currentSample?.descriptor}: ${message}`;
   if (!isLoggingToStderr()) {
     console.error('\n' + fullMessage);
   }
   log(fullMessage);
   if (!(global as any).__TEST__) {
-    results[currentFile][currentSampleId]++;
+    results[currentFile][currentSample!.descriptor]++;
   }
 }
 
diff --git a/src/test/__snapshots__/asciidoc.test.ts.snap b/src/test/__snapshots__/asciidoc.test.ts.snap
index 6c049e0..d5d24c9 100644
--- a/src/test/__snapshots__/asciidoc.test.ts.snap
+++ b/src/test/__snapshots__/asciidoc.test.ts.snap
@@ -7,39 +7,39 @@ Array [
     "content": "function greet(who: string) {
   console.log('Hello', who);
 }",
+    "descriptor": "./backticks.asciidoc:2",
     "id": "greet-ts",
     "isTSX": false,
     "language": "ts",
     "nodeModules": Array [],
     "prefixes": Array [],
     "sectionHeader": null,
-    "sectionId": null,
     "sourceFile": "backticks.asciidoc",
     "tsOptions": Object {},
   },
   Object {
     "checkJS": false,
     "content": "export const Example = () => <div />;",
+    "descriptor": "./backticks.asciidoc:11",
     "id": "greet-tsx",
     "isTSX": false,
     "language": "tsx",
     "nodeModules": Array [],
     "prefixes": Array [],
     "sectionHeader": null,
-    "sectionId": null,
     "sourceFile": "backticks.asciidoc",
     "tsOptions": Object {},
   },
   Object {
     "checkJS": false,
     "content": "console.log(\\"end of file\\");",
+    "descriptor": "./backticks.asciidoc:26",
     "id": "backticks-26",
     "isTSX": false,
     "language": "ts",
     "nodeModules": Array [],
     "prefixes": Array [],
     "sectionHeader": null,
-    "sectionId": null,
     "sourceFile": "backticks.asciidoc",
     "tsOptions": Object {},
   },
@@ -53,13 +53,13 @@ Array [
   Object {
     "checkJS": false,
     "content": "function somethingDangerous() {}",
+    "descriptor": "./commented-sample.asciidoc:6",
     "id": "commented-sample-6",
     "isTSX": false,
     "language": "ts",
     "nodeModules": Array [],
     "prefixes": Array [],
     "sectionHeader": null,
-    "sectionId": null,
     "sourceFile": "commented-sample.asciidoc",
     "tsOptions": Object {},
   },
@@ -74,6 +74,7 @@ try {
   console.warn('alas!');
 }
 val  // Type is number | null",
+    "descriptor": "./commented-sample.asciidoc:12",
     "id": "commented-sample-12",
     "isTSX": false,
     "language": "ts",
@@ -84,7 +85,6 @@ val  // Type is number | null",
       },
     ],
     "sectionHeader": null,
-    "sectionId": null,
     "sourceFile": "commented-sample.asciidoc",
     "tsOptions": Object {},
   },
@@ -98,13 +98,13 @@ Array [
     "content": "function greet(who: string) {
   console.log('Hello', who);
 }",
+    "descriptor": "./doc1.asciidoc:2",
     "id": "greet-ts",
     "isTSX": false,
     "language": "ts",
     "nodeModules": Array [],
     "prefixes": Array [],
     "sectionHeader": null,
-    "sectionId": null,
     "sourceFile": "doc1.asciidoc",
     "tsOptions": Object {},
   },
@@ -120,13 +120,13 @@ Array [
 //   x: number;
 //   y: number;
 // }",
+    "descriptor": "./multilinetype.asciidoc:3",
     "id": "multilinetype-3",
     "isTSX": false,
     "language": "ts",
     "nodeModules": Array [],
     "prefixes": Array [],
     "sectionHeader": null,
-    "sectionId": null,
     "sourceFile": "multilinetype.asciidoc",
     "tsOptions": Object {},
   },
@@ -137,13 +137,13 @@ Array [
   // ...
   return c;
 }",
+    "descriptor": "./multilinetype.asciidoc:13",
     "id": "multilinetype-13",
     "isTSX": false,
     "language": "ts",
     "nodeModules": Array [],
     "prefixes": Array [],
     "sectionHeader": null,
-    "sectionId": null,
     "sourceFile": "multilinetype.asciidoc",
     "tsOptions": Object {},
   },
@@ -155,13 +155,13 @@ Array [
   Object {
     "checkJS": false,
     "content": "console.log('Hello', 'TS');",
+    "descriptor": "./noid.asciidoc:9",
     "id": "noid-9",
     "isTSX": false,
     "language": "ts",
     "nodeModules": Array [],
     "prefixes": Array [],
     "sectionHeader": null,
-    "sectionId": null,
     "sourceFile": "noid.asciidoc",
     "tsOptions": Object {},
   },
@@ -173,19 +173,20 @@ Array [
   Object {
     "checkJS": false,
     "content": "type AB = 'a' | 'b';",
+    "descriptor": "./prepend.asciidoc:1",
     "id": "prefix",
     "isTSX": false,
     "language": "ts",
     "nodeModules": Array [],
     "prefixes": Array [],
     "sectionHeader": null,
-    "sectionId": null,
     "sourceFile": "prepend.asciidoc",
     "tsOptions": Object {},
   },
   Object {
     "checkJS": false,
     "content": "const a: AB = 'a';",
+    "descriptor": "./prepend.asciidoc:7",
     "id": "combined",
     "isTSX": false,
     "language": "ts",
@@ -196,20 +197,19 @@ Array [
       },
     ],
     "sectionHeader": null,
-    "sectionId": null,
     "sourceFile": "prepend.asciidoc",
     "tsOptions": Object {},
   },
   Object {
     "checkJS": false,
     "content": "const a: AB = 'a';",
+    "descriptor": "./prepend.asciidoc:15",
     "id": "final",
     "isTSX": false,
     "language": "ts",
     "nodeModules": Array [],
     "prefixes": Array [],
     "sectionHeader": null,
-    "sectionId": null,
     "sourceFile": "prepend.asciidoc",
     "tsOptions": Object {},
   },
@@ -221,19 +221,20 @@ Array [
   Object {
     "checkJS": false,
     "content": "type AB = 'a' | 'b';",
+    "descriptor": "./prepend-multiple.asciidoc:3",
     "id": "prepend-multiple-3",
     "isTSX": false,
     "language": "ts",
     "nodeModules": Array [],
     "prefixes": Array [],
     "sectionHeader": null,
-    "sectionId": null,
     "sourceFile": "prepend-multiple.asciidoc",
     "tsOptions": Object {},
   },
   Object {
     "checkJS": false,
     "content": "type ABC = AB | 'c';",
+    "descriptor": "./prepend-multiple.asciidoc:9",
     "id": "prepend-multiple-9",
     "isTSX": false,
     "language": "ts",
@@ -244,13 +245,13 @@ Array [
       },
     ],
     "sectionHeader": null,
-    "sectionId": null,
     "sourceFile": "prepend-multiple.asciidoc",
     "tsOptions": Object {},
   },
   Object {
     "checkJS": false,
     "content": "const c: ABC = 'c';",
+    "descriptor": "./prepend-multiple.asciidoc:14",
     "id": "prepend-multiple-14",
     "isTSX": false,
     "language": "ts",
@@ -264,7 +265,6 @@ Array [
       },
     ],
     "sectionHeader": null,
-    "sectionId": null,
     "sourceFile": "prepend-multiple.asciidoc",
     "tsOptions": Object {},
   },
diff --git a/src/test/__snapshots__/markdown.test.ts.snap b/src/test/__snapshots__/markdown.test.ts.snap
index 9904727..8741633 100644
--- a/src/test/__snapshots__/markdown.test.ts.snap
+++ b/src/test/__snapshots__/markdown.test.ts.snap
@@ -5,13 +5,13 @@ Array [
   Object {
     "checkJS": false,
     "content": "function somethingDangerous() {}",
+    "descriptor": "./commented-sample.md:5",
     "id": "commented-sample-5",
     "isTSX": false,
     "language": "ts",
     "nodeModules": Array [],
     "prefixes": Array [],
     "sectionHeader": null,
-    "sectionId": null,
     "sourceFile": "commented-sample.md",
     "tsOptions": Object {},
   },
@@ -26,6 +26,7 @@ try {
   console.warn('alas!');
 }
 val  // Type is number | null",
+    "descriptor": "./commented-sample.md:10",
     "id": "commented-sample-10",
     "isTSX": false,
     "language": "ts",
@@ -36,7 +37,6 @@ val  // Type is number | null",
       },
     ],
     "sectionHeader": null,
-    "sectionId": null,
     "sourceFile": "commented-sample.md",
     "tsOptions": Object {},
   },
@@ -50,13 +50,13 @@ Array [
     "content": "function greet(who: string) {
   console.log('Hello', who);
 }",
+    "descriptor": "./doc1.md:2",
     "id": "greet-ts",
     "isTSX": false,
     "language": "ts",
     "nodeModules": Array [],
     "prefixes": Array [],
     "sectionHeader": null,
-    "sectionId": null,
     "sourceFile": "doc1.md",
     "tsOptions": Object {},
   },
@@ -64,13 +64,13 @@ Array [
     "checkJS": false,
     "content": "console.log(\\"end of file\\");
 ",
+    "descriptor": "./doc1.md:19",
     "id": "doc1-19",
     "isTSX": false,
     "language": "ts",
     "nodeModules": Array [],
     "prefixes": Array [],
     "sectionHeader": null,
-    "sectionId": null,
     "sourceFile": "doc1.md",
     "tsOptions": Object {},
   },
@@ -86,13 +86,13 @@ Array [
 //   x: number;
 //   y: number;
 // }",
+    "descriptor": "./multilinetype.md:3",
     "id": "multilinetype-3",
     "isTSX": false,
     "language": "ts",
     "nodeModules": Array [],
     "prefixes": Array [],
     "sectionHeader": null,
-    "sectionId": null,
     "sourceFile": "multilinetype.md",
     "tsOptions": Object {},
   },
@@ -103,13 +103,13 @@ Array [
   // ...
   return c;
 }",
+    "descriptor": "./multilinetype.md:13",
     "id": "multilinetype-13",
     "isTSX": false,
     "language": "ts",
     "nodeModules": Array [],
     "prefixes": Array [],
     "sectionHeader": null,
-    "sectionId": null,
     "sourceFile": "multilinetype.md",
     "tsOptions": Object {},
   },
@@ -121,13 +121,13 @@ Array [
   Object {
     "checkJS": false,
     "content": "console.log('Hello', 'TS');",
+    "descriptor": "./noid.md:7",
     "id": "noid-7",
     "isTSX": false,
     "language": "ts",
     "nodeModules": Array [],
     "prefixes": Array [],
     "sectionHeader": null,
-    "sectionId": null,
     "sourceFile": "noid.md",
     "tsOptions": Object {},
   },
@@ -139,19 +139,20 @@ Array [
   Object {
     "checkJS": false,
     "content": "type AB = 'a' | 'b';",
+    "descriptor": "./prepend.md:1",
     "id": "prefix",
     "isTSX": false,
     "language": "ts",
     "nodeModules": Array [],
     "prefixes": Array [],
     "sectionHeader": null,
-    "sectionId": null,
     "sourceFile": "prepend.md",
     "tsOptions": Object {},
   },
   Object {
     "checkJS": false,
     "content": "const a: AB = 'a';",
+    "descriptor": "./prepend.md:6",
     "id": "combined",
     "isTSX": false,
     "language": "ts",
@@ -162,20 +163,19 @@ Array [
       },
     ],
     "sectionHeader": null,
-    "sectionId": null,
     "sourceFile": "prepend.md",
     "tsOptions": Object {},
   },
   Object {
     "checkJS": false,
     "content": "const a: AB = 'a';",
+    "descriptor": "./prepend.md:13",
     "id": "final",
     "isTSX": false,
     "language": "ts",
     "nodeModules": Array [],
     "prefixes": Array [],
     "sectionHeader": null,
-    "sectionId": null,
     "sourceFile": "prepend.md",
     "tsOptions": Object {},
   },
@@ -187,19 +187,20 @@ Array [
   Object {
     "checkJS": false,
     "content": "type AB = 'a' | 'b';",
+    "descriptor": "./prepend-multiple.md:2",
     "id": "prepend-multiple-2",
     "isTSX": false,
     "language": "ts",
     "nodeModules": Array [],
     "prefixes": Array [],
     "sectionHeader": null,
-    "sectionId": null,
     "sourceFile": "prepend-multiple.md",
     "tsOptions": Object {},
   },
   Object {
     "checkJS": false,
     "content": "type ABC = AB | 'c';",
+    "descriptor": "./prepend-multiple.md:7",
     "id": "prepend-multiple-7",
     "isTSX": false,
     "language": "ts",
@@ -210,13 +211,13 @@ Array [
       },
     ],
     "sectionHeader": null,
-    "sectionId": null,
     "sourceFile": "prepend-multiple.md",
     "tsOptions": Object {},
   },
   Object {
     "checkJS": false,
     "content": "const c: ABC = 'c';",
+    "descriptor": "./prepend-multiple.md:11",
     "id": "prepend-multiple-11",
     "isTSX": false,
     "language": "ts",
@@ -230,7 +231,6 @@ Array [
       },
     ],
     "sectionHeader": null,
-    "sectionId": null,
     "sourceFile": "prepend-multiple.md",
     "tsOptions": Object {},
   },
diff --git a/src/test/asciidoc.test.ts b/src/test/asciidoc.test.ts
index 065d46b..e6e96c7 100644
--- a/src/test/asciidoc.test.ts
+++ b/src/test/asciidoc.test.ts
@@ -92,6 +92,7 @@ describe('extractSamples', () => {
       {
         ...baseExtract,
         language: 'ts',
+        descriptor: './source.asciidoc:4',
         id: 'greet-ts',
         content: dedent`
           function greet(who: string) {
@@ -107,6 +108,7 @@ describe('extractSamples', () => {
       {
         ...baseExtract,
         language: 'ts',
+        descriptor: './source.asciidoc:10',
         id: 'noid-10',
         content: `console.log('Hello', 'TS');`,
       },
@@ -145,6 +147,7 @@ describe('extractSamples', () => {
       {
         ...baseExtract,
         language: 'ts',
+        descriptor: './source.asciidoc:2',
         id: 'implicit-any',
         content: `const x: number = null;`,
         tsOptions: {
@@ -155,6 +158,7 @@ describe('extractSamples', () => {
       {
         ...baseExtract,
         language: 'ts',
+        descriptor: './source.asciidoc:10',
         id: 'strict',
         content: `const x: number = null;`,
         tsOptions: {
@@ -188,6 +192,7 @@ describe('extractSamples', () => {
       {
         ...baseExtract,
         language: 'ts',
+        descriptor: './source.asciidoc:27',
         id: 'prepended-with-ids',
         content: `console.log(a);`,
         prefixes: [{id: 'prefix'}, {id: 'combined'}],
@@ -213,6 +218,7 @@ describe('extractSamples', () => {
       {
         ...baseExtract,
         language: 'ts',
+        descriptor: './source.asciidoc:1',
         id: 'tsx-example',
         content: `console.log(a);`,
         isTSX: true,
@@ -244,6 +250,7 @@ describe('extractSamples', () => {
     ).toEqual([
       {
         ...baseExtract,
+        descriptor: './source.asciidoc:4',
         id: 'header-reset-4',
         sectionHeader: 'Chapter 1',
         language: 'ts',
@@ -251,8 +258,8 @@ describe('extractSamples', () => {
       },
       {
         ...baseExtract,
+        descriptor: './source.asciidoc:11',
         id: 'header-reset-11',
-        sectionId: 'chapter-2',
         sectionHeader: 'Chapter 2',
         language: 'ts',
         content: `const x = 12;`,
@@ -281,6 +288,7 @@ describe('extractSamples', () => {
     ).toEqual([
       {
         ...baseExtract,
+        descriptor: './source.asciidoc:4',
         id: 'header-reset-4',
         sectionHeader: 'Chapter 1',
         language: 'ts',
@@ -288,6 +296,7 @@ describe('extractSamples', () => {
       },
       {
         ...baseExtract,
+        descriptor: './source.asciidoc:10',
         id: 'header-reset-10',
         sectionHeader: 'Chapter 1',
         language: 'ts',
diff --git a/src/test/code-sample.test.ts b/src/test/code-sample.test.ts
index c7e413a..2884808 100644
--- a/src/test/code-sample.test.ts
+++ b/src/test/code-sample.test.ts
@@ -50,12 +50,14 @@ describe('code-sample', () => {
     expect(applyPrefixes(extractSamples(ASCIIDOC_PREPEND, 'prepend', 'source.asciidoc'))).toEqual([
       {
         ...baseSample,
+        descriptor: './source.asciidoc:3',
         language: 'ts',
         id: 'prefix',
         content: `type AB = 'a' | 'b';`,
       },
       {
         ...baseSample,
+        descriptor: './source.asciidoc:9',
         language: 'ts',
         id: 'combined',
         content: dedent`
@@ -64,6 +66,7 @@ describe('code-sample', () => {
       },
       {
         ...baseSample,
+        descriptor: './source.asciidoc:17',
         language: 'ts',
         id: 'final',
         content: `const a: AB = 'a';`,
@@ -77,12 +80,14 @@ describe('code-sample', () => {
     ).toEqual([
       {
         ...baseSample,
+        descriptor: './source.asciidoc:4',
         language: 'ts',
         id: 'mpd-4',
         content: `type AB = 'a' | 'b';`,
       },
       {
         ...baseSample,
+        descriptor: './source.asciidoc:10',
         language: 'ts',
         id: 'mpd-10',
         content: dedent`
@@ -91,6 +96,7 @@ describe('code-sample', () => {
       },
       {
         ...baseSample,
+        descriptor: './source.asciidoc:15',
         language: 'ts',
         id: 'mpd-15',
         content: dedent`
@@ -129,6 +135,7 @@ describe('code-sample', () => {
     ).toEqual([
       {
         ...baseSample,
+        descriptor: './source.asciidoc:1',
         language: 'ts',
         id: 'a',
         content: dedent`
@@ -139,6 +146,7 @@ describe('code-sample', () => {
       },
       {
         ...baseSample,
+        descriptor: './source.asciidoc:10',
         language: 'ts',
         id: 'b',
         content: dedent`
@@ -177,6 +185,7 @@ describe('code-sample', () => {
     ).toEqual([
       {
         ...baseSample,
+        descriptor: './source.asciidoc:1',
         language: 'js',
         id: 'a',
         content: dedent`
@@ -186,6 +195,7 @@ describe('code-sample', () => {
       },
       {
         ...baseSample,
+        descriptor: './source.asciidoc:9',
         language: 'ts',
         id: 'b',
         content: dedent`
@@ -221,12 +231,14 @@ describe('code-sample', () => {
     ).toEqual([
       {
         ...baseSample,
+        descriptor: './source.asciidoc:1',
         language: 'ts',
         id: 'hello',
         content: `console.log('Hello');`,
       },
       {
         ...baseSample,
+        descriptor: './source.asciidoc:8',
         language: null,
         id: 'hello-output',
         content: 'Hello',
diff --git a/src/test/common.ts b/src/test/common.ts
index 376f615..76a6235 100644
--- a/src/test/common.ts
+++ b/src/test/common.ts
@@ -5,7 +5,6 @@ export const baseSample = {
   nodeModules: [],
   isTSX: false,
   checkJS: false,
-  sectionId: null,
   sectionHeader: null,
   sourceFile: 'source.asciidoc',
 };