From 5e2ef371e943d7cfca7b341b687119a1a7aea6b4 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rousseau Date: Sun, 6 Dec 2020 15:07:57 -0500 Subject: [PATCH] feat(options-v2): Improve options validation for svelte v2 parser (#29) --- lib/parser.js | 174 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 122 insertions(+), 52 deletions(-) diff --git a/lib/parser.js b/lib/parser.js index 6137d8d..21a6439 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -8,27 +8,40 @@ const jsdoc = require('./jsdoc'); const hasOwnProperty = utils.hasOwnProperty; -const DEFAULT_OPTIONS = { - /** - * Flag, indicating that source locations should be extracted from source. - */ - includeSourceLocations: false, - range: false, - // comment: true, - attachComment: true, - - // create a top-level tokens array containing all tokens - tokens: true, - - // The version of ECMAScript syntax to use - ecmaVersion: 9, - - // Type of script to parse - sourceType: 'module', - - ecmaFeatures: { - } -}; +/** + * @link https://github.com/eslint/espree#options + */ +function getAstDefaultOptions() { + return { + /** attach range information to each node */ + range: true, + + /** attach line/column location information to each node */ + loc: true, + + /** create a top-level comments array containing all comments */ + comment: true, + + /** create a top-level tokens array containing all tokens */ + tokens: true, + + /** + * Set to 3, 5 (default), 6, 7, 8, 9, 10, 11, or 12 to specify + * the version of ECMAScript syntax you want to use. + * + * You can also set to 2015 (same as 6), 2016 (same as 7), + * 2017 (same as 8), 2018 (same as 9), 2019 (same as 10), + * 2020 (same as 11), or 2021 (same as 12) to use the year-based naming. + */ + ecmaVersion: 9, + + /** specify which type of script you're parsing ("script" or "module") */ + sourceType: 'module', + + /** specify additional language features */ + ecmaFeatures: {} + }; +} const SUPPORTED_FEATURES = [ 'name', @@ -46,25 +59,32 @@ const SUPPORTED_FEATURES = [ 'refs', 'store' ]; +const isFeatureSupported = (v) => SUPPORTED_FEATURES.includes(v); + +const INFO_FEATURES_SUPPORTED = `Supported features: ${JSON.stringify(SUPPORTED_FEATURES)}`; + +function getUnsupporteFeaturesString(arr) { + return `Features [${utils.printArray(arr)}] in ` + + 'options.features are not supported by the SvelteV2 Parser. ' + + INFO_FEATURES_SUPPORTED; +} const EVENT_EMIT_RE = /\bfire\s*\(\s*((?:'[^']*')|(?:"[^"]*")|(?:`[^`]*`))/; class Parser extends EventEmitter { constructor(options) { - options = Object.assign({}, DEFAULT_OPTIONS, options); + super(); Parser.validateOptions(options); - super(); - this.source = options.source; - this.features = options.features || SUPPORTED_FEATURES; + this.features = options.features; if (hasOwnProperty(options.source, 'script') && options.source.script) { this.scriptOffset = options.source.scriptOffset || 0; try { - this.ast = espree.parse(options.source.script, options); + this.ast = espree.parse(options.source.script, options.ast); this.sourceCode = new eslint.SourceCode({ text: options.source.script, @@ -73,11 +93,14 @@ class Parser extends EventEmitter { } catch (e) { const script = utils.escapeImportKeyword(options.source.script); - this.ast = espree.parse(script, Object.assign({}, options, { + const newAstOptions = { + ...options.ast, loc: true, range: true, comment: true - })); + }; + + this.ast = espree.parse(script, newAstOptions); this.sourceCode = new eslint.SourceCode({ text: script, @@ -91,31 +114,86 @@ class Parser extends EventEmitter { } this.includeSourceLocations = options.includeSourceLocations; - this.componentName = null; + this.componentName = options.componentName; this.template = options.source.template; this.filename = options.filename; - this.eventsEmmited = {}; + this.eventsEmmited = options.eventsEmmited; this.defaultMethodVisibility = options.defaultMethodVisibility; this.defaultActionVisibility = options.defaultActionVisibility; - this.identifiers = {}; - this.imports = {}; + this.identifiers = options.identifiers; + this.imports = options.imports; + } + + static getDefaultOptions() { + return { + includeSourceLocations: true, + defaultMethodVisibility: utils.DEFAULT_VISIBILITY, + defaultActionVisibility: utils.DEFAULT_VISIBILITY, + features: [...SUPPORTED_FEATURES], + componentName: null, + eventsEmmited: {}, + identifiers: {}, + imports: {}, + ast: getAstDefaultOptions(), + }; + } + + static normalizeOptions(options) { + const defaults = Parser.getDefaultOptions(); + + Object.keys(defaults).forEach((optionKey) => { + // If the key was not set by the user, apply default value. + if (!(optionKey in options)) { + options[optionKey] = defaults[optionKey]; + } + }); } static validateOptions(options) { - if (!options.source) { - throw new Error('options.source is required'); + Parser.normalizeOptions(options); + + // Check the presence and basic format of multiple options + for (const key of ['eventsEmmited', 'identifiers', 'imports']) { + const hasKey = (key in options); + + if (!hasKey) { + throw new Error(`options.${key} is required`); + } + + const hasCorrectType = typeof options[key] === 'object'; + + if (!hasCorrectType) { + throw new TypeError(`options.${key} must be of type 'object'`); + } + } + + if ('source' in options) { + ['script', 'scriptOffset'].forEach(key => { + if (!(key in options.source)) { + throw new TypeError('options.source must have keys \'script\' and \'scriptOffset\''); + } + }); } - if (options.features) { + if ('features' in options) { if (!Array.isArray(options.features)) { throw new TypeError('options.features must be an array'); } - options.features.forEach((feature) => { - if (!SUPPORTED_FEATURES.includes(feature)) { - throw new Error(`Unknow '${feature}' feature. Supported features: ` + JSON.stringify(SUPPORTED_FEATURES)); - } - }); + if (options.features.length === 0) { + throw new Error( + 'options.features must contain at least one feature. ' + + INFO_FEATURES_SUPPORTED + ); + } + + if (!options.features.every(isFeatureSupported)) { + const notSupported = options.features.filter( + (iv) => !isFeatureSupported(iv) + ); + + throw new Error(getUnsupporteFeaturesString(notSupported)); + } } } @@ -379,19 +457,15 @@ class Parser extends EventEmitter { } internalWalk() { - if (this.features.length === 0) { - return this.emit('end'); - } - if (this.template) { this.parseTemplate(); } - if (this.ast === null) { - if (this.features.includes('name')) { - this.parseComponentName(); - } + if (this.features.includes('name')) { + this.parseComponentName(); + } + if (this.ast === null) { return this.emit('end'); } @@ -450,10 +524,6 @@ class Parser extends EventEmitter { } else if (body.expression !== null && body.expression.right && body.expression.right.properties) { body.expression.right.properties.forEach((property) => this.extractProperties(property)); } - - if (this.features.includes('name')) { - this.parseComponentName(); - } }); this.emit('end');