From 0b9470da72d1894c58de428f44d5dac0958d39cc Mon Sep 17 00:00:00 2001 From: Taylor Woll Date: Tue, 4 Jun 2019 20:44:06 -0700 Subject: [PATCH] Implement hashbang syntax Hashbang is a single-line comment syntax designed to mimic interpreter directive syntax common in command-line scripting. The proposal has reached stage 3 and the syntax is fully supported in SpiderMonkey and V8. See the proposal: https://github.com/tc39/proposal-hashbang This a pretty simple syntax with only a couple of rules. The hashbang acts like a single-line comment except the hashbang sequence (`#!`) must be the first token in the source text. --- lib/Common/ConfigFlagsList.h | 4 + lib/Parser/Scan.cpp | 11 +++ lib/Runtime/Base/ThreadConfigFlagsList.h | 1 + test/Scanner/Hashbang.js | 110 +++++++++++++++++++++++ test/Scanner/rlexe.xml | 6 ++ 5 files changed, 132 insertions(+) create mode 100644 test/Scanner/Hashbang.js diff --git a/lib/Common/ConfigFlagsList.h b/lib/Common/ConfigFlagsList.h index 7d9b5ed0c06..c02c2ca220c 100644 --- a/lib/Common/ConfigFlagsList.h +++ b/lib/Common/ConfigFlagsList.h @@ -680,6 +680,7 @@ PHASE(All) #define DEFAULT_CONFIG_ES2018RegExDotAll (true) #define DEFAULT_CONFIG_ESBigInt (false) #define DEFAULT_CONFIG_ESNumericSeparator (true) +#define DEFAULT_CONFIG_ESHashbang (true) #define DEFAULT_CONFIG_ESSymbolDescription (true) #define DEFAULT_CONFIG_ESGlobalThis (true) #ifdef COMPILE_DISABLE_ES6RegExPrototypeProperties @@ -1219,6 +1220,9 @@ FLAGR(Boolean, ESBigInt, "Enable ESBigInt flag", DEFAULT_CONFIG_ESBigInt) // ES Numeric Separator support for numeric constants FLAGR(Boolean, ESNumericSeparator, "Enable Numeric Separator flag", DEFAULT_CONFIG_ESNumericSeparator) +// ES Hashbang support for interpreter directive syntax +FLAGR(Boolean, ESHashbang, "Enable Hashbang syntax", DEFAULT_CONFIG_ESHashbang) + // ES Symbol.prototype.description flag FLAGR(Boolean, ESSymbolDescription, "Enable Symbol.prototype.description", DEFAULT_CONFIG_ESSymbolDescription) diff --git a/lib/Parser/Scan.cpp b/lib/Parser/Scan.cpp index 4f6fb0188d8..416310bc881 100644 --- a/lib/Parser/Scan.cpp +++ b/lib/Parser/Scan.cpp @@ -1646,6 +1646,7 @@ tokens Scanner::ScanCore(bool identifyKwds) #endif switch (ch) { +LDefault: default: if (ch == kchLS || ch == kchPS ) @@ -1961,6 +1962,16 @@ tokens Scanner::ScanCore(bool identifyKwds) } } break; + + case '#': + // Hashbang syntax is a single line comment only if it is the first token in the source + if (m_scriptContext->GetConfig()->IsESHashbangEnabled() && this->PeekFirst(p, last) == '!' && m_pchBase == m_pchMinTok) + { + p++; + goto LSkipLineComment; + } + goto LDefault; + case '/': token = tkDiv; switch(this->PeekFirst(p, last)) diff --git a/lib/Runtime/Base/ThreadConfigFlagsList.h b/lib/Runtime/Base/ThreadConfigFlagsList.h index 15d21b0a3f5..7abb5f4ee1e 100644 --- a/lib/Runtime/Base/ThreadConfigFlagsList.h +++ b/lib/Runtime/Base/ThreadConfigFlagsList.h @@ -49,6 +49,7 @@ FLAG_RELEASE(IsESSharedArrayBufferEnabled, ESSharedArrayBuffer) FLAG_RELEASE(IsESDynamicImportEnabled, ESDynamicImport) FLAG_RELEASE(IsESBigIntEnabled, ESBigInt) FLAG_RELEASE(IsESNumericSeparatorEnabled, ESNumericSeparator) +FLAG_RELEASE(IsESHashbangEnabled, ESHashbang) FLAG_RELEASE(IsESExportNsAsEnabled, ESExportNsAs) FLAG_RELEASE(IsESSymbolDescriptionEnabled, ESSymbolDescription) FLAG_RELEASE(IsESGlobalThisEnabled, ESGlobalThis) diff --git a/test/Scanner/Hashbang.js b/test/Scanner/Hashbang.js new file mode 100644 index 00000000000..36b88495b58 --- /dev/null +++ b/test/Scanner/Hashbang.js @@ -0,0 +1,110 @@ +//------------------------------------------------------------------------------------------------------- +// Copyright (C) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information. +//------------------------------------------------------------------------------------------------------- + +WScript.LoadScriptFile("..\\UnitTestFramework\\UnitTestFramework.js"); + +let invalidScripts = [ + [" #!\n", "Hashbang must be the first token (even before whitespace)"], + ["\n#!\n", "Hashbang must be the first token (even before whitespace)"], + ["##!\n", "Hashbang token is '#!'"], + [";#!\n", "Hashbang must be the first token"], + ["'use strict'#!\n", "Hashbang must come before 'use strict'"], + ["#!\n#!\n", "Only one hashbang may exist because it has to be the first token"], + ["function foo() {\n#!\n}", "Hashbang must be the first token in the script (not local function)"], + ["function foo() {#!\n}", "Hashbang must be the first token in the script (not local function)"], + ["#\\041\n", "Hashbang can't be made of encoded characters"], + ["#\\u0021\n", "Hashbang can't be made of encoded characters"], + ["#\\u{21}\n", "Hashbang can't be made of encoded characters"], + ["#\\x21\n", "Hashbang can't be made of encoded characters"], + ["\\043!\n", "Hashbang can't be made of encoded characters"], + ["\\u0023!\n", "Hashbang can't be made of encoded characters"], + ["\\u{23}!\n", "Hashbang can't be made of encoded characters"], + ["\\x23!\n", "Hashbang can't be made of encoded characters"], + ["\\u0023\\u0021\n", "Hashbang can't be made of encoded characters"], + ["Function('#!\n','')", "Hashbang is not valid in function evaluator contexts"], + ["new Function('#!\n','')", "Hashbang is not valid in function evaluator contexts"], + ["{\n#!\n}\n", "Hashbang not valid in block"], + ["#!/*\nthrow 123;\n*/\nthrow 456;", "Hashbang comments out a single line"], + ["\\\\ single line comment\n#! hashbang\n", "Single line comment may not preceed hashbang"], + ["/**/#! hashbang\n", "Multi-line comment may not preceed hashbang"], + ["/**/\n#! hashbang\n", "Multi-line comment may not preceed hashbang"], +]; + +var tests = [ + { + name: "Valid hashbang in ordinary script", + body: function () { + assert.areEqual(2, WScript.LoadScript("#! throw 'error';\nthis.prop=2;").prop); + assert.areEqual(3, WScript.LoadScript("#! throw 'error'\u{000D}this.prop=3;").prop); + assert.areEqual(4, WScript.LoadScript("#! throw 'error'\u{2028}this.prop=4;").prop); + assert.areEqual(5, WScript.LoadScript("#! throw 'error'\u{2029}this.prop=5;").prop); + } + }, + { + name: "Valid hashbang in module script", + body: function () { + WScript.RegisterModuleSource('module_hashbang_valid.js', "#! export default 123;\n export default 456;"); + + testRunner.LoadModule(` + import {default as prop} from 'module_hashbang_valid.js'; + assert.areEqual(456, prop); + `, 'samethread', false, false); + } + }, + { + name: "Valid hashbang in eval", + body: function () { + assert.areEqual(undefined, eval('#!')); + assert.areEqual(undefined, eval('#!\n')); + assert.areEqual(1, eval('#!\n1')); + assert.areEqual(undefined, eval('#!2\n')); + } + }, + { + name: "Valid hashbang in indirect eval", + body: function () { + let _eval = eval; + assert.areEqual(undefined, _eval('#!')); + assert.areEqual(undefined, _eval('#!\n')); + assert.areEqual(1, _eval('#!\n1')); + assert.areEqual(undefined, _eval('#!2\n')); + } + }, + { + name: "Invalid hashbang in ordinary script", + body: function () { + for (a of invalidScripts) { + assert.throws(()=>WScript.LoadScript(a[0]), SyntaxError, a[1]); + } + } + }, + { + name: "Invalid hashbang in module script", + body: function () { + for (a of invalidScripts) { + assert.throws(()=>WScript.LoadModule(a[0]), SyntaxError, a[1]); + } + } + }, + { + name: "Invalid hashbang in eval", + body: function () { + for (a of invalidScripts) { + assert.throws(()=>eval(a[0]), SyntaxError, a[1]); + } + } + }, + { + name: "Invalid hashbang in indirect eval", + body: function () { + let _eval = eval; + for (a of invalidScripts) { + assert.throws(()=>_eval(a[0]), SyntaxError, a[1]); + } + } + }, +]; + +testRunner.runTests(tests, { verbose: WScript.Arguments[0] != "summary" }); diff --git a/test/Scanner/rlexe.xml b/test/Scanner/rlexe.xml index 06f87bd0bcc..45a50c362f1 100644 --- a/test/Scanner/rlexe.xml +++ b/test/Scanner/rlexe.xml @@ -12,4 +12,10 @@ InvalidCharacter.baseline + + + Hashbang.js + -args summary -endargs -ESHashbang + +