草庐IT

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

coder 2025-04-02 原文

当使用 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 - 查找 javascript new Function() 构造函数抛出的 SyntaxError 的详细信息的更多相关文章

  1. ruby-on-rails - Rails 常用字符串(用于通知和错误信息等) - 2

    大约一年前,我决定确保每个包含非唯一文本的Flash通知都将从模块中的方法中获取文本。我这样做的最初原因是为了避免一遍又一遍地输入相同的字符串。如果我想更改措辞,我可以在一个地方轻松完成,而且一遍又一遍地重复同一件事而出现拼写错误的可能性也会降低。我最终得到的是这样的:moduleMessagesdefformat_error_messages(errors)errors.map{|attribute,message|"Error:#{attribute.to_s.titleize}#{message}."}enddeferror_message_could_not_find(obje

  2. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  3. ruby - 在没有 sass 引擎的情况下使用 sass 颜色函数 - 2

    我想在一个没有Sass引擎的类中使用Sass颜色函数。我已经在项目中使用了sassgem,所以我认为搭载会像以下一样简单:classRectangleincludeSass::Script::FunctionsdefcolorSass::Script::Color.new([0x82,0x39,0x06])enddefrender#hamlengineexecutedwithcontextofself#sothatwithintemlateicouldcall#%stop{offset:'0%',stop:{color:lighten(color)}}endend更新:参见上面的#re

  4. ruby-on-rails - 在 ruby​​ 中使用 gsub 函数替换单词 - 2

    我正在尝试用ruby​​中的gsub函数替换字符串中的某些单词,但有时效果很好,在某些情况下会出现此错误?这种格式有什么问题吗NoMethodError(undefinedmethod`gsub!'fornil:NilClass):模型.rbclassTest"replacethisID1",WAY=>"replacethisID2andID3",DELTA=>"replacethisID4"}end另一个模型.rbclassCheck 最佳答案 啊,我找到了!gsub!是一个非常奇怪的方法。首先,它替换了字符串,所以它实际上修改了

  5. ruby - 在 Ruby 中有条件地定义函数 - 2

    我有一些代码在几个不同的位置之一运行:作为具有调试输出的命令行工具,作为不接受任何输出的更大程序的一部分,以及在Rails环境中。有时我需要根据代码的位置对代码进行细微的更改,我意识到以下样式似乎可行:print"Testingnestedfunctionsdefined\n"CLI=trueifCLIdeftest_printprint"CommandLineVersion\n"endelsedeftest_printprint"ReleaseVersion\n"endendtest_print()这导致:TestingnestedfunctionsdefinedCommandLin

  6. ruby - 当使用::指定模块时,为什么 Ruby 不在更高范围内查找类? - 2

    我刚刚被困在这个问题上一段时间了。以这个基地为例:moduleTopclassTestendmoduleFooendend稍后,我可以通过这样做在Foo中定义扩展Test的类:moduleTopmoduleFooclassSomeTest但是,如果我尝试通过使用::指定模块来最小化缩进:moduleTop::FooclassFailure这失败了:NameError:uninitializedconstantTop::Foo::Test这是一个错误,还是仅仅是Ruby解析变量名的方式的逻辑结果? 最佳答案 Isthisabug,or

  7. ruby - 查找字符串中的内容类型(数字、日期、时间、字符串等) - 2

    我正在尝试解析一个CSV文件并使用SQL命令自动为其创建一个表。CSV中的第一行给出了列标题。但我需要推断每个列的类型。Ruby中是否有任何函数可以找到每个字段中内容的类型。例如,CSV行:"12012","Test","1233.22","12:21:22","10/10/2009"应该产生像这样的类型['integer','string','float','time','date']谢谢! 最佳答案 require'time'defto_something(str)if(num=Integer(str)rescueFloat(s

  8. ruby - 在 Ruby 中重新分配常量时抛出异常? - 2

    我早就知道Ruby中的“常量”(即大写的变量名)不是真正常量。与其他编程语言一样,对对象的引用是唯一存储在变量/常量中的东西。(侧边栏:Ruby确实具有“卡住”引用对象不被修改的功能,据我所知,许多其他语言都没有提供这种功能。)所以这是我的问题:当您将一个值重新分配给常量时,您会收到如下警告:>>FOO='bar'=>"bar">>FOO='baz'(irb):2:warning:alreadyinitializedconstantFOO=>"baz"有没有办法强制Ruby抛出异常而不是打印警告?很难弄清楚为什么有时会发生重新分配。 最佳答案

  9. ruby - 在 Ruby 中按名称传递函数 - 2

    如何在Ruby中按名称传递函数?(我使用Ruby才几个小时,所以我还在想办法。)nums=[1,2,3,4]#Thisworks,butismoreverbosethanI'dlikenums.eachdo|i|putsiend#InJS,Icouldjustdosomethinglike:#nums.forEach(console.log)#InF#,itwouldbesomethinglike:#List.iternums(printf"%A")#InRuby,IwishIcoulddosomethinglike:nums.eachputs在Ruby中能不能做到类似的简洁?我可以只

  10. 【鸿蒙应用开发系列】- 获取系统设备信息以及版本API兼容调用方式 - 2

    在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList​()Obt

随机推荐