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

When creating new function from JavaScript code using new Function(params,body) constructor, passing invalid string in body yelds SyntaxError. While this exception contains error message (ie: Unexpected token =), but does not seem to contain context (ie. line/column or character where error was found).

Example 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的解决方案。

请注意:我确实有理由使用等效代码。

评论
  • ofbgg
    ofbgg 回复

    浏览器解决方案:

    您可以使用最新的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

  • 时光逆转
    时光逆转 回复

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

    简单的答案:目前不可能

    There is currently no cross-platform way to retrive syntax error position from new Function() or eval() call.

    部分解决方案

    1. Firefox support non-standard properties error.lineNumber and error.e.columnNumber. This can be used with feature detection if position of error is not critical.
    2. There are filled bug reports/feature request for v8 that could bring support of (1) to chrome/node.js: Issue #1281, #1914, #2589
    3. Use separate javascript parser, based on JSLint or PEG.js.
    4. Write custom javascript parser for the job.

    解决方案1和2不完整,依赖于不属于标准的功能。如果此信息是帮助而不是要求,那么它们可能是合适的。

    解决方案3取决于外部代码库,这是原始问题明确要求的。如果需要此信息,并且可以接受较大的代码库,则比较合适。

    解决方案4是不切实际的。

    鸣谢:@ user3896470,@ ivan-kuckir,@ aprillion

  • sit_in
    sit_in 回复

    In Chromium-based browsers, as you've seen, putting try/catch around something that throws a SyntaxError while V8 is parsing the code (before actually running it) won't produce anything helpful; it will describe the line that caused the evaluation of the problematic script in the stack trace, but no details on where the problem was in said script.

    But, there's a cross-browser workaround. Instead of using try/catch, you can add an error listener to window, and the first argument provided to the callback will be an ErrorEvent which has useful lineno and colno properties:

    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
        ^
    

    There are a couple issues, though: it would be good to make sure in the error listened for is an error thrown when running checkSyntax. Also, try/catch can be used for runtime errors (including syntax errors) after the script text has been parsed into an AST by the interpreter. So, you might have checkSyntax only check that the Javascript is initially parsable, and nothing else, and then use try/catch (if you want to run the code for real) to catch runtime errors. You can do this by inserting throw new Error to the top of the text that's evaled.

    这是一个方便的基于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
         ^
    

    If the fact that an error is thrown at window is a problem (for example, if something else is already listening for window errors, which you don't want to disturb, and you can't attach your listener first and call stopImmediatePropagation() on the event), another option is to use a web worker instead, which has its own execution context completely separate from the original 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

    Essentially, what checkSyntax is doing is checking to see if the code provided can be parsed into an Abstract Syntax Tree by the current interpreter. You can also use packages like @babel/parser or acorn to attempt to parse the string, though you'll have to configure it for the syntax permitted in the current environment (which will change as new syntax gets added to the language).

    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>

    The above works for browsers. In Node, the situation is different: listening for an uncaughtException can't be used to intercept the details of syntax errors, AFAIK. However, you can use vm module to attempt to compile the code, and if it throws a SyntaxError before running, you'll see something like this. Running

    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>
    

    So, just look at the first item in the stack to get the line number, and at the number of spaces before the ^ to get the column number. Using a similar technique as earlier, throw an error on the first line if parsing is successful:

    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的解决方案。

    除非您必须在没有外部库的情况下实现此目标,否则使用库确实是最简单(且经过反复测试)的解决方案。如前面所示(和其他解析器),橡子也可以在Node中工作。