当使用 new Function(params,body) 构造函数从 JavaScript 代码创建新函数时,在 body 中传递无效字符串会产生 SyntaxError。虽然此异常包含错误消息(即:Unexpected token =
),但似乎不包含上下文(即发现错误的行/列或字符)。
fiddle 示例:https://jsfiddle.net/gheh1m8p/
var testWithSyntaxError = "{\n\n\n=2;}";
try {
var f=new Function('',testWithSyntaxError);
} catch(e) {
console.log(e instanceof SyntaxError);
console.log(e.message);
console.log(e.name);
console.log(e.fileName);
console.log(e.lineNumber);
console.log(e.columnNumber);
console.log(e.stack);
}
输出:
true
(index):54 Unexpected token =
(index):55 SyntaxError
(index):56 undefined
(index):57 undefined
(index):58 undefined
(index):59 SyntaxError: Unexpected token =
at Function (native)
at window.onload (https://fiddle.jshell.net/_display/:51:8)
我如何在不使用外部依赖项的情况下查明传递字符串中的 SyntaxError 位置? 我需要浏览器和 nodejs 的解决方案。
请注意:我确实有正当理由使用 eval 等效代码。
最佳答案
如您所见,在基于 Chromium 的浏览器中,将 try
/catch
在 V8 解析代码时(在实际运行之前)抛出 SyntaxError 的东西不会产生任何有用的东西;它将描述导致在堆栈跟踪中评估有问题的脚本的行,但没有关于问题在所述脚本中的位置的详细信息。
但是,有一个跨浏览器的解决方法。而不是使用 try
/catch
,您可以添加 error
window
的听众,并且提供给回调的第一个参数将是 ErrorEvent
有用的lineno
和 colno
特性:
window.addEventListener('error', (errorEvent) => {
const { lineno, colno } = errorEvent;
console.log(`Error thrown at: ${lineno}:${colno}`);
// Don't pollute the console with additional info:
errorEvent.preventDefault();
});
const checkSyntax = (str) => {
// Using setTimeout because when an error is thrown without a catch,
// even if the error listener calls preventDefault(),
// the current thread will stop
setTimeout(() => {
eval(str);
});
};
checkSyntax(`console.log('foo') bar baz`);
checkSyntax(`foo bar baz`);
Look in your browser console to see this in action, not in the snippet console
在浏览器控制台中检查结果:
Error thrown at: 1:20
Error thrown at: 1:5
这就是我们想要的!字符 20 对应于
console.log('foo') bar baz
^
并且字符 5 对应于
foo bar baz
^
但是有几个问题:最好在
error
中确定。监听是运行时抛出的错误checkSyntax
.另外,try
/catch
可用于解释器将脚本文本解析为 AST 后的运行时错误(包括语法错误)。所以,你可能有 checkSyntax
只检查 Javascript 最初是可解析的,没有别的,然后使用 try/catch
(如果您想真正运行代码)以捕获运行时错误。您可以通过插入 throw new Error
来执行此操作到文本的顶部 eval
编。这是一个方便的基于 Promise 的函数,可以实现:
// Use an IIFE to keep from polluting the global scope
(async () => {
let stringToEval;
let checkSyntaxResolve;
const cleanup = () => {
stringToEval = null;
checkSyntaxResolve = null; // not necessary, but makes things clearer
};
window.addEventListener('error', (errorEvent) => {
if (!stringToEval) {
// The error was caused by something other than the checkSyntax function below; ignore it
return;
}
const stringToEvalToPrint = stringToEval.split('\n').slice(1).join('\n');
// Don't pollute the console with additional info:
errorEvent.preventDefault();
if (errorEvent.message === 'Uncaught Error: Parsing successful!') {
console.log(`Parsing successful for: ${stringToEvalToPrint}`);
checkSyntaxResolve();
cleanup();
return;
}
const { lineno, colno } = errorEvent;
console.log(`Syntax error thrown at: ${lineno - 1}:${colno}`);
console.log(describeError(stringToEval, lineno, colno));
// checkSyntaxResolve should *always* be defined at this point - checkSyntax's eval was just called (synchronously)
checkSyntaxResolve();
cleanup();
});
const checkSyntax = (str) => {
console.log('----------------------------------------');
return new Promise((resolve) => {
checkSyntaxResolve = resolve;
// Using setTimeout because when an error is thrown without a catch,
// even if the 'error' listener calls preventDefault(),
// the current thread will stop
setTimeout(() => {
// If we only want to check the syntax for initial parsing validity,
// but not run the code for real, throw an error at the top:
stringToEval = `throw new Error('Parsing successful!');\n${str}`;
eval(stringToEval);
});
});
};
const describeError = (stringToEval, lineno, colno) => {
const lines = stringToEval.split('\n');
const line = lines[lineno - 1];
return `${line}\n${' '.repeat(colno - 1) + '^'}`;
};
await checkSyntax(`console.log('I will throw') bar baz`);
await checkSyntax(`foo bar baz will throw too`);
await checkSyntax(`console.log('A snippet without compile errors'); const foo = bar;`);
await checkSyntax(`console.log('A multi line snippet');
With a syntax error on the second line`);
})();
Look in your browser console to see this in action, not in the snippet console
await checkSyntax(`console.log('I will throw') bar baz`);
await checkSyntax(`foo bar baz will throw too`);
await checkSyntax(`console.log('A snippet without compile errors'); const foo = bar;`);
await checkSyntax(`console.log('A multi line snippet');
With a syntax error on the second line`);
结果:
----------------------------------------
Syntax error thrown at: 1:29
console.log('I will throw') bar baz
^
----------------------------------------
Syntax error thrown at: 1:5
foo bar baz will throw too
^
----------------------------------------
Parsing successful for: console.log('A snippet without compile errors'); const foo = bar;
----------------------------------------
Syntax error thrown at: 2:6
With a syntax error on the second line
^
如果在
window
处引发错误的事实是一个问题(例如,如果其他东西已经在监听窗口错误,您不想打扰,并且您不能先附加您的监听器并在事件上调用 stopImmediatePropagation()
),另一种选择是使用而是一个 web worker,它有自己的执行上下文,与原始的 window
完全分离:// Worker:
const getErrorEvent = (() => {
const workerFn = () => {
const doEvalAndReply = (jsText) => {
self.addEventListener(
'error',
(errorEvent) => {
// Don't pollute the browser console:
errorEvent.preventDefault();
// The properties we want are actually getters on the prototype;
// they won't be retrieved when just stringifying
// so, extract them manually, and put them into a new object:
const { lineno, colno, message } = errorEvent;
const plainErrorEventObj = { lineno, colno, message };
self.postMessage(JSON.stringify(plainErrorEventObj));
},
{ once: true }
);
eval(jsText);
};
self.addEventListener('message', (e) => {
doEvalAndReply(e.data);
});
};
const blob = new Blob(
[ `(${workerFn})();`],
{ type: "text/javascript" }
);
const worker = new Worker(window.URL.createObjectURL(blob));
// Use a queue to ensure processNext only calls the worker once the worker is idle
const processingQueue = [];
let processing = false;
const processNext = () => {
processing = true;
const { resolve, jsText } = processingQueue.shift();
worker.addEventListener(
'message',
({ data }) => {
resolve(JSON.parse(data));
if (processingQueue.length) {
processNext();
} else {
processing = false;
}
},
{ once: true }
);
worker.postMessage(jsText);
};
return (jsText) => new Promise((resolve) => {
processingQueue.push({ resolve, jsText });
if (!processing) {
processNext();
}
});
})();
// Calls worker:
(async () => {
const checkSyntax = async (str) => {
console.log('----------------------------------------');
const stringToEval = `throw new Error('Parsing successful!');\n${str}`;
const { lineno, colno, message } = await getErrorEvent(stringToEval);
if (message === 'Uncaught Error: Parsing successful!') {
console.log(`Parsing successful for: ${str}`);
return;
}
console.log(`Syntax error thrown at: ${lineno - 1}:${colno}`);
console.log(describeError(stringToEval, lineno, colno));
};
const describeError = (stringToEval, lineno, colno) => {
const lines = stringToEval.split('\n');
const line = lines[lineno - 1];
return `${line}\n${' '.repeat(colno - 1) + '^'}`;
};
await checkSyntax(`console.log('I will throw') bar baz`);
await checkSyntax(`foo bar baz will throw too`);
await checkSyntax(`console.log('A snippet without compile errors'); const foo = bar;`);
await checkSyntax(`console.log('A multi line snippet');
With a syntax error on the second line`);
})();
Look in your browser console to see this in action, not in the snippet console
本质上,什么
checkSyntax
正在检查是否可以将提供的代码解析为 Abstract Syntax Tree由当前的解释器。您还可以使用 @babel/parser 之类的软件包或 acorn尝试解析字符串,尽管您必须将其配置为当前环境中允许的语法(这将随着新语法添加到语言中而改变)。const checkSyntax = (str) => {
try {
acorn.Parser.parse(str);
console.log('Parsing successful');
} catch(e){
console.error(e.message);
}
};
checkSyntax(`console.log('I will throw') bar baz`);
checkSyntax(`foo bar baz will throw too`);
checkSyntax(`console.log('A snippet without compile errors'); const foo = bar;`);
checkSyntax(`console.log('A multi line snippet');
With a syntax error on the second line`);
<script src="https://cdn.jsdelivr.net/npm/acorn@6.1.1/dist/acorn.min.js"></script>
以上适用于浏览器。在 Node 中,情况有所不同:监听 uncaughtException不能用来拦截语法错误的细节,AFAIK。但是,您可以使用 vm尝试编译代码的模块,如果它在运行之前抛出一个 SyntaxError,你会看到类似这样的东西。运行
console.log('I will throw') bar baz
导致一堆
evalmachine.<anonymous>:1
console.log('I will throw') bar baz
^^^
SyntaxError: Unexpected identifier
at createScript (vm.js:80:10)
at Object.runInNewContext (vm.js:135:10)
<etc>
因此,只需查看堆栈中的第一项即可获取行号,以及
^
之前的空格数。获取列号。使用与之前类似的技术,如果解析成功,则在第一行抛出错误:const vm = require('vm');
const checkSyntax = (code) => {
console.log('---------------------------');
try {
vm.runInNewContext(`throw new Error();\n${code}`);
}
catch (e) {
describeError(e.stack);
}
};
const describeError = (stack) => {
const match = stack
.match(/^\D+(\d+)\n(.+\n( *)\^+)\n\n(SyntaxError.+)/);
if (!match) {
console.log('Parse successful!');
return;
}
const [, linenoPlusOne, caretString, colSpaces, message] = match;
const lineno = linenoPlusOne - 1;
const colno = colSpaces.length + 1;
console.log(`${lineno}:${colno}: ${message}\n${caretString}`);
};
checkSyntax(`console.log('I will throw') bar baz`);
checkSyntax(`foo bar baz will throw too`);
checkSyntax(`console.log('A snippet without compile errors'); const foo = bar;`);
checkSyntax(`console.log('A multi line snippet');
With a syntax error on the second line`);
结果:
---------------------------
1:29: SyntaxError: Unexpected identifier
console.log('I will throw') bar baz
^^^
---------------------------
1:5: SyntaxError: Unexpected identifier
foo bar baz will throw too
^^^
---------------------------
Parse successful!
---------------------------
2:6: SyntaxError: Unexpected identifier
With a syntax error on the second line
^
那说:
How can I, without using external dependencies, pinpoint SyntaxError location withinn passed string? I require solution both for browser and nodejs.
除非您必须在没有外部库的情况下实现这一点,否则使用库确实是最简单(并且经过验证)的解决方案。如前所述,Acorn(和其他解析器)也可以在 Node 中工作。
关于javascript - 查找 javascript new Function() 构造函数抛出的 SyntaxError 的详细信息,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/35252731/