UNPKG

27.4 kBJavaScriptView Raw
1// MIT License
2//
3// Copyright 2016-2020 Electric Imp
4//
5// SPDX-License-Identifier: MIT
6//
7// Permission is hereby granted, free of charge, to any person obtaining a copy
8// of this software and associated documentation files (the "Software"), to deal
9// in the Software without restriction, including without limitation the rights
10// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11// copies of the Software, and to permit persons to whom the Software is
12// furnished to do so, subject to the following conditions:
13//
14// The above copyright notice and this permission notice shall be
15// included in all copies or substantial portions of the Software.
16//
17// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
20// EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
21// OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
22// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
23// OTHER DEALINGS IN THE SOFTWARE.
24
25'use strict';
26
27const url = require('url');
28const path = require('path');
29const upath = require('upath');
30const fs = require('fs');
31const md5 = require('md5');
32
33const Expression = require('./Expression');
34const AbstractReader = require('./Readers/AbstractReader');
35const FileCache = require('./FileCache');
36const merge = require('./merge');
37
38// instruction types
39const INSTRUCTIONS = {
40 SET: 'set',
41 LOOP: 'loop',
42 ERROR: 'error',
43 WARNING: 'warning',
44 MACRO: 'macro',
45 OUTPUT: 'output',
46 INCLUDE: 'include',
47 CONDITIONAL: 'conditional',
48};
49
50// custom errors
51const Errors = {
52 'UserDefinedError': class UserDefinedError extends Error {
53 },
54 'MacroIsAlreadyDeclared': class MacroIsAlreadyDeclared extends Error {
55 },
56 'ExpressionEvaluationError': class ExpressionEvaluationError extends Error {
57 },
58 'SourceInclusionError': class SourceInclusionError extends Error {
59 },
60 'MaxExecutionDepthReachedError': class MaxExecutionDepthReachedError extends Error {
61 }
62};
63
64// maximum nesting depth
65const MAX_EXECUTION_DEPTH = 256;
66
67/**
68 * Builder VM
69 */
70class Machine {
71
72 constructor() {
73 this.file = 'main'; // default source filename
74 this.path = upath.resolve('.'); // default source path
75 this.readers = {};
76 this.globals = {};
77 this.fileCache = new FileCache(this);
78 this._initBuiltinFunctions();
79 }
80
81 /**
82 * Execute some code
83 * @param {string} source
84 * @param {{}={}} context
85 */
86 execute(source, context) {
87 // reset state
88 this._reset();
89
90 // parse
91 const ast = this.parser.parse(source);
92
93 // read dependencies
94 this._loadDependencies();
95
96 // read directives, merge it with actual context
97 context = this._loadAndMergeDirectives(context);
98
99 // execute
100 context = merge(
101 {__FILE__: this.file, __PATH__: this.path},
102 this._builtinFunctions,
103 this.globals,
104 context
105 );
106
107 const buffer = [];
108 this._execute(ast, context, buffer);
109
110 // save dependencies
111 this._saveDependencies();
112
113 // save directives
114 this._saveDirectives();
115
116 // return output buffer contents
117 return buffer.join('');
118 }
119
120 clearCache() {
121 this.fileCache.clearCache();
122 }
123
124 /**
125 * Init built-in expression functions
126 * @private
127 */
128 _initBuiltinFunctions() {
129 this._builtinFunctions = {}; // builtin functions
130
131 // include()
132 const that = this;
133 this._builtinFunctions['include'] = function() {
134 const args = [].slice.call(arguments);
135 if (args.length < 1) {
136 throw Error('Wrong number of arguments for include()');
137 }
138
139 const buffer = [];
140
141 // include macro in inline mode
142 that._includeSource(
143 args[0],
144 /* enable inline mode for all subsequent operations */
145 merge(this, {__INLINE__: true}),
146 buffer,
147 false,
148 true
149 );
150
151 // trim trailing newline in inline mode
152 that._trimLastLine(buffer);
153
154 return buffer.join('');
155 };
156 }
157
158 /**
159 * Reset state
160 * @private
161 */
162 _reset() {
163 this._macros = {}; // macros
164 this._depth = 0; // nesting level
165 this._includedSources = new Set(); // all included sources
166 this._includedSourcesHashes = new Map(); // all included sources content hash values
167 this._globalContext = {}; // global context
168 }
169
170 /**
171 * Format path
172 * @private
173 */
174 _formatPath(filepath, filename) {
175 return path.normalize(path.join(filepath, filename));
176 }
177
178 /**
179 * Execute AST
180 * @param {[]} ast
181 * @param {{}} context
182 * @param {string[]} buffer - output buffer
183 * @private
184 */
185 _execute(ast, context, buffer) {
186
187 if (this._depth === MAX_EXECUTION_DEPTH) {
188 throw new Errors.MaxExecutionDepthReachedError(
189 // Since anything greater than zero means a recurring call
190 // from the entry base block, __LINE__ will be defined in context.
191 // MAX_INCLUDE_DEPTH == 0 doesn't allow execution at all.
192 `Maximum execution depth reached, possible cyclic reference? (${this._formatPath(context.__PATH__, context.__FILE__)}:${context.__LINE__})`
193 );
194 }
195
196 this._depth++;
197
198 for (const instruction of ast) {
199
200 // set __LINE__
201 context = merge(
202 context,
203 {__LINE__: instruction._line}
204 );
205
206 try {
207
208 switch (instruction.type) {
209
210 case INSTRUCTIONS.INCLUDE:
211 this._executeInclude(instruction, context, buffer);
212 break;
213
214 case INSTRUCTIONS.OUTPUT:
215 this._executeOutput(instruction, context, buffer);
216 break;
217
218 case INSTRUCTIONS.SET:
219 this._executeSet(instruction, context, buffer);
220 break;
221
222 case INSTRUCTIONS.CONDITIONAL:
223 this._executeConditional(instruction, context, buffer);
224 break;
225
226 case INSTRUCTIONS.ERROR:
227 this._executeError(instruction, context, buffer);
228 break;
229
230 case INSTRUCTIONS.WARNING:
231 this._executeWarning(instruction, context, buffer);
232 break;
233
234 case INSTRUCTIONS.MACRO:
235 this._executeMacro(instruction, context, buffer);
236 break;
237
238 case INSTRUCTIONS.LOOP:
239 this._executeLoop(instruction, context, buffer);
240 break;
241
242 default:
243 throw new Error(`Unsupported instruction "${instruction.type}"`);
244 }
245
246 } catch (e) {
247
248 // add file/line information to errors
249 if (e instanceof Expression.Errors.ExpressionError) {
250 throw new Errors.ExpressionEvaluationError(`${e.message} (${this._formatPath(context.__PATH__, context.__FILE__)}:${context.__LINE__})`);
251 } else if (e instanceof AbstractReader.Errors.SourceReadingError) {
252 throw new Errors.SourceInclusionError(`${e.message} (${this._formatPath(context.__PATH__, context.__FILE__)}:${context.__LINE__})`);
253 } else {
254 throw e;
255 }
256
257 }
258 }
259
260 this._depth--;
261 }
262
263 /**
264 * Execute "include" instruction
265 * @param {{type, value}} instruction
266 * @param {{}} context
267 * @param {string[]} buffer
268 * @private
269 */
270 _executeInclude(instruction, context, buffer) {
271
272 const macro = this.expression.parseMacroCall(
273 instruction.value,
274 context,
275 this._macros
276 );
277
278 if (macro) {
279 // macro inclusion
280 this._includeMacro(macro, context, buffer);
281 } else {
282 // source inclusion
283 this._includeSource(instruction.value, context, buffer, instruction.once);
284 }
285 }
286
287 /**
288 * Concatenate github/bitbucket/azure/git-local URL prefix and local relative include
289 * @param {string[]} prefix
290 * @param {string[]} includePath
291 * @private
292 */
293 _formatURL(prefix, includePath) {
294 if (!prefix) {
295 return undefined;
296 }
297
298 const res = prefix.match(/^(github:)(.*)/) ||
299 prefix.match(/^(bitbucket-server:)(.*)/) ||
300 prefix.match(/^(git-azure-repos:)(.*)/) ||
301 prefix.match(/^(git-local:)(.*)/);
302
303 if (res === null) {
304 return undefined;
305 }
306
307 const suffix = upath.normalize(upath.join(res[2], includePath));
308 return `${res[1]}${suffix}`;
309 }
310
311 /**
312 * Replace local includes to github/bitbucket/azure/git-local/weblink URLs if requested
313 * @param {string} includePath
314 * @param {{}} context
315 * @private
316 */
317 _remoteRelativeIncludes(includePath, context) {
318
319 if (!this.remoteRelativeIncludes) {
320 return includePath;
321 }
322
323 // Consider cases when includePath starts with "/"
324 if (this._isUnixAbsolutePath(includePath)) {
325 if (context.__REPO_PREFIX__) {
326 // If the include path is absolute (in unix way, i.e. starts from '/') and is
327 // included from repository file, we can consider it relatively to the repo root
328 const relativePath = this._formatURL(context.__REPO_PREFIX__, includePath);
329 // Adding ref to the path if the ref exists
330 return this._addRefToPath(relativePath, context);
331 } else if (context.__URL_ROOT__) {
332 // If the include path is absolute in unix way and is included from
333 // weblink, we can consider it relatively to the URL root
334 return url.resolve(context.__URL_ROOT__, includePath);
335 }
336 }
337
338 const targetReader = this._getReader(includePath);
339
340 if (path.isAbsolute(includePath) || targetReader === this.readers.http) {
341 // If this is an absolute local path or a web link, don't do anything with it
342 return includePath;
343 }
344
345 // If the include path is a repository absolute path and refers to the repo it was included from and doesn't have a ref
346 // specified, we add the ref specified for the previous include (the path to the file where the current include was taken from)
347 // Otherwise, we just return the include path back if it is a repository absolute path
348 if (this._isRepositoryInclude(includePath)) {
349 const parsedPath = targetReader.parsePath(includePath);
350
351 if (parsedPath.__REPO_PREFIX__ == context.__REPO_PREFIX__ && !parsedPath.__REPO_REF__) {
352 return this._addRefToPath(includePath, context);
353 }
354
355 // Absolute github/bitbucket/azure/git-local include
356 return includePath;
357 }
358
359 // Check if the parent file is included from repository source - if so, consider includePath relative to the repo root
360 const remotePath = this._formatURL(context.__PATH__, includePath);
361 if (remotePath && this._isRepositoryInclude(remotePath)) {
362 return this._addRefToPath(remotePath, context);
363 }
364
365 // Check if the parent file is included from a web link - if so, consider includePath relative to the URL root
366 if (context.__URL_ROOT__) {
367 const pathToFile = upath.join(upath.dirname(context.__URL_PATH__), includePath);
368 return url.resolve(context.__URL_ROOT__, pathToFile);
369 }
370
371 return includePath;
372 }
373
374 /**
375 * Include source
376 * @param {string} source
377 * @param {{}} context
378 * @param {string[]} buffer
379 * @param {boolean=false} once
380 * @param {boolean=false} evaluated - is source ref already evaluated?
381 * @private
382 */
383 _includeSource(source, context, buffer, once, evaluated) {
384 // replace all \ with / because otherwise readers will not read the source
385 source = source.replace(/\\/g, '/');
386
387 // path is an expression, evaluate it
388 let includePath = evaluated ? source : this.expression.evaluate(
389 source,
390 context,
391 ).trim();
392
393 // checkout local includes in the remote sources (github/bitbucket/azure/git-local/weblink)
394 includePath = this._remoteRelativeIncludes(includePath, context);
395 // if included path starts from '/' and is being included from Windows system,
396 // we join the root of the current volume (such as C:/, D:/ and so on) to the
397 // included path
398 if (this._isUnixAbsolutePath(includePath) && process.platform === "win32") {
399 const root = path.parse(this._path).root;
400 includePath = upath.join(root, includePath);
401 }
402
403 // if once flag is set, then check if source has already been included (and avoid the read below if avoidable)
404 if (once && this._includedSources.has(includePath)) {
405 this.logger.debug(`Skipping source "${includePath}" - path has already been included previously`);
406 return;
407 }
408
409 const reader = this._getReader(includePath);
410 this.logger.info(`Including source "${includePath}"`);
411
412 // read
413 const res = this.fileCache.read(reader, includePath, this.dependencies, context);
414
415 // calculate md5 hash
416 const md5sum = md5(res.content);
417
418 // if once flag is set, then check if source has already been included
419 if (once && this._includedSourcesHashes.has(md5sum)) {
420 this._includedSources.add(includePath) // Prevent fetches in the future for the same path
421 this.logger.debug(`Skipping source "${includePath}" - contents have already been included previously`);
422 return;
423 }
424
425 // Check if source with same hash value has already been included
426 if (!this.suppressDupWarning && this._includedSourcesHashes.has(md5sum)) {
427 const path = this._includedSourcesHashes.get(md5sum).path;
428 const file = this._includedSourcesHashes.get(md5sum).file;
429 const line = this._includedSourcesHashes.get(md5sum).line;
430 const dupPath = includePath;
431 const dupFile = context.__FILE__;
432 const dupLine = context.__LINE__;
433 const message = `Warning: duplicated includes detected! The same exact file content is included from
434 ${file}:${line} (${path})
435 ${dupFile}:${dupLine} (${dupPath})`;
436
437 console.error("\x1b[33m" + message + '\u001b[39m');
438 }
439
440 const info = {
441 path: includePath,
442 file: context.__FILE__,
443 line: context.__LINE__,
444 };
445
446 this._includedSourcesHashes.set(md5sum, info);
447
448 // provide filename for correct error messages
449 this.parser.file = res.includePathParsed.__FILE__;
450
451 // parse
452 const ast = this.parser.parse(res.content);
453
454 // update context
455 context = merge(
456 context,
457 res.includePathParsed
458 );
459
460 // store included source
461 this._includedSources.add(includePath);
462
463 // execute included AST
464 this._execute(ast, context, buffer);
465 }
466
467 /**
468 * Include macro
469 * @param {{name, args: []}} macro
470 * @param {{}} context
471 * @param {string[]} buffer
472 * @private
473 */
474 _includeMacro(macro, context, buffer) {
475 // context for macro
476 const macroContext = {};
477
478 // iterate through macro arguments
479 // missing arguments will not be defined in macro context (ie will be evaluated as nulls)
480 // extra arguments passed in macro call are omitted
481 for (let i = 0; i < Math.min(this._macros[macro.name].args.length, macro.args.length); i++) {
482 macroContext[this._macros[macro.name].args[i]] = macro.args[i];
483 }
484
485 // update context
486
487 // __FILE__/__PATH__ (file macro is defined in)
488 macroContext.__FILE__ = this._macros[macro.name].file;
489 macroContext.__PATH__ = this._macros[macro.name].path;
490
491 // execute macro
492 this._execute(
493 this._macros[macro.name].body,
494 merge(context, macroContext),
495 buffer
496 );
497 }
498
499 /**
500 * Execute "output" instruction
501 * @param {{type, value, computed}} instruction
502 * @param {{}} context
503 * @param {string[]} buffer
504 * @private
505 */
506 _executeOutput(instruction, context, buffer) {
507
508 if (instruction.computed) {
509
510 // pre-computed output
511 this._out(
512 String(instruction.value),
513 context,
514 buffer
515 );
516
517 } else {
518
519 // evaluate & output
520 this._out(
521 String(this.expression.evaluate(
522 instruction.value,
523 context
524 )),
525 context,
526 buffer
527 );
528
529 }
530 }
531
532 /**
533 * Execute "set" instruction
534 * @param {{type, variable, value}} instruction
535 * @param {{}} context
536 * @param {string[]} buffer
537 * @private
538 */
539 _executeSet(instruction, context, buffer) {
540 this._globalContext[instruction.variable] =
541 this.expression.evaluate(
542 instruction.value,
543 context
544 );
545 }
546
547 /**
548 * Execute "error" instruction
549 * @param {{type, value}} instruction
550 * @param {{}} context
551 * @param {string[]} buffer
552 * @private
553 */
554 _executeError(instruction, context, buffer) {
555 throw new Errors.UserDefinedError(
556 this.expression.evaluate(instruction.value,
557 context
558 )
559 );
560 }
561
562 /**
563 * Execute "warning" instruction
564 * @param {{type, value}} instruction
565 * @param {{}} context
566 * @param {string[]} buffer
567 * @private
568 */
569 _executeWarning(instruction, context, buffer) {
570 const message = this.expression.evaluate(instruction.value,
571 context
572 );
573 console.error("\x1b[33m" + message + '\u001b[39m');
574 }
575
576 /**
577 * Execute "conditional" instruction
578 * @param {{type, test, consequent, alternate, elseifs}} instruction
579 * @param {{}} context
580 * @param {string[]} buffer
581 * @private
582 */
583 _executeConditional(instruction, context, buffer) {
584
585 const test = this.expression.evaluate(
586 instruction.test,
587 context
588 );
589
590 if (test) {
591
592 this._execute(instruction.consequent, context, buffer);
593
594 } else {
595
596 // elseifs
597 if (instruction.elseifs) {
598 for (const elseif of instruction.elseifs) {
599 if (this._executeConditional(elseif, context, buffer)) {
600 // "@elseif true" stops if-elseif...-else flow
601 return;
602 }
603 }
604 }
605
606 // else
607 if (instruction.alternate) {
608 this._execute(instruction.alternate, context, buffer);
609 }
610
611 }
612
613 return test;
614 }
615
616 /**
617 * Execute macro declaration instruction
618 * @param {{type, declaration, body: []}} instruction
619 * @param {{}} context
620 * @param {string[]} buffer
621 * @private
622 */
623 _executeMacro(instruction, context, buffer) {
624 // parse declaration of a macro
625 const macro = this.expression.parseMacroDeclaration(instruction.declaration);
626
627 // do not allow macro redeclaration
628 if (this._macros.hasOwnProperty(macro.name)) {
629 throw new Errors.MacroIsAlreadyDeclared(
630 `Macro "${macro.name}" is already declared in ` +
631 `${this._macros[macro.name].file}:${this._macros[macro.name].line}` +
632 ` (${this._formatPath(context.__PATH__, context.__FILE__)}:${context.__LINE__})`
633 );
634 }
635
636 // save macro
637 this._macros[macro.name] = {
638 file: context.__FILE__, // file at declaration
639 path: context.__PATH__, // path at declaration
640 line: context.__LINE__, // line of declaration
641 args: macro.args,
642 body: instruction.body
643 };
644
645 // add macro to supported function in expression expression
646 const that = this;
647 this._globalContext[macro.name] = (function(macro) {
648 return function() {
649 const args = [].slice.call(arguments);
650 const buffer = [];
651 macro.args = args;
652
653 // include macro in inline mode
654 that._includeMacro(
655 macro,
656 /* enable inline mode for all subsequent operations */
657 merge(this, {__INLINE__: true}),
658 buffer
659 );
660
661 // trim trailing newline (only in inline mode for macros)
662 that._trimLastLine(buffer);
663
664 return buffer.join('');
665 };
666 })(macro);
667 }
668
669 /**
670 * Execute loop instruction
671 * @param {{type, while, repeat, body: []}} instruction
672 * @param {{}} context
673 * @param {string[]} buffer
674 * @private
675 */
676 _executeLoop(instruction, context, buffer) {
677
678 let index = 0;
679
680 while (true) {
681 // evaluate test expression
682 const test = this._expression.evaluate(
683 instruction.while || instruction.repeat,
684 context
685 );
686
687 // check break condition
688 if (instruction.while && !test) {
689 break;
690 } else if (instruction.repeat && test === index) {
691 break;
692 }
693
694 // execute body
695 this._execute(
696 instruction.body,
697 merge(
698 context,
699 {loop: {index, iteration: index + 1}}
700 ),
701 buffer
702 );
703
704 // increment index
705 index++;
706 }
707
708 }
709
710 /**
711 * Perform output operation
712 * @param {string|string[]} output
713 * @param {{}} context
714 * @param {string[]} buffer
715 * @private
716 */
717 _out(output, context, buffer) {
718 // generate line control statement
719 if (this.generateLineControlStatements && !context.__INLINE__) {
720 if (buffer.lastOutputFile !== context.__FILE__ /* detect file switch */) {
721 let parsedURL = url.parse(context.__PATH__);
722 let source = parsedURL.protocol ?
723 `${context.__PATH__}/${context.__FILE__}` :
724 path.join(context.__PATH__, context.__FILE__);
725 buffer.push(`#line ${context.__LINE__} "${source.replace(/"/g, '\\\"')}"\n`);
726 buffer.lastOutputFile = context.__FILE__;
727 }
728 }
729
730 // append output to buffer
731 if (Array.isArray(output)) {
732 for (const chunk of output) {
733 buffer.push(chunk);
734 }
735 } else {
736 buffer.push(output);
737 }
738 }
739
740 /**
741 * Find reader
742 *
743 * @param {*} source
744 * @return {AbstractReader}
745 * @private
746 */
747 _getReader(source) {
748 for (const type in this.readers) {
749 const reader = this.readers[type];
750 if (reader.supports(source)) {
751 return reader;
752 }
753 }
754
755 throw new Error(`Source "${source}" is not supported`);
756 }
757
758 /**
759 * Check if path is unix absolute path
760 *
761 * @param {string} source
762 * @return {boolean}
763 * @private
764 */
765 _isUnixAbsolutePath(source) {
766 return source.length ? (source[0] === '/') : false;
767 }
768
769 /**
770 * Check if included from repository
771 *
772 * @param {*} source
773 * @return {boolean}
774 * @private
775 */
776 _isRepositoryInclude(source) {
777 const reader = this._getReader(source);
778 return reader === this.readers.github ||
779 reader === this.readers.bitbucketSrv ||
780 reader === this.readers.azureRepos ||
781 reader === this.readers.gitLocal;
782 }
783
784 /**
785 * Concatenates path with repo ref if needed
786 *
787 * @param {string} includePath
788 * @param {{}} context
789 * @return {string}
790 * @private
791 */
792 _addRefToPath(includePath, context) {
793 return (context.__REPO_REF__ && includePath.indexOf("@") == -1) ? `${includePath}@${context.__REPO_REF__}` : includePath;
794 }
795
796 /**
797 * Trim last buffer line
798 * @param {string[]} buffer
799 * @private
800 */
801 _trimLastLine(buffer) {
802 // trim trailing newline in inline mode
803 if (buffer.length > 0) {
804 buffer[buffer.length - 1] =
805 buffer[buffer.length - 1]
806 .replace(/(\r\n|\n)$/, '');
807 }
808 }
809
810 /**
811 * Read dependencies JSON file content
812 * @param
813 * @return
814 * @private
815 */
816 _loadDependencies() {
817 if (!this.dependenciesUseFile) {
818 if (this.dependenciesSaveFile) {
819 this.dependencies = new Map();
820 }
821
822 return;
823 }
824
825 if (!fs.existsSync(this.dependenciesUseFile)) {
826 throw new Error(`The dependencies JSON file '${this.dependenciesUseFile}' does not exist`)
827 }
828
829 try {
830 this.dependencies = new Map(JSON.parse(fs.readFileSync(this.dependenciesUseFile, 'utf8')));
831 } catch(err) {
832 throw new Error(`The dependencies JSON file '${this.dependenciesUseFile}' cannot be used: ${err.message}`);
833 }
834 }
835
836 /**
837 * Read directives JSON file content and merge it with actual context
838 * @param {*} context
839 * @return {*}
840 * @private
841 */
842 _loadAndMergeDirectives(context) {
843 if (!this.directivesUseFile) {
844 if (this.directivesSaveFile) {
845 this.directives = Object.assign({}, context);
846 }
847
848 return context;
849 }
850
851 if (!fs.existsSync(this.directivesUseFile)) {
852 throw new Error(`The directives JSON file '${this.directivesUseFile}' does not exist`);
853 }
854
855 try {
856 this.directives = merge(JSON.parse(fs.readFileSync(this.directivesUseFile).toString()), context);
857 } catch(err) {
858 throw new Error(`The directives JSON file '${this.directivesUseFile}' cannot be used: ${err.message}`);
859 }
860
861 return Object.assign({}, this.directives);
862 }
863
864 /**
865 * Save dependencies JSON file
866 * @param
867 * @return
868 * @private
869 */
870 _saveDependencies() {
871 if (!this.dependenciesSaveFile) {
872 return;
873 }
874
875 try {
876 fs.writeFileSync(this.dependenciesSaveFile, JSON.stringify([...this.dependencies], null, 2), 'utf-8');
877 } catch (err) {
878 throw new Error(`The ${this.dependenciesSaveFile} file cannot be saved: ${err.message}`);
879 }
880 }
881
882 /**
883 * Save defined variables to directives JSON file
884 * @param
885 * @return
886 * @private
887 */
888 _saveDirectives() {
889 if (!this.directivesSaveFile || !this.directives) {
890 return;
891 }
892
893 try {
894 fs.writeFileSync(this.directivesSaveFile, JSON.stringify(this.directives, null, 2));
895 } catch(err) {
896 throw new Error(`The ${this.directivesSaveFile} file cannot be saved: ${err.message}`);
897 }
898 }
899
900 // <editor-fold desc="Accessors" defaultstate="collapsed">
901
902 /**
903 * @return {*} value
904 */
905 get readers() {
906 return this._readers;
907 }
908
909 /**
910 * @param {*} value
911 */
912 set readers(value) {
913 this._readers = value;
914 }
915
916 /**
917 * @return {Expression}
918 */
919 get expression() {
920 return this._expression;
921 }
922
923 /**
924 * @param {Expression} value
925 */
926 set expression(value) {
927 this._expression = value;
928 }
929
930 /**
931 * @return {{debug(),info(),warning(),error()}}
932 */
933 get logger() {
934 return this._logger || {
935 debug: console.log,
936 info: console.info,
937 warning: console.warning,
938 error: console.error
939 };
940 }
941
942 /**
943 * @param {{debug(),info(),warning(),error()}} value
944 */
945 set logger(value) {
946 this._logger = value;
947
948 for (const readerType in this.readers) {
949 this.readers[readerType].logger = value;
950 }
951 }
952
953 /**
954 * @return {AstParser}
955 */
956 get parser() {
957 return this._astParser;
958 }
959
960 /**
961 * @param {AstParser} value
962 */
963 set parser(value) {
964 this._astParser = value;
965 }
966
967 /**
968 * Generate line control statements?
969 * @see https://gcc.gnu.org/onlinedocs/cpp/Line-Control.html
970 * @return {boolean}
971 */
972 get generateLineControlStatements() {
973 return this._generateLineControlStatements || false;
974 }
975
976 /**
977 * @param {boolean} value
978 */
979 set generateLineControlStatements(value) {
980 this._generateLineControlStatements = value;
981 }
982
983 /**
984 * Use cache?
985 * @return {boolean}
986 */
987 get useCache() {
988 return this.fileCache.useCache;
989 }
990
991 /**
992 * @param {boolean} value
993 */
994 set useCache(value) {
995 this.fileCache.useCache = value;
996 }
997
998 /**
999 * Filename
1000 * @return {string}
1001 */
1002 get file() {
1003 return this._file;
1004 }
1005
1006 /**
1007 * @param {string} value
1008 */
1009 set file(value) {
1010 this._file = value;
1011 }
1012
1013 get path() {
1014 return this._path;
1015 }
1016
1017 set path(value) {
1018 this._path = value;
1019 }
1020
1021 get globals() {
1022 return this._globals;
1023 }
1024
1025 set globals(value) {
1026 this._globals = value;
1027 }
1028
1029 get excludeList() {
1030 return this.fileCache.excludeList;
1031 }
1032
1033 /**
1034 * Construct exclude regexp list from filename
1035 * @param {string} name of exclude file. '' for default
1036 */
1037 set excludeList(fileName) {
1038 this.fileCache.excludeList = fileName;
1039 }
1040 // </editor-fold>
1041}
1042
1043module.exports = Machine;
1044module.exports.INSTRUCTIONS = INSTRUCTIONS;
1045module.exports.Errors = Errors;