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

标签 javascript

当使用 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有用的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 后的运行时错误(包括语法错误)。所以,你可能有 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/

相关文章:

javascript - Angular Radio ng-change 仅在第一次时有效

javascript - 需要了解Javascript函数提升示例

javascript - Node.js 行被跳过然后被处理

javascript - typescript 中的 Object.freeze/Object.seal

javascript - 如何在元胞自动机中对细胞进行聚类?

javascript - 制作 "Mark all"按钮

javascript - 延迟提交直到函数完成

javascript - 单击链接标题时显示空白

javascript - PHP AJAX session 变量不起作用

javascript - Node-mysql pool.query 在 10 分钟查询后断开连接