Skip to content

Commit 8b82232

Browse files
authored
Add string.replaceAll lualib and optimize/fix string.replace (#1129)
* Optimize StringReplace * Add `string.replaceAll` lualib function This makes Node v15 (for ES2021 support) minimum required to run tests * Add tests for string.replace(All) function call arguments * Use node 16.9.0 for tests (for es2021 support) * Use node 15.14.0 for tests (for es2021 support) Node 16 with "vm" currently has a bug which prevents some js tests from compiling at all, somehow * Changes for PR
1 parent 3a3465b commit 8b82232

File tree

6 files changed

+92
-33
lines changed

6 files changed

+92
-33
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@ jobs:
2727

2828
steps:
2929
- uses: actions/checkout@v2
30-
- name: Use Node.js 12.13.1
30+
- name: Use Node.js 15.14.0
3131
uses: actions/setup-node@v1
3232
with:
33-
node-version: 12.13.1
33+
node-version: 15.14.0
3434
- run: npm ci
3535
- run: npm run build
3636
- run: npx jest --maxWorkers 2 --coverage

src/LuaLib.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export enum LuaLibFeature {
8383
StringPadEnd = "StringPadEnd",
8484
StringPadStart = "StringPadStart",
8585
StringReplace = "StringReplace",
86+
StringReplaceAll = "StringReplaceAll",
8687
StringSlice = "StringSlice",
8788
StringSplit = "StringSplit",
8889
StringStartsWith = "StringStartsWith",

src/lualib/StringReplace.ts

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,16 @@ function __TS__StringReplace(
22
this: void,
33
source: string,
44
searchValue: string,
5-
replaceValue: string | ((substring: string) => string)
5+
replaceValue: string | ((match: string, offset: number, string: string) => string)
66
): string {
7-
[searchValue] = string.gsub(searchValue, "[%%%(%)%.%+%-%*%?%[%^%$]", "%%%1");
8-
9-
if (typeof replaceValue === "string") {
10-
[replaceValue] = string.gsub(replaceValue, "%%", "%%%%");
11-
const [result] = string.gsub(source, searchValue, replaceValue, 1);
12-
return result;
13-
} else {
14-
const [result] = string.gsub(
15-
source,
16-
searchValue,
17-
match => (replaceValue as (substring: string) => string)(match),
18-
1
19-
);
20-
return result;
7+
const [startPos, endPos] = string.find(source, searchValue, undefined, true);
8+
if (!startPos) {
9+
return source;
2110
}
11+
const sub = string.sub;
12+
const before = sub(source, 1, startPos - 1);
13+
const replacement =
14+
typeof replaceValue === "string" ? replaceValue : replaceValue(searchValue, startPos - 1, source);
15+
const after = sub(source, endPos + 1);
16+
return before + replacement + after;
2217
}

src/lualib/StringReplaceAll.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
function __TS__StringReplaceAll(
2+
this: void,
3+
source: string,
4+
searchValue: string,
5+
replaceValue: string | ((match: string, offset: number, string: string) => string)
6+
): string {
7+
let replacer: (match: string, offset: number, string: string) => string;
8+
if (typeof replaceValue === "string") {
9+
replacer = () => replaceValue;
10+
} else {
11+
replacer = replaceValue;
12+
}
13+
const parts: string[] = [];
14+
let partsIndex = 1;
15+
16+
const sub = string.sub;
17+
if (searchValue.length === 0) {
18+
parts[0] = replacer("", 0, source);
19+
partsIndex = 2;
20+
for (const i of $range(1, source.length)) {
21+
parts[partsIndex - 1] = sub(source, i, i);
22+
parts[partsIndex] = replacer("", i, source);
23+
partsIndex += 2;
24+
}
25+
} else {
26+
const find = string.find;
27+
let currentPos = 1;
28+
while (true) {
29+
const [startPos, endPos] = find(source, searchValue, currentPos, true);
30+
if (!startPos) break;
31+
parts[partsIndex - 1] = sub(source, currentPos, startPos - 1);
32+
parts[partsIndex] = replacer(searchValue, startPos - 1, source);
33+
partsIndex += 2;
34+
35+
currentPos = endPos + 1;
36+
}
37+
parts[partsIndex - 1] = sub(source, currentPos);
38+
}
39+
return table.concat(parts);
40+
}

src/transformation/builtins/string.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export function transformStringPrototypeCall(
2828
switch (expressionName) {
2929
case "replace":
3030
return transformLuaLibFunction(context, LuaLibFeature.StringReplace, node, caller, ...params);
31+
case "replaceAll":
32+
return transformLuaLibFunction(context, LuaLibFeature.StringReplaceAll, node, caller, ...params);
3133
case "concat":
3234
return transformLuaLibFunction(context, LuaLibFeature.StringConcat, node, caller, ...params);
3335

test/unit/builtins/string.spec.ts

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -54,22 +54,43 @@ test("string index (side effect)", () => {
5454
`.expectToMatchJsResult();
5555
});
5656

57-
test.each([
58-
{ inp: "hello test", searchValue: "", replaceValue: "" },
59-
{ inp: "hello test", searchValue: " ", replaceValue: "" },
60-
{ inp: "hello test", searchValue: "hello", replaceValue: "" },
61-
{ inp: "hello test", searchValue: "test", replaceValue: "" },
62-
{ inp: "hello test", searchValue: "test", replaceValue: "world" },
63-
{ inp: "hello test", searchValue: "test", replaceValue: "%world" },
64-
{ inp: "hello test", searchValue: "test", replaceValue: "." },
65-
{ inp: "hello %test", searchValue: "test", replaceValue: "world" },
66-
{ inp: "hello %test", searchValue: "%test", replaceValue: "world" },
67-
{ inp: "hello test.", searchValue: ".", replaceValue: "$" },
68-
{ inp: "hello test", searchValue: "test", replaceValue: () => "a" },
69-
{ inp: "hello test", searchValue: "test", replaceValue: () => "%a" },
70-
{ inp: "aaa", searchValue: "a", replaceValue: "b" },
71-
])("string.replace (%p)", ({ inp, searchValue, replaceValue }) => {
72-
util.testExpression`"${inp}".replace(${util.formatCode(searchValue, replaceValue)})`.expectToMatchJsResult();
57+
describe.each(["replace", "replaceAll"])("string.%s", method => {
58+
const testCases = [
59+
{ inp: "hello test", searchValue: "", replaceValue: "" },
60+
{ inp: "hello test", searchValue: "", replaceValue: "_" },
61+
{ inp: "hello test", searchValue: " ", replaceValue: "" },
62+
{ inp: "hello test", searchValue: "hello", replaceValue: "" },
63+
{ inp: "hello test", searchValue: "test", replaceValue: "" },
64+
{ inp: "hello test", searchValue: "test", replaceValue: "world" },
65+
{ inp: "hello test", searchValue: "test", replaceValue: "%world" },
66+
{ inp: "hello test", searchValue: "test", replaceValue: "." },
67+
{ inp: "hello %test", searchValue: "test", replaceValue: "world" },
68+
{ inp: "hello %test", searchValue: "%test", replaceValue: "world" },
69+
{ inp: "hello test.", searchValue: ".", replaceValue: "$" },
70+
{ inp: "aaa", searchValue: "a", replaceValue: "b" },
71+
];
72+
73+
test.each(testCases)("string replacer %p", ({ inp, searchValue, replaceValue }) => {
74+
util.testExpression`"${inp}${inp}".${method}(${util.formatCode(
75+
searchValue,
76+
replaceValue
77+
)})`.expectToMatchJsResult();
78+
});
79+
80+
test.each(testCases)("function replacer %p", ({ inp, searchValue, replaceValue }) => {
81+
util.testFunction`
82+
const result = {
83+
args: [],
84+
string: ""
85+
}
86+
function replacer(...args: any[]): string {
87+
result.args.push(...args)
88+
return ${util.formatCode(replaceValue)}
89+
}
90+
result.string = "${inp}${inp}".${method}(${util.formatCode(searchValue)}, replacer)
91+
return result
92+
`.expectToMatchJsResult();
93+
});
7394
});
7495

7596
test.each([

0 commit comments

Comments
 (0)