查找javascript new Function()构造函数抛出的SyntaxError的详细信息

q35jwt9p  于 2023-06-20  发布在  Java
关注(0)|答案(4)|浏览(248)

当使用new Function(params,body)构造函数从JavaScript代码创建新函数时,在body中传递无效字符串会产生SyntaxError。而此异常包含错误消息(即:Unexpected token =),但似乎不包含上下文(即。行/列或发现错误的字符)。
小提琴示例: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-equivalent代码的正当理由。

798qvoo8

798qvoo81#

在基于Chromium的浏览器中,正如您所看到的,在V8解析代码(在实际运行之前)时抛出SyntaxError的东西周围放置try/catch不会产生任何有用的东西;它将描述在堆栈跟踪中 * 导致有问题的脚本的评估 * 的行,但没有关于问题在所述脚本中的位置的细节。
但是,有一个跨浏览器的解决方案。不使用try/catch,您可以将error侦听器添加到window,提供给回调的第一个参数将是ErrorEvent,它具有有用的linenocolno属性:

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之后。因此,您可以让checkSyntaxonly 检查JavaScript最初是否可解析,而不检查其他内容,然后 * 使用try/catch(如果您想真实的运行代码)来捕获运行时错误。您可以通过将throw new Error插入到eval ed文本的顶部来实现此操作。
这里有一个方便的基于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完全分离:

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
     ^

上面写着:
我如何在不使用外部依赖项的情况下,在传入的字符串中精确定位SyntaxError位置?我需要浏览器和nodejs的解决方案。
除非您必须在没有外部库的情况下实现这一点,否则使用库确实是最简单(并且经过测试)的解决方案。如前所述,Acorn(和其他解析器)也可以在Node中工作。

wz3gfoph

wz3gfoph2#

我总结了一些评论和其他研究:

简单回答:目前不可能

目前还没有跨平台的方法从new Function()eval()调用中检索语法错误位置。

部分解

  1. Firefox支持非标准属性error.lineNumbererror.e.columnNumber。如果错误的位置不是关键的,则这可以与特征检测一起使用。
  2. v8的bug报告/功能请求已经完成,可以为chrome/node.js带来对(1)的支持:发布#1281#1914#2589
    1.使用基于JSLintPEG.js的单独javascript解析器。
    1.为作业编写自定义javascript解析器。
    解决方案1和2不完整,依赖于不属于标准的功能。如果这些信息是一种帮助,而不是一种要求,它们可能是合适的。
    解决方案3依赖于外部代码库,这是原始问题明确要求的。如果需要此信息并且较大的代码库是可接受的折衷,则它是合适的。
    解决方案4不切实际。
    Credits:@user3896470,@ivan-kuckir,@aprillion
5cnsuln7

5cnsuln73#

浏览器解决方案:

您可以使用最新的Firefox来获取所需的信息,如错误行号和字符串中的列号。
示例:

var testWithSyntaxError = "{\n\n\n\nvar x=3;\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);               
  }

Firefox控制台中的输出:

undefined
  true
  expected expression, got '='
  SyntaxError
  debugger eval code
  6
  1
  @debugger eval code:4:11

其中6是行号,1是字符串中错误的列号。
在Chrome中无法使用。有关于此问题的chrome浏览器的错误。参见:
https://bugs.chromium.org/p/v8/issues/detail?id=1281
https://bugs.chromium.org/p/v8/issues/detail?id=1914
https://bugs.chromium.org/p/v8/issues/detail?id=2589

i5desfxk

i5desfxk4#

这是一个启发式的近似方法,但是可以对部分代码调用Function()构造函数(只是为了解析,而不是运行),以查看SyntaxError是否仍然存在。
看起来反复从结尾删除线条大多有效。这容易添加新的语法错误-例如可以启动代码块{...并且不完成...}它们-但是通常解析器保持报告最早的错误。只有当您删除最初有问题的行时,错误才会改变/消失。
Function()构造函数的注意事项:它插入function anonymous(arg1, arg2作为线1,
) {作为第2行(参见.toString()成功解析的函数)。
这将使所有行号偏移2!下面的代码补偿了SyntaxError,但对于任何非语法异常,它将影响.stack跟踪中的数字-这些我没有尝试纠正。

// Firefox reports 1-based `lineNumber`, plus Function() prepends 2 lines.
// Measure instead of guessing!
let functionLineOffset = 0
try {
  Function('!@#$')
} catch (err) {
  functionLineOffset = err.lineNumber
}

// Uses `Function` constructor rules: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/Function
//  - `code` should contain a `return` statement if you want a result.
//  - assignments without `var`/`let`/`const` do write to global namespace.
var betterEvalFunction = code => {
  let parsedFunc
  try {
    parsedFunc = new Function(code)
  } catch (err) {
    if (err instanceof SyntaxError) {
      // Goal: find & show responsible line directly in the message
      const {
        name,
        message,
        lineNumber,
        columnNumber
      } = err
      var lines = code.split('\n')

      // Firefox, non-standard
      if (lineNumber !== undefined) {
        const line = lineNumber - functionLineOffset
        throw SyntaxError(
          `${message} (at ${line + 1}:${columnNumber}):\n` +
          `${lines[line] || ''}\n` +
          (columnNumber === undefined ? '' : `${' '.repeat(columnNumber)}^`))
      }

      // Other browsers leave you in the dark :-(
      // ESTIMATE where it happened by slicing lines off the end until message disappears/changes
      // (likely to have unclosed braces etc. — but assuming it reports the first of all errors)
      var lastLine
      for (lastLine = lines.length - 1; lastLine > 0; lastLine--) {
        try {
          new Function(lines.slice(0, lastLine).join('\n')) // only parse, don't call
          console.log('error disappeared')
          break
        } catch (err2) {
          if (err2.message === err.message && err2 instanceof SyntaxError) {
            continue
          }
          console.log('error changed to:', err2.message)
          break
        }
      }
      throw SyntaxError(
        `${err.message} (probably around line ${lastLine + 1}):\n` +
        (lines[lastLine] || ''))
    }
    throw err
  }

  return parsedFunc()
}

///////////////////////////////////////////////////////

var testIt = code => {
  try {
    betterEvalFunction(code)
  } catch (err) {
    console.log(`${err.name}: ${err.message}`)
  }
}

testIt(
` foo
  bar@baz
   .quux()
  another@error`)

testIt(
`{
  foo
  bar
   .quux()
  'opening { is unclosed'
`)

testIt(
` const x = 1
  // error disappears without the 2nd assignment
  const x = 2
`)

testIt(
` return [
    2,
    3,
    // error changes here — no !@# but now the [ is unclosed.
  ] !@#
  !@#
`)

相关问题