Skip to content

Commit 2936a9f

Browse files
committed
integrate code block extraction into doctests script
1 parent 4177bc3 commit 2936a9f

12 files changed

+77
-425
lines changed

analysis/src/Protocol.ml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,11 @@ let optWrapInQuotes s =
137137
| None -> None
138138
| Some s -> Some (wrapInQuotes s)
139139

140+
let stringifyResult = function
141+
| Ok r -> stringifyObject [("TAG", Some (wrapInQuotes "Ok")); ("_0", Some r)]
142+
| Error e ->
143+
stringifyObject [("TAG", Some (wrapInQuotes "Error")); ("_0", Some e)]
144+
140145
let stringifyCompletionItem c =
141146
stringifyObject
142147
[

runtime/RescriptTools_ExtractCodeBlocks.res

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ type codeBlock = {
77
/**
88
`decodeFromJson(json)` parse JSON generated from `rescript-tools extract-codeblocks` command
99
*/
10-
external decodeFromJson: Stdlib_JSON.t => array<codeBlock> = "%identity"
10+
external decodeFromJson: Stdlib_JSON.t => result<array<codeBlock>, string> = "%identity"

tests/docstring_tests/DocTest.res

Lines changed: 13 additions & 187 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,5 @@
11
open Node
22

3-
module Docgen = RescriptTools.Docgen
4-
5-
type example = {
6-
id: string,
7-
kind: string,
8-
name: string,
9-
docstrings: array<string>,
10-
}
11-
123
// Only major version
134
let nodeVersion =
145
Process.version
@@ -49,189 +40,21 @@ let getOutput = buffer =>
4940
let extractDocFromFile = async file => {
5041
let toolsBin = Path.join([Process.cwd(), "cli", "rescript-tools.js"])
5142

52-
let {stdout} = await SpawnAsync.run(~command=toolsBin, ~args=["doc", file])
43+
let {stdout} = await SpawnAsync.run(
44+
~command=toolsBin,
45+
~args=["extract-codeblocks", file, "--transform-assert-equal"],
46+
)
5347

5448
try {
5549
stdout
5650
->getOutput
5751
->JSON.parseOrThrow
58-
->Docgen.decodeFromJson
52+
->RescriptTools.ExtractCodeBlocks.decodeFromJson
5953
} catch {
60-
| JsExn(_) => JsError.panic(`Failed to generate docstrings from ${file}`)
61-
| _ => assert(false)
62-
}
63-
}
64-
65-
let getExamples = ({items}: Docgen.doc) => {
66-
let rec loop = (items: list<Docgen.item>, acc: list<example>) => {
67-
switch items {
68-
| list{Value({docstrings, id, name}), ...rest} =>
69-
loop(rest, list{{id, name, docstrings, kind: "value"}, ...acc})
70-
| list{Type({docstrings, id, name}), ...rest} =>
71-
loop(rest, list{{id, name, docstrings, kind: "type"}, ...acc})
72-
| list{Module({id, name, docstrings, items}), ...rest} =>
73-
loop(
74-
list{...rest, ...List.fromArray(items)},
75-
list{{id, name, docstrings, kind: "module"}, ...acc},
76-
)
77-
| list{ModuleType({id, name, docstrings, items}), ...rest} =>
78-
loop(
79-
list{...rest, ...List.fromArray(items)},
80-
list{{id, name, docstrings, kind: "moduleType"}, ...acc},
81-
)
82-
| list{ModuleAlias({id, name, docstrings, items}), ...rest} =>
83-
loop(
84-
list{...rest, ...List.fromArray(items)},
85-
list{{id, name, docstrings, kind: "moduleAlias"}, ...acc},
86-
)
87-
| list{} => acc
88-
}
54+
| JsExn(e) =>
55+
Console.error(e)
56+
JsError.panic(`Failed to extract code blocks from ${file}`)
8957
}
90-
91-
items
92-
->List.fromArray
93-
->loop(list{})
94-
->List.toArray
95-
->Array.filter(({docstrings}) => Array.length(docstrings) > 0)
96-
}
97-
98-
let getCodeBlocks = example => {
99-
let rec loopEndCodeBlock = (lines, acc) => {
100-
switch lines {
101-
| list{hd, ...rest} =>
102-
if (
103-
hd
104-
->String.trim
105-
->String.endsWith("```")
106-
) {
107-
acc
108-
} else {
109-
loopEndCodeBlock(rest, list{hd, ...acc})
110-
}
111-
| list{} => panic(`Failed to find end of code block for ${example.kind}: ${example.id}`)
112-
}
113-
}
114-
115-
// Transform lines that contain == patterns to use assertEqual
116-
let transformEqualityAssertions = code => {
117-
let lines = code->String.split("\n")
118-
lines
119-
->Array.mapWithIndex((line, idx) => {
120-
let trimmedLine = line->String.trim
121-
122-
// Check if the line contains == and is not inside a comment
123-
if (
124-
trimmedLine->String.includes("==") &&
125-
!(trimmedLine->String.startsWith("//")) &&
126-
!(trimmedLine->String.startsWith("/*")) &&
127-
// Not an expression line
128-
!(trimmedLine->String.startsWith("if")) &&
129-
!(trimmedLine->String.startsWith("|")) &&
130-
!(trimmedLine->String.startsWith("let")) &&
131-
!(trimmedLine->String.startsWith("~")) &&
132-
!(trimmedLine->String.startsWith("module")) &&
133-
!(trimmedLine->String.startsWith("->")) &&
134-
!(trimmedLine->String.endsWith(","))
135-
) {
136-
// Split string at == (not ===) and transform to assertEqual
137-
let parts = {
138-
let rec searchFrom = (currentLine: string, startIndex: int): option<(string, string)> => {
139-
if startIndex >= currentLine->String.length {
140-
// Base case: reached end of string without finding a suitable "=="
141-
None
142-
} else {
143-
let lineSuffix = currentLine->String.sliceToEnd(~start=startIndex)
144-
let idxEqEqInSuffix = lineSuffix->String.indexOfOpt("==")
145-
let idxEqEqEqInSuffix = lineSuffix->String.indexOfOpt("===")
146-
147-
switch (idxEqEqInSuffix, idxEqEqEqInSuffix) {
148-
| (None, _) =>
149-
// No "==" found in the rest of the string.
150-
None
151-
| (Some(iEqEq), None) =>
152-
// Found "==" but no "===" in the suffix.
153-
// This "==" must be standalone.
154-
// Calculate its absolute index in the original `currentLine`.
155-
let actualIdx = startIndex + iEqEq
156-
let left = currentLine->String.slice(~start=0, ~end=actualIdx)
157-
let right = currentLine->String.sliceToEnd(~start=actualIdx + 2)
158-
Some((left, right))
159-
| (Some(iEqEq), Some(iEqEqEq)) =>
160-
// Found both "==" and "===" in the suffix.
161-
if iEqEq < iEqEqEq {
162-
// The "==" occurs before "===". This "==" is standalone.
163-
// Example: "a == b === c". In suffix "a == b === c", iEqEq is for "a ==".
164-
let actualIdx = startIndex + iEqEq
165-
let left = currentLine->String.slice(~start=0, ~end=actualIdx)
166-
let right = currentLine->String.sliceToEnd(~start=actualIdx + 2)
167-
Some((left, right))
168-
} else {
169-
// iEqEq >= iEqEqEq
170-
// This means the first "==" encountered (at iEqEq relative to suffix)
171-
// is part of or comes after the first "===" encountered (at iEqEqEq relative to suffix).
172-
// Example: "a === b". In suffix "a === b", iEqEqEq is for "a ===", iEqEq is also for "a ==".
173-
// We must skip over the "===" found at iEqEqEq and search again.
174-
// The next search should start after this "===".
175-
// The "===" starts at (startIndex + iEqEqEq) in the original line. It has length 3.
176-
searchFrom(currentLine, startIndex + iEqEqEq + 3)
177-
}
178-
}
179-
}
180-
}
181-
searchFrom(line, 0)
182-
}
183-
let parts = switch parts {
184-
| Some((left, right)) if right->String.trim->String.length === 0 =>
185-
Some((
186-
left,
187-
lines
188-
->Array.get(idx + 1)
189-
->Option.getOrThrow(
190-
~message="Expected to have an expected expression on the next line",
191-
),
192-
))
193-
| _ => parts
194-
}
195-
switch parts {
196-
| Some((left, right)) if !(right->String.includes(")") || right->String.includes("//")) =>
197-
`(${left->String.trim})->assertEqual(${right->String.trim})`
198-
| _ => line
199-
}
200-
} else {
201-
line
202-
}
203-
})
204-
->Array.join("\n")
205-
}
206-
207-
let rec loop = (lines: list<string>, acc: list<string>) => {
208-
switch lines {
209-
| list{hd, ...rest} =>
210-
switch hd
211-
->String.trim
212-
->String.startsWith("```res") {
213-
| true =>
214-
let code = loopEndCodeBlock(rest, list{})
215-
let codeString =
216-
code
217-
->List.reverse
218-
->List.toArray
219-
->Array.join("\n")
220-
->transformEqualityAssertions
221-
loop(rest, list{codeString, ...acc})
222-
| false => loop(rest, acc)
223-
}
224-
| list{} => acc
225-
}
226-
}
227-
228-
example.docstrings
229-
->Array.reduce([], (acc, docstring) => acc->Array.concat(docstring->String.split("\n")))
230-
->List.fromArray
231-
->loop(list{})
232-
->List.toArray
233-
->Belt.Array.reverse
234-
->Array.join("\n\n")
23558
}
23659

23760
let batchSize = OS.cpus()->Array.length
@@ -258,7 +81,10 @@ let extractExamples = async () => {
25881
let examples = []
25982
await docFiles->ArrayUtils.forEachAsyncInBatches(~batchSize, async f => {
26083
let doc = await extractDocFromFile(Path.join(["runtime", f]))
261-
examples->Array.pushMany(doc->getExamples)
84+
switch doc {
85+
| Ok(doc) => examples->Array.pushMany(doc)
86+
| Error(e) => Console.error(e)
87+
}
26288
})
26389

26490
examples->Array.sort((a, b) => String.compare(a.id, b.id))
@@ -303,7 +129,7 @@ let main = async () => {
303129
)
304130
None
305131
} else {
306-
let code = getCodeBlocks(example)
132+
let code = example.code
307133

308134
if code->String.length === 0 {
309135
None

0 commit comments

Comments
 (0)