From 9d65528b0190482707edb0aad684e7bcc0478b21 Mon Sep 17 00:00:00 2001
From: Dave Eddy <dave@daveeddy.com>
Date: Mon, 17 Aug 2015 17:33:13 -0400
Subject: [PATCH] node: add -c|--check CLI arg to syntax check script

PR-URL: https://github.com/nodejs/node/pull/2411
Reviewed-By: Rod Vagg <rod@vagg.org>
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
Reviewed-By: Evan Lucas <evanlucas@me.com>
---
 doc/node.1                                  |  2 +
 lib/internal/module.js                      | 15 ++++
 lib/module.js                               | 16 +---
 node.gyp                                    |  1 +
 src/node.cc                                 |  9 +++
 src/node.js                                 | 16 ++++
 test/fixtures/syntax/bad_syntax.js          |  1 +
 test/fixtures/syntax/bad_syntax_shebang.js  |  2 +
 test/fixtures/syntax/good_syntax.js         |  1 +
 test/fixtures/syntax/good_syntax_shebang.js |  2 +
 test/parallel/test-cli-syntax.js            | 84 +++++++++++++++++++++
 11 files changed, 136 insertions(+), 13 deletions(-)
 create mode 100644 lib/internal/module.js
 create mode 100644 test/fixtures/syntax/bad_syntax.js
 create mode 100644 test/fixtures/syntax/bad_syntax_shebang.js
 create mode 100644 test/fixtures/syntax/good_syntax.js
 create mode 100644 test/fixtures/syntax/good_syntax_shebang.js
 create mode 100644 test/parallel/test-cli-syntax.js

diff --git a/doc/node.1 b/doc/node.1
index af494dade98b22..854cc68ef2c87f 100644
--- a/doc/node.1
+++ b/doc/node.1
@@ -49,6 +49,8 @@ and servers.
 
   -p, --print            print result of --eval
 
+  -c, --check            syntax check script without executing
+
   -i, --interactive      always enter the REPL even if stdin
                          does not appear to be a terminal
 
diff --git a/lib/internal/module.js b/lib/internal/module.js
new file mode 100644
index 00000000000000..7f3a39e539424a
--- /dev/null
+++ b/lib/internal/module.js
@@ -0,0 +1,15 @@
+'use strict';
+
+module.exports.stripBOM = stripBOM;
+
+/**
+ * Remove byte order marker. This catches EF BB BF (the UTF-8 BOM)
+ * because the buffer-to-string conversion in `fs.readFileSync()`
+ * translates it to FEFF, the UTF-16 BOM.
+ */
+function stripBOM(content) {
+  if (content.charCodeAt(0) === 0xFEFF) {
+    content = content.slice(1);
+  }
+  return content;
+}
diff --git a/lib/module.js b/lib/module.js
index da8a906f951541..b1091ca66d6b08 100644
--- a/lib/module.js
+++ b/lib/module.js
@@ -2,6 +2,7 @@
 
 const NativeModule = require('native_module');
 const util = require('util');
+const internalModule = require('internal/module');
 const internalUtil = require('internal/util');
 const runInThisContext = require('vm').runInThisContext;
 const assert = require('assert').ok;
@@ -435,21 +436,10 @@ Module.prototype._compile = function(content, filename) {
 };
 
 
-function stripBOM(content) {
-  // Remove byte order marker. This catches EF BB BF (the UTF-8 BOM)
-  // because the buffer-to-string conversion in `fs.readFileSync()`
-  // translates it to FEFF, the UTF-16 BOM.
-  if (content.charCodeAt(0) === 0xFEFF) {
-    content = content.slice(1);
-  }
-  return content;
-}
-
-
 // Native extension for .js
 Module._extensions['.js'] = function(module, filename) {
   var content = fs.readFileSync(filename, 'utf8');
-  module._compile(stripBOM(content), filename);
+  module._compile(internalModule.stripBOM(content), filename);
 };
 
 
@@ -457,7 +447,7 @@ Module._extensions['.js'] = function(module, filename) {
 Module._extensions['.json'] = function(module, filename) {
   var content = fs.readFileSync(filename, 'utf8');
   try {
-    module.exports = JSON.parse(stripBOM(content));
+    module.exports = JSON.parse(internalModule.stripBOM(content));
   } catch (err) {
     err.message = filename + ': ' + err.message;
     throw err;
diff --git a/node.gyp b/node.gyp
index 22079785d951f4..9575f4cbcd4a40 100644
--- a/node.gyp
+++ b/node.gyp
@@ -70,6 +70,7 @@
       'lib/zlib.js',
       'lib/internal/child_process.js',
       'lib/internal/freelist.js',
+      'lib/internal/module.js',
       'lib/internal/socket_list.js',
       'lib/internal/repl.js',
       'lib/internal/util.js',
diff --git a/src/node.cc b/src/node.cc
index 501caf280ad462..faac8b4629b269 100644
--- a/src/node.cc
+++ b/src/node.cc
@@ -121,6 +121,7 @@ using v8::Value;
 
 static bool print_eval = false;
 static bool force_repl = false;
+static bool syntax_check_only = false;
 static bool trace_deprecation = false;
 static bool throw_deprecation = false;
 static bool abort_on_uncaught_exception = false;
@@ -2823,6 +2824,11 @@ void SetupProcessObject(Environment* env,
     READONLY_PROPERTY(process, "_print_eval", True(env->isolate()));
   }
 
+  // -c, --check
+  if (syntax_check_only) {
+    READONLY_PROPERTY(process, "_syntax_check_only", True(env->isolate()));
+  }
+
   // -i, --interactive
   if (force_repl) {
     READONLY_PROPERTY(process, "_forceRepl", True(env->isolate()));
@@ -3079,6 +3085,7 @@ static void PrintHelp() {
          "  -v, --version         print Node.js version\n"
          "  -e, --eval script     evaluate script\n"
          "  -p, --print           evaluate script and print result\n"
+         "  -c, --check           syntax check script without executing\n"
          "  -i, --interactive     always enter the REPL even if stdin\n"
          "                        does not appear to be a terminal\n"
          "  -r, --require         module to preload (option can be repeated)\n"
@@ -3208,6 +3215,8 @@ static void ParseArgs(int* argc,
       }
       args_consumed += 1;
       local_preload_modules[preload_module_count++] = module;
+    } else if (strcmp(arg, "--check") == 0 || strcmp(arg, "-c") == 0) {
+      syntax_check_only = true;
     } else if (strcmp(arg, "--interactive") == 0 || strcmp(arg, "-i") == 0) {
       force_repl = true;
     } else if (strcmp(arg, "--no-deprecation") == 0) {
diff --git a/src/node.js b/src/node.js
index a2b21fb4342b3e..da0fc6725e3ecc 100644
--- a/src/node.js
+++ b/src/node.js
@@ -93,6 +93,22 @@
         process.argv[1] = path.resolve(process.argv[1]);
 
         var Module = NativeModule.require('module');
+
+        // check if user passed `-c` or `--check` arguments to Node.
+        if (process._syntax_check_only != null) {
+          var vm = NativeModule.require('vm');
+          var fs = NativeModule.require('fs');
+          var internalModule = NativeModule.require('internal/module');
+          // read the source
+          var filename = Module._resolveFilename(process.argv[1]);
+          var source = fs.readFileSync(filename, 'utf-8');
+          // remove shebang and BOM
+          source = internalModule.stripBOM(source.replace(/^\#\!.*/, ''));
+          // compile the script, this will throw if it fails
+          new vm.Script(source, {filename: filename, displayErrors: true});
+          process.exit(0);
+        }
+
         startup.preloadModules();
         if (global.v8debug &&
             process.execArgv.some(function(arg) {
diff --git a/test/fixtures/syntax/bad_syntax.js b/test/fixtures/syntax/bad_syntax.js
new file mode 100644
index 00000000000000..c2cd118b23b133
--- /dev/null
+++ b/test/fixtures/syntax/bad_syntax.js
@@ -0,0 +1 @@
+var foo bar;
diff --git a/test/fixtures/syntax/bad_syntax_shebang.js b/test/fixtures/syntax/bad_syntax_shebang.js
new file mode 100644
index 00000000000000..1de5d2adc47f4b
--- /dev/null
+++ b/test/fixtures/syntax/bad_syntax_shebang.js
@@ -0,0 +1,2 @@
+#!/usr/bin/env node
+var foo bar;
diff --git a/test/fixtures/syntax/good_syntax.js b/test/fixtures/syntax/good_syntax.js
new file mode 100644
index 00000000000000..d8427075a48b0e
--- /dev/null
+++ b/test/fixtures/syntax/good_syntax.js
@@ -0,0 +1 @@
+var foo = 'bar';
diff --git a/test/fixtures/syntax/good_syntax_shebang.js b/test/fixtures/syntax/good_syntax_shebang.js
new file mode 100644
index 00000000000000..f9ff7d5694252a
--- /dev/null
+++ b/test/fixtures/syntax/good_syntax_shebang.js
@@ -0,0 +1,2 @@
+#!/usr/bin/env node
+var foo = 'bar';
diff --git a/test/parallel/test-cli-syntax.js b/test/parallel/test-cli-syntax.js
new file mode 100644
index 00000000000000..20fdfdc995acbb
--- /dev/null
+++ b/test/parallel/test-cli-syntax.js
@@ -0,0 +1,84 @@
+'use strict';
+
+const assert = require('assert');
+const spawnSync = require('child_process').spawnSync;
+const path = require('path');
+
+const common = require('../common');
+
+var node = process.execPath;
+
+// test both sets of arguments that check syntax
+var syntaxArgs = [
+  ['-c'],
+  ['--check']
+];
+
+// test good syntax with and without shebang
+[
+  'syntax/good_syntax.js',
+  'syntax/good_syntax',
+  'syntax/good_syntax_shebang.js',
+  'syntax/good_syntax_shebang',
+].forEach(function(file) {
+  file = path.join(common.fixturesDir, file);
+
+  // loop each possible option, `-c` or `--check`
+  syntaxArgs.forEach(function(args) {
+    var _args = args.concat(file);
+    var c = spawnSync(node, _args, {encoding: 'utf8'});
+
+    // no output should be produced
+    assert.equal(c.stdout, '', 'stdout produced');
+    assert.equal(c.stderr, '', 'stderr produced');
+    assert.equal(c.status, 0, 'code == ' + c.status);
+  });
+});
+
+// test bad syntax with and without shebang
+[
+  'syntax/bad_syntax.js',
+  'syntax/bad_syntax',
+  'syntax/bad_syntax_shebang.js',
+  'syntax/bad_syntax_shebang'
+].forEach(function(file) {
+  file = path.join(common.fixturesDir, file);
+
+  // loop each possible option, `-c` or `--check`
+  syntaxArgs.forEach(function(args) {
+    var _args = args.concat(file);
+    var c = spawnSync(node, _args, {encoding: 'utf8'});
+
+    // no stdout should be produced
+    assert.equal(c.stdout, '', 'stdout produced');
+
+    // stderr should have a syntax error message
+    var match = c.stderr.match(/^SyntaxError: Unexpected identifier$/m);
+    assert(match, 'stderr incorrect');
+
+    assert.equal(c.status, 1, 'code == ' + c.status);
+  });
+});
+
+// test file not found
+[
+  'syntax/file_not_found.js',
+  'syntax/file_not_found'
+].forEach(function(file) {
+  file = path.join(common.fixturesDir, file);
+
+  // loop each possible option, `-c` or `--check`
+  syntaxArgs.forEach(function(args) {
+    var _args = args.concat(file);
+    var c = spawnSync(node, _args, {encoding: 'utf8'});
+
+    // no stdout should be produced
+    assert.equal(c.stdout, '', 'stdout produced');
+
+    // stderr should have a module not found error message
+    var match = c.stderr.match(/^Error: Cannot find module/m);
+    assert(match, 'stderr incorrect');
+
+    assert.equal(c.status, 1, 'code == ' + c.status);
+  });
+});