File: process_command.js

package info (click to toggle)
node-stdlib 0.0.96%2Bds1%2B~cs0.0.429-2
  • links: PTS, VCS
  • area: main
  • in suites: sid, trixie
  • size: 421,476 kB
  • sloc: javascript: 1,562,831; ansic: 109,702; lisp: 49,823; cpp: 27,224; python: 7,871; sh: 6,807; makefile: 6,089; fortran: 3,102; awk: 387
file content (184 lines) | stat: -rw-r--r-- 6,794 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
/**
* @license Apache-2.0
*
* Copyright (c) 2019 The Stdlib Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*    http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

'use strict';

// MODULES //

var logger = require( 'debug' );
var parse = require( 'acorn' ).parse;
var parseLoose = require( 'acorn-loose' ).parse;
var contains = require( '@stdlib/assert/contains' );
var isSilentCommand = require( './is_silent_command.js' );
var processAwait = require( './process_top_level_await.js' );


// VARIABLES //

var debug = logger( 'repl:command' );
var AOPTS = {
	'ecmaVersion': 'latest'
};


// MAIN //

/**
* Processes a "raw" input command.
*
* ## Notes
*
* -   The function wraps trailing object literals as expressions when the literal can be misinterpreted as a block (e.g., `'{"a":1}'`, `'{"a":1};'` `'1+1;{"a":1}'`, `'1+1;{"a":1};'`, etc).
*
* @private
* @param {string} code - unevaluated command
* @returns {(string|Error)} processed command or an error
*/
function processCommand( code ) {
	var wrapped;
	var node;
	var ast;
	var tmp;
	var err;
	var i;

	// If an unevaluated command contains a top-level `await` expression, we need to transform into valid syntax...
	debug( 'Checking for `await` keyword...' );
	if ( contains( code, 'await' ) ) {
		debug( 'Attempting to process top-level `await` expression...' );
		tmp = processAwait( code );
		if ( typeof tmp === 'string' ) {
			debug( 'Successfully processed top-level `await` expression.' );
			code = tmp;
		} else {
			debug( 'Unable to process command as a top-level `await` expression.' );
			debug( 'Error: %s', tmp.message );
		}
	} else {
		debug( 'Unable to detect a top-level `await` expression.' );
	}
	// Attempt to strictly parse the provided code string:
	debug( 'Attempting to generate an AST...' );
	try {
		ast = parse( code, AOPTS );
		debug( 'Successfully generated an AST.' );
	} catch ( error ) {
		debug( 'Unable to generate an AST.' );
		debug( 'Error: %s', error.message );
		err = error; // cache the error message
	}
	if ( err === void 0 ) {
		// If the body is empty, assume that we have been given a multi-line comment...
		if ( ast.body.length === 0 ) {
			debug( 'Detected multi-line comment.' );
			return code;
		}
		// Check whether the code ends in an empty block statement, and, if so, interpret as an empty object literal...
		for ( i = ast.body.length-1; i >= 0; i-- ) {
			node = ast.body[ i ];
			if ( node.type !== 'EmptyStatement' ) {
				break;
			}
		}
		if ( node.type === 'BlockStatement' && node.body.length === 0 ) {
			debug( 'Detected a trailing empty object literal. Wrapping as an expression...' );
			code = code.slice( 0, node.start ) + '(' + code.slice( node.start, node.end ) + ')' + code.slice( node.end );
		}
		// Check whether the command is "silenced" (i.e., terminates in a semicolon):
		node = ast.body[ ast.body.length-1 ];
		if ( isSilentCommand( code ) ) {
			// To ensure that "silenced" variable/function declarations produce a return value which can be assigned to `ans`, check the last node for declarations...
			if ( node.type === 'VariableDeclaration' ) {
				debug( 'Found a trailing variable declaration which is silenced. Appending statement...' );
				tmp = node.declarations[ node.declarations.length-1 ];
				code += tmp.id.name + ';'; // assign value of the last declared variable
			} else if ( node.type === 'FunctionDeclaration' ) {
				debug( 'Found a trailing function declaration which is silenced. Appending statement...' );
				code += node.id.name + ';';
			}
		}
		// To ensure that "un-silenced" variable/function declarations produce a return value, check the last node for declarations...
		else if ( node.type === 'VariableDeclaration' ) {
			debug( 'Found a trailing variable declaration which is not silenced. Appending expression...' );
			tmp = node.declarations[ node.declarations.length-1 ];
			code += ';' + tmp.id.name; // return the assigned value of last declared variable
		} else if ( node.type === 'FunctionDeclaration' ) {
			debug( 'Found a trailing function declaration which is not silenced. Appending expression...' );
			code += ';' + node.id.name;
		}
		return code;
	}
	debug( 'Checking for a trailing object literal...' );

	// Make a best-effort attempt to parse the code string into an AST:
	ast = parseLoose( code, AOPTS );

	// If the body is empty, assume that we have been given an unterminated comment...
	if ( ast.body.length === 0 ) {
		debug( 'Detected unterminated comment.' );
		return err; // original error message
	}
	// Get the last (non-empty) node:
	for ( i = ast.body.length-1; i >= 0; i-- ) {
		node = ast.body[ i ];
		if ( node.type !== 'EmptyStatement' ) {
			break;
		}
	}
	// Check for a trailing node which is interpreted as a block statement, as this node could be an object literal...
	if ( node.type === 'BlockStatement' ) {
		debug( 'Last (non-empty) statement interpreted as a block statement. Checking if object literal...' );
		tmp = code.slice( node.start, node.end );
		try {
			// Check whether the trailing node can be interpreted as an expression:
			parse( 'return ' + tmp + ';', {
				'allowReturnOutsideFunction': true,
				'ecmaVersion': AOPTS.ecmaVersion
			});

			// Since no syntax error occurred, check whether we can parse if we parenthesize:
			wrapped = '(' + tmp + ')\n';
			parse( wrapped, AOPTS );
		} catch ( error ) {
			debug( 'Failed to detect an object literal.' );
			debug( 'Error: %s', error.message );
			debug( 'Unable to generate AST.' );
			return err; // original error message
		}
		debug( 'Detected an object literal. Wrapping as an expression...' );
		code = code.slice( 0, node.start ) + wrapped + code.slice( node.end ); // Note: this should include trailing empty statements (if any)
		debug( 'Attempting to generate an AST...' );
		try {
			ast = parse( code, AOPTS );
		} catch ( error ) {
			debug( 'Unable to generate an AST.' );
			debug( 'Error: %s', error.message );
			return err; // original error message
		}
		debug( 'Successfully generated an AST.' );
		return code;
	}
	// As the last statement is *not* a block statement, our inability to parse is not due to a trailing object literal, so assume that the user has made some other syntax error...
	debug( 'Unable to generate an AST.' );
	return err; // original error message
}


// EXPORTS //

module.exports = processCommand;