X Tutup
Skip to content

Commit c9fdd5c

Browse files
authored
Better require search (#1379)
* Manual regex to find requires while skipping strings and comments * Unit tests for find-lua-requires * Resolve require-finding and rewriting without regex * Add support for requires without parentheses * fix stray tests * Fix prettier
1 parent 893791b commit c9fdd5c

File tree

4 files changed

+357
-38
lines changed

4 files changed

+357
-38
lines changed
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
export interface LuaRequire {
2+
from: number;
3+
to: number;
4+
requirePath: string;
5+
}
6+
7+
export function findLuaRequires(lua: string): LuaRequire[] {
8+
return findRequire(lua, 0);
9+
}
10+
11+
function findRequire(lua: string, offset: number): LuaRequire[] {
12+
const result = [];
13+
14+
while (offset < lua.length) {
15+
const c = lua[offset];
16+
if (
17+
c === "r" &&
18+
(offset === 0 ||
19+
isWhitespace(lua[offset - 1]) ||
20+
lua[offset - 1] === "]" ||
21+
lua[offset - 1] === "(" ||
22+
lua[offset - 1] === "[")
23+
) {
24+
const m = matchRequire(lua, offset);
25+
if (m.matched) {
26+
offset = m.match.to + 1;
27+
result.push(m.match);
28+
} else {
29+
offset = m.end;
30+
}
31+
} else if (c === '"' || c === "'") {
32+
offset = readString(lua, offset, c).offset; // Skip string and surrounding quotes
33+
} else if (c === "-" && offset + 1 < lua.length && lua[offset + 1] === "-") {
34+
offset = skipComment(lua, offset);
35+
} else {
36+
offset++;
37+
}
38+
}
39+
40+
return result;
41+
}
42+
43+
type MatchResult<T> = { matched: true; match: T } | { matched: false; end: number };
44+
45+
function matchRequire(lua: string, offset: number): MatchResult<LuaRequire> {
46+
const start = offset;
47+
for (const c of "require") {
48+
if (offset > lua.length) {
49+
return { matched: false, end: offset };
50+
}
51+
52+
if (lua[offset] !== c) {
53+
return { matched: false, end: offset };
54+
}
55+
offset++;
56+
}
57+
58+
offset = skipWhitespace(lua, offset);
59+
60+
let hasParentheses = false;
61+
62+
if (offset > lua.length) {
63+
return { matched: false, end: offset };
64+
} else {
65+
if (lua[offset] === "(") {
66+
hasParentheses = true;
67+
offset++;
68+
offset = skipWhitespace(lua, offset);
69+
} else if (lua[offset] === '"' || lua[offset] === "'") {
70+
// require without parentheses
71+
} else {
72+
// otherwise fail match
73+
return { matched: false, end: offset };
74+
}
75+
}
76+
77+
if (offset > lua.length || (lua[offset] !== '"' && lua[offset] !== "'")) {
78+
return { matched: false, end: offset };
79+
}
80+
81+
const { value: requireString, offset: offsetAfterString } = readString(lua, offset, lua[offset]);
82+
offset = offsetAfterString; // Skip string and surrounding quotes
83+
84+
if (hasParentheses) {
85+
offset = skipWhitespace(lua, offset);
86+
87+
if (offset > lua.length || lua[offset] !== ")") {
88+
return { matched: false, end: offset };
89+
}
90+
91+
offset++;
92+
}
93+
94+
return { matched: true, match: { from: start, to: offset - 1, requirePath: requireString } };
95+
}
96+
97+
function readString(lua: string, offset: number, delimiter: string): { value: string; offset: number } {
98+
expect(lua, offset, delimiter);
99+
offset++;
100+
101+
let start = offset;
102+
let result = "";
103+
104+
let escaped = false;
105+
while (offset < lua.length && (lua[offset] !== delimiter || escaped)) {
106+
if (lua[offset] === "\\" && !escaped) {
107+
escaped = true;
108+
} else {
109+
if (lua[offset] === delimiter) {
110+
result += lua.slice(start, offset - 1);
111+
start = offset;
112+
}
113+
escaped = false;
114+
}
115+
116+
offset++;
117+
}
118+
119+
if (offset < lua.length) {
120+
expect(lua, offset, delimiter);
121+
}
122+
123+
result += lua.slice(start, offset);
124+
return { value: result, offset: offset + 1 };
125+
}
126+
127+
function skipWhitespace(lua: string, offset: number): number {
128+
while (offset < lua.length && isWhitespace(lua[offset])) {
129+
offset++;
130+
}
131+
return offset;
132+
}
133+
134+
function isWhitespace(c: string): boolean {
135+
return c === " " || c === "\t" || c === "\r" || c === "\n";
136+
}
137+
138+
function skipComment(lua: string, offset: number): number {
139+
expect(lua, offset, "-");
140+
expect(lua, offset + 1, "-");
141+
offset += 2;
142+
143+
if (offset + 1 < lua.length && lua[offset] === "[" && lua[offset + 1] === "[") {
144+
return skipMultiLineComment(lua, offset);
145+
} else {
146+
return skipSingleLineComment(lua, offset);
147+
}
148+
}
149+
150+
function skipMultiLineComment(lua: string, offset: number): number {
151+
while (offset < lua.length && !(lua[offset] === "]" && lua[offset - 1] === "]")) {
152+
offset++;
153+
}
154+
return offset + 1;
155+
}
156+
157+
function skipSingleLineComment(lua: string, offset: number): number {
158+
while (offset < lua.length && lua[offset] !== "\n") {
159+
offset++;
160+
}
161+
return offset + 1;
162+
}
163+
164+
function expect(lua: string, offset: number, char: string) {
165+
if (lua[offset] !== char) {
166+
throw new Error(`Expected ${char} at position ${offset} but found ${lua[offset]}`);
167+
}
168+
}

src/transpilation/resolve.ts

Lines changed: 29 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { getEmitPathRelativeToOutDir, getProjectRoot, getSourceDir } from "./tra
88
import { formatPathToLuaPath, normalizeSlashes, trimExtension } from "../utils";
99
import { couldNotReadDependency, couldNotResolveRequire } from "./diagnostics";
1010
import { BuildMode, CompilerOptions } from "../CompilerOptions";
11+
import { findLuaRequires, LuaRequire } from "./find-lua-requires";
1112

1213
const resolver = resolve.ResolverFactory.createResolver({
1314
extensions: [".lua"],
@@ -40,12 +41,13 @@ class ResolutionContext {
4041
if (this.resolvedFiles.has(file.fileName)) return;
4142
this.resolvedFiles.set(file.fileName, file);
4243

43-
for (const required of findRequiredPaths(file.code)) {
44+
// Do this backwards so the replacements do not mess with the positions of the previous requires
45+
for (const required of findLuaRequires(file.code).reverse()) {
4446
// Do not resolve noResolution paths
45-
if (required.startsWith("@NoResolution:")) {
47+
if (required.requirePath.startsWith("@NoResolution:")) {
4648
// Remove @NoResolution prefix if not building in library mode
4749
if (!isBuildModeLibrary(this.program)) {
48-
const path = required.replace("@NoResolution:", "");
50+
const path = required.requirePath.replace("@NoResolution:", "");
4951
replaceRequireInCode(file, required, path);
5052
replaceRequireInSourceMap(file, required, path);
5153
}
@@ -58,25 +60,27 @@ class ResolutionContext {
5860
}
5961
}
6062

61-
public resolveImport(file: ProcessedFile, required: string): void {
63+
public resolveImport(file: ProcessedFile, required: LuaRequire): void {
6264
// Do no resolve lualib - always use the lualib of the application entry point, not the lualib from external packages
63-
if (required === "lualib_bundle") {
65+
if (required.requirePath === "lualib_bundle") {
6466
this.resolvedFiles.set("lualib_bundle", { fileName: "lualib_bundle", code: "" });
6567
return;
6668
}
6769

68-
if (this.noResolvePaths.has(required)) {
70+
if (this.noResolvePaths.has(required.requirePath)) {
6971
if (this.options.tstlVerbose) {
70-
console.log(`Skipping module resolution of ${required} as it is in the tsconfig noResolvePaths.`);
72+
console.log(
73+
`Skipping module resolution of ${required.requirePath} as it is in the tsconfig noResolvePaths.`
74+
);
7175
}
7276
return;
7377
}
7478

75-
const dependencyPath = this.resolveDependencyPath(file, required);
79+
const dependencyPath = this.resolveDependencyPath(file, required.requirePath);
7680
if (!dependencyPath) return this.couldNotResolveImport(required, file);
7781

7882
if (this.options.tstlVerbose) {
79-
console.log(`Resolved ${required} to ${normalizeSlashes(dependencyPath)}`);
83+
console.log(`Resolved ${required.requirePath} to ${normalizeSlashes(dependencyPath)}`);
8084
}
8185

8286
this.processDependency(dependencyPath);
@@ -110,13 +114,13 @@ class ResolutionContext {
110114
this.addAndResolveDependencies(dependency);
111115
}
112116

113-
private couldNotResolveImport(required: string, file: ProcessedFile): void {
117+
private couldNotResolveImport(required: LuaRequire, file: ProcessedFile): void {
114118
const fallbackRequire = fallbackResolve(required, getSourceDir(this.program), path.dirname(file.fileName));
115119
replaceRequireInCode(file, required, fallbackRequire);
116120
replaceRequireInSourceMap(file, required, fallbackRequire);
117121

118122
this.diagnostics.push(
119-
couldNotResolveRequire(required, path.relative(getProjectRoot(this.program), file.fileName))
123+
couldNotResolveRequire(required.requirePath, path.relative(getProjectRoot(this.program), file.fileName))
120124
);
121125
}
122126

@@ -305,35 +309,23 @@ function isBuildModeLibrary(program: ts.Program) {
305309
return program.getCompilerOptions().buildMode === BuildMode.Library;
306310
}
307311

308-
function findRequiredPaths(code: string): string[] {
309-
// Find all require("<path>") paths in a lua code string
310-
const paths: string[] = [];
311-
const pattern = /(^|\s|;|=|\()require\s*\(?(["|'])(.+?)\2\)?/g;
312-
// eslint-disable-next-line @typescript-eslint/ban-types
313-
let match: RegExpExecArray | null;
314-
while ((match = pattern.exec(code))) {
315-
paths.push(match[3]);
316-
}
317-
318-
return paths;
319-
}
320-
321-
function replaceRequireInCode(file: ProcessedFile, originalRequire: string, newRequire: string): void {
312+
function replaceRequireInCode(file: ProcessedFile, originalRequire: LuaRequire, newRequire: string): void {
322313
const requirePath = formatPathToLuaPath(newRequire.replace(".lua", ""));
323-
324-
// Escape special characters to prevent the regex from breaking...
325-
const escapedRequire = originalRequire.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
326-
327-
file.code = file.code.replace(
328-
new RegExp(`(^|\\s|;|=|\\()require\\s*\\(?["|']${escapedRequire}["|']\\)?`),
329-
`$1require("${requirePath}")`
330-
);
314+
file.code = file.code =
315+
file.code.substring(0, originalRequire.from) +
316+
`require("${requirePath}")` +
317+
file.code.substring(originalRequire.to + 1);
331318
}
332319

333-
function replaceRequireInSourceMap(file: ProcessedFile, originalRequire: string, newRequire: string): void {
320+
function replaceRequireInSourceMap(file: ProcessedFile, originalRequire: LuaRequire, newRequire: string): void {
334321
const requirePath = formatPathToLuaPath(newRequire.replace(".lua", ""));
335322
if (file.sourceMapNode) {
336-
replaceInSourceMap(file.sourceMapNode, file.sourceMapNode, `"${originalRequire}"`, `"${requirePath}"`);
323+
replaceInSourceMap(
324+
file.sourceMapNode,
325+
file.sourceMapNode,
326+
`"${originalRequire.requirePath}"`,
327+
`"${requirePath}"`
328+
);
337329
}
338330
}
339331

@@ -375,10 +367,10 @@ function hasSourceFileInProject(filePath: string, program: ts.Program) {
375367
}
376368

377369
// Transform an import path to a lua require that is probably not correct, but can be used as fallback when regular resolution fails
378-
function fallbackResolve(required: string, sourceRootDir: string, fileDir: string): string {
370+
function fallbackResolve(required: LuaRequire, sourceRootDir: string, fileDir: string): string {
379371
return formatPathToLuaPath(
380372
path
381-
.normalize(path.join(path.relative(sourceRootDir, fileDir), required))
373+
.normalize(path.join(path.relative(sourceRootDir, fileDir), required.requirePath))
382374
.split(path.sep)
383375
.filter(s => s !== "." && s !== "..")
384376
.join(path.sep)

test/transpile/module-resolution.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,7 @@ test("module resolution should not rewrite @NoResolution requires in library mod
411411
});
412412

413413
// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1050
414-
test("module resolution should not try to resolve resolve-like functions", () => {
414+
test("module resolution should not try to resolve require-like functions", () => {
415415
util.testModule`
416416
function custom_require(this: void, value: string) {
417417
return value;

0 commit comments

Comments
 (0)
X Tutup