From 117d81371a58d5e4db43f819d52792ec2cb1c53c Mon Sep 17 00:00:00 2001 From: Dmitry Zakharov Date: Wed, 28 May 2025 12:55:52 +0400 Subject: [PATCH 1/2] Turn eqeq in doc comments to asserts for testing --- runtime/Belt_HashMap.resi | 2 +- runtime/Stdlib_Console.resi | 14 ++-- runtime/Stdlib_String.resi | 7 +- scripts/format_check.sh | 2 +- tests/docstring_tests/DocTest.res | 107 ++++++++++++++++++++++++--- tests/docstring_tests/DocTest.res.js | 61 ++++++++++++++- 6 files changed, 168 insertions(+), 25 deletions(-) diff --git a/runtime/Belt_HashMap.resi b/runtime/Belt_HashMap.resi index 359757c946..170396b160 100644 --- a/runtime/Belt_HashMap.resi +++ b/runtime/Belt_HashMap.resi @@ -174,7 +174,7 @@ let s1 = Belt.HashMap.copy(s0) Belt.HashMap.set(s0, 2, "3") -assertEqual(Belt.HashMap.get(s0, 2) == Belt.HashMap.get(s1, 2), false) +assertEqual(Belt.HashMap.get(s0, 2) != Belt.HashMap.get(s1, 2), true) ``` */ let copy: t<'key, 'value, 'id> => t<'key, 'value, 'id> diff --git a/runtime/Stdlib_Console.resi b/runtime/Stdlib_Console.resi index 5c0f4f8d8d..6a885ce4e1 100644 --- a/runtime/Stdlib_Console.resi +++ b/runtime/Stdlib_Console.resi @@ -14,7 +14,7 @@ on MDN. ```rescript Console.assert_(false, "Hello World!") -Console.assert_(42 == 42, "The answer") +Console.assert_(42 === 42, "The answer") ``` */ @val @@ -27,7 +27,7 @@ external assert_: (bool, 'a) => unit = "console.assert" ```rescript Console.assert2(false, "Hello", "World") -Console.assert2(42 == 42, [1, 2, 3], '4') +Console.assert2(42 === 42, [1, 2, 3], '4') ``` */ @val @@ -40,7 +40,7 @@ external assert2: (bool, 'a, 'b) => unit = "console.assert" ```rescript Console.assert3(false, "Hello", "World", "ReScript") -Console.assert3(42 == 42, "One", 2, #3) +Console.assert3(42 === 42, "One", 2, #3) ``` */ @val @@ -54,7 +54,7 @@ external assert3: (bool, 'a, 'b, 'c) => unit = "console.assert" ```rescript let value = 42 Console.assert4(false, "Hello", "World", "ReScript", "!!!") -Console.assert4(value == 42, [1, 2], (3, 4), [#5, #6], #"polyvar") +Console.assert4(value === 42, [1, 2], (3, 4), [#5, #6], #"polyvar") ``` */ @val @@ -68,7 +68,7 @@ external assert4: (bool, 'a, 'b, 'c, 'd) => unit = "console.assert" ```rescript let value = 42 Console.assert5(false, "Hello", "World", "JS", '!', '!') -Console.assert5(value == 42, [1, 2], (3, 4), [#5, #6], #"polyvar", {"name": "ReScript"}) +Console.assert5(value === 42, [1, 2], (3, 4), [#5, #6], #"polyvar", {"name": "ReScript"}) ``` */ @val @@ -82,7 +82,7 @@ external assert5: (bool, 'a, 'b, 'c, 'd, 'e) => unit = "console.assert" ```rescript let value = 42 Console.assert6(false, "Hello", "World", "JS", '!', '!', '?') -Console.assert6(value == 42, [1, 2], (3, 4), [#5, #6], #"polyvar", {"name": "ReScript"}, 42) +Console.assert6(value === 42, [1, 2], (3, 4), [#5, #6], #"polyvar", {"name": "ReScript"}, 42) ``` */ @val @@ -96,7 +96,7 @@ external assert6: (bool, 'a, 'b, 'c, 'd, 'e, 'f) => unit = "console.assert" ```rescript let value = 42 Console.assertMany(false, ["Hello", "World"]) -Console.assertMany(value == 42, [1, 2, 3]) +Console.assertMany(value === 42, [1, 2, 3]) ``` */ @val diff --git a/runtime/Stdlib_String.resi b/runtime/Stdlib_String.resi index c74996dea3..6ae9d1c916 100644 --- a/runtime/Stdlib_String.resi +++ b/runtime/Stdlib_String.resi @@ -891,11 +891,8 @@ See [`String.split`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Ref ## Examples ```rescript -String.splitByRegExpAtMost("Hello World. How are you doing?", %re("/ /"), ~limit=3) == [ - Some("Hello"), - Some("World."), - Some("How"), -] +String.splitByRegExpAtMost("Hello World. How are you doing?", / /, ~limit=3) == + [Some("Hello"), Some("World."), Some("How")] ``` */ @send diff --git a/scripts/format_check.sh b/scripts/format_check.sh index 656f86a732..4415b470c1 100755 --- a/scripts/format_check.sh +++ b/scripts/format_check.sh @@ -21,7 +21,7 @@ case "$(uname -s)" in if ./cli/rescript.js format -check $files; then printf "${successGreen}✅ ReScript code formatting ok.${reset}\n" else - printf "${warningYellow}⚠️ ReScript code formatting issues found.${reset}\n" + printf "${warningYellow}⚠️ ReScript code formatting issues found. Run 'make format' to fix.${reset}\n" exit 1 fi ;; diff --git a/tests/docstring_tests/DocTest.res b/tests/docstring_tests/DocTest.res index 305b2ced66..18fac8a5da 100644 --- a/tests/docstring_tests/DocTest.res +++ b/tests/docstring_tests/DocTest.res @@ -112,6 +112,96 @@ let getCodeBlocks = example => { } } + // Transform lines that contain == patterns to use assertEqual + let transformEqualityAssertions = code => { + let lines = code->String.split("\n") + lines + ->Array.mapWithIndex((line, idx) => { + let trimmedLine = line->String.trim + + // Check if the line contains == and is not inside a comment + if ( + trimmedLine->String.includes("==") && + !(trimmedLine->String.startsWith("//")) && + !(trimmedLine->String.startsWith("/*")) && + // Not an expression line + !(trimmedLine->String.startsWith("if")) && + !(trimmedLine->String.startsWith("|")) && + !(trimmedLine->String.startsWith("let")) && + !(trimmedLine->String.startsWith("~")) && + !(trimmedLine->String.startsWith("module")) && + !(trimmedLine->String.startsWith("->")) && + !(trimmedLine->String.endsWith(",")) + ) { + // Split string at == (not ===) and transform to assertEqual + let parts = { + let rec searchFrom = (currentLine: string, startIndex: int): option<(string, string)> => { + if startIndex >= currentLine->String.length { + // Base case: reached end of string without finding a suitable "==" + None + } else { + let lineSuffix = currentLine->String.sliceToEnd(~start=startIndex) + let idxEqEqInSuffix = lineSuffix->String.indexOfOpt("==") + let idxEqEqEqInSuffix = lineSuffix->String.indexOfOpt("===") + + switch (idxEqEqInSuffix, idxEqEqEqInSuffix) { + | (None, _) => + // No "==" found in the rest of the string. + None + | (Some(iEqEq), None) => + // Found "==" but no "===" in the suffix. + // This "==" must be standalone. + // Calculate its absolute index in the original `currentLine`. + let actualIdx = startIndex + iEqEq + let left = currentLine->String.slice(~start=0, ~end=actualIdx) + let right = currentLine->String.sliceToEnd(~start=actualIdx + 2) + Some((left, right)) + | (Some(iEqEq), Some(iEqEqEq)) => + // Found both "==" and "===" in the suffix. + if iEqEq < iEqEqEq { + // The "==" occurs before "===". This "==" is standalone. + // Example: "a == b === c". In suffix "a == b === c", iEqEq is for "a ==". + let actualIdx = startIndex + iEqEq + let left = currentLine->String.slice(~start=0, ~end=actualIdx) + let right = currentLine->String.sliceToEnd(~start=actualIdx + 2) + Some((left, right)) + } else { + // iEqEq >= iEqEqEq + // This means the first "==" encountered (at iEqEq relative to suffix) + // is part of or comes after the first "===" encountered (at iEqEqEq relative to suffix). + // Example: "a === b". In suffix "a === b", iEqEqEq is for "a ===", iEqEq is also for "a ==". + // We must skip over the "===" found at iEqEqEq and search again. + // The next search should start after this "===". + // The "===" starts at (startIndex + iEqEqEq) in the original line. It has length 3. + searchFrom(currentLine, startIndex + iEqEqEq + 3) + } + } + } + } + searchFrom(line, 0) + } + let parts = switch parts { + | Some((left, right)) if right->String.trim->String.length === 0 => + Some(( + left, + lines + ->Array.get(idx + 1) + ->Option.getExn(~message="Expected to have an expected expression on the next line"), + )) + | _ => parts + } + switch parts { + | Some((left, right)) if !(right->String.includes(")") || right->String.includes("//")) => + `(${left->String.trim})->assertEqual(${right->String.trim})` + | _ => line + } + } else { + line + } + }) + ->Array.join("\n") + } + let rec loop = (lines: list, acc: list) => { switch lines { | list{hd, ...rest} => @@ -120,16 +210,13 @@ let getCodeBlocks = example => { ->String.startsWith("```res") { | true => let code = loopEndCodeBlock(rest, list{}) - loop( - rest, - list{ - code - ->List.reverse - ->List.toArray - ->Array.join("\n"), - ...acc, - }, - ) + let codeString = + code + ->List.reverse + ->List.toArray + ->Array.join("\n") + ->transformEqualityAssertions + loop(rest, list{codeString, ...acc}) | false => loop(rest, acc) } | list{} => acc diff --git a/tests/docstring_tests/DocTest.res.js b/tests/docstring_tests/DocTest.res.js index 7fda54793a..68be8d88fb 100644 --- a/tests/docstring_tests/DocTest.res.js +++ b/tests/docstring_tests/DocTest.res.js @@ -14,6 +14,7 @@ import * as Stdlib_Dict from "rescript/lib/es6/Stdlib_Dict.js"; import * as Stdlib_List from "rescript/lib/es6/Stdlib_List.js"; import * as Stdlib_Array from "rescript/lib/es6/Stdlib_Array.js"; import * as Stdlib_Option from "rescript/lib/es6/Stdlib_Option.js"; +import * as Stdlib_String from "rescript/lib/es6/Stdlib_String.js"; import * as Stdlib_JsError from "rescript/lib/es6/Stdlib_JsError.js"; import * as Primitive_string from "rescript/lib/es6/Primitive_string.js"; import * as Promises from "node:fs/promises"; @@ -179,6 +180,63 @@ function getCodeBlocks(example) { continue; }; }; + let transformEqualityAssertions = code => { + let lines = code.split("\n"); + return lines.map((line, idx) => { + let trimmedLine = line.trim(); + if (!(trimmedLine.includes("==") && !trimmedLine.startsWith("//") && !trimmedLine.startsWith("/*") && !trimmedLine.startsWith("if") && !trimmedLine.startsWith("|") && !trimmedLine.startsWith("let") && !trimmedLine.startsWith("~") && !trimmedLine.startsWith("module") && !trimmedLine.startsWith("->") && !trimmedLine.endsWith(","))) { + return line; + } + let searchFrom = (currentLine, _startIndex) => { + while (true) { + let startIndex = _startIndex; + if (startIndex >= currentLine.length) { + return; + } + let lineSuffix = currentLine.slice(startIndex); + let idxEqEqInSuffix = Stdlib_String.indexOfOpt(lineSuffix, "=="); + let idxEqEqEqInSuffix = Stdlib_String.indexOfOpt(lineSuffix, "==="); + if (idxEqEqInSuffix === undefined) { + return; + } + if (idxEqEqEqInSuffix !== undefined) { + if (idxEqEqInSuffix < idxEqEqEqInSuffix) { + let actualIdx = startIndex + idxEqEqInSuffix | 0; + let left = currentLine.slice(0, actualIdx); + let right = currentLine.slice(actualIdx + 2 | 0); + return [ + left, + right + ]; + } + _startIndex = (startIndex + idxEqEqEqInSuffix | 0) + 3 | 0; + continue; + } + let actualIdx$1 = startIndex + idxEqEqInSuffix | 0; + let left$1 = currentLine.slice(0, actualIdx$1); + let right$1 = currentLine.slice(actualIdx$1 + 2 | 0); + return [ + left$1, + right$1 + ]; + }; + }; + let parts = searchFrom(line, 0); + let parts$1 = parts !== undefined && parts[1].trim().length === 0 ? [ + parts[0], + Stdlib_Option.getExn(lines[idx + 1 | 0], "Expected to have an expected expression on the next line") + ] : parts; + if (parts$1 === undefined) { + return line; + } + let right = parts$1[1]; + if (right.includes(")") || right.includes("//")) { + return line; + } else { + return "(" + parts$1[0].trim() + ")->assertEqual(" + right.trim() + ")"; + } + }).join("\n"); + }; let loop = (_lines, _acc) => { while (true) { let acc = _acc; @@ -189,8 +247,9 @@ function getCodeBlocks(example) { let rest = lines.tl; if (lines.hd.trim().startsWith("```res")) { let code = loopEndCodeBlock(rest, /* [] */0); + let codeString = transformEqualityAssertions(Stdlib_List.toArray(Stdlib_List.reverse(code)).join("\n")); _acc = { - hd: Stdlib_List.toArray(Stdlib_List.reverse(code)).join("\n"), + hd: codeString, tl: acc }; _lines = rest; From a6e75958bde5885aabee1eafb11325a779e29be9 Mon Sep 17 00:00:00 2001 From: Dmitry Zakharov Date: Wed, 28 May 2025 13:05:14 +0400 Subject: [PATCH 2/2] Update completion file --- tests/analysis_tests/tests/src/expected/CompletionJsx.res.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/analysis_tests/tests/src/expected/CompletionJsx.res.txt b/tests/analysis_tests/tests/src/expected/CompletionJsx.res.txt index ed71ec31d9..412e810c27 100644 --- a/tests/analysis_tests/tests/src/expected/CompletionJsx.res.txt +++ b/tests/analysis_tests/tests/src/expected/CompletionJsx.res.txt @@ -802,7 +802,7 @@ Path s "kind": 12, "tags": [], "detail": "(string, RegExp.t, ~limit: int) => array>", - "documentation": {"kind": "markdown", "value": "\n`splitByRegExpAtMost(str, regexp, ~limit)` splits the given `str` at every\noccurrence of `regexp` and returns an array of the first `limit` resulting\nsubstrings. If `limit` is negative or greater than the number of substrings, the\narray will contain all the substrings.\nSee [`String.split`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split) on MDN.\n\n## Examples\n\n```rescript\nString.splitByRegExpAtMost(\"Hello World. How are you doing?\", %re(\"/ /\"), ~limit=3) == [\n Some(\"Hello\"),\n Some(\"World.\"),\n Some(\"How\"),\n]\n```\n"}, + "documentation": {"kind": "markdown", "value": "\n`splitByRegExpAtMost(str, regexp, ~limit)` splits the given `str` at every\noccurrence of `regexp` and returns an array of the first `limit` resulting\nsubstrings. If `limit` is negative or greater than the number of substrings, the\narray will contain all the substrings.\nSee [`String.split`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split) on MDN.\n\n## Examples\n\n```rescript\nString.splitByRegExpAtMost(\"Hello World. How are you doing?\", / /, ~limit=3) ==\n [Some(\"Hello\"), Some(\"World.\"), Some(\"How\")]\n```\n"}, "sortText": "splitByRegExpAtMost", "insertText": "->String.splitByRegExpAtMost", "additionalTextEdits": [{