diff --git a/lib/options.js b/lib/options.js index dbbe408..2731b56 100644 --- a/lib/options.js +++ b/lib/options.js @@ -2,6 +2,7 @@ const { isString, printArray, isVisibilitySupported, VISIBILITIES } = require('. /** * @typedef {import("../typings").SvelteParserOptions} SvelteParserOptions + * @typedef {import("../typings").JSVisibilityScope} JSVisibilityScope */ /** @type {BufferEncoding[]} */ @@ -34,7 +35,7 @@ function getUnsupportedVisibilitiesString(arr) { INFO_VISIBILITIES_SUPPORTED; } -const ErrorMessage = Object.freeze({ +const OptionsError = Object.freeze({ OptionsRequired: 'An options object is required.', InputRequired: 'One of options.filename or options.fileContent is required.', EncodingMissing: 'Internal Error: options.encoding is not set.', @@ -43,12 +44,14 @@ const ErrorMessage = Object.freeze({ IgnoredVisibilitiesMissing: 'Internal Error: options.ignoredVisibilities is not set.', IgnoredVisibilitiesFormat: ERROR_VISIBILITIES_FORMAT + INFO_VISIBILITIES_SUPPORTED, IgnoredVisibilitiesNotSupported: (arr) => getUnsupportedVisibilitiesString(arr), + IncludeSourceLocationsMissing: 'Internal Error: options.includeSourceLocationsMissing is not set.', + IncludeSourceLocationsFormat: 'Expected options.includeSourceLocations to be a boolean.', }); /** @type {BufferEncoding} */ const DEFAULT_ENCODING = 'utf8'; -/** @type {SymbolVisibility[]} */ +/** @type {JSVisibilityScope[]} */ const DEFAULT_IGNORED_VISIBILITIES = ['protected', 'private']; /** @returns {SvelteParserOptions} */ @@ -56,6 +59,7 @@ function getDefaultOptions() { return { encoding: DEFAULT_ENCODING, ignoredVisibilities: [...DEFAULT_IGNORED_VISIBILITIES], + includeSourceLocations: false, }; } @@ -70,11 +74,10 @@ function retrieveFileOptions(options) { /** * Applies default values to options. - * @param {SvelteParserOptions} options + * @param {SvelteParserOptions} options object to normalize (mutated) + * @param {SvelteParserOptions} defaults default values to normalize 'options' */ -function normalize(options) { - const defaults = getDefaultOptions(); - +function normalize(options, defaults) { Object.keys(defaults).forEach((optionKey) => { /** * If the key was not set by the user, apply default value. @@ -94,10 +97,10 @@ function normalize(options) { */ function validate(options) { if (!options) { - throw new Error(ErrorMessage.OptionsRequired); + throw new Error(OptionsError.OptionsRequired); } - normalize(options); + normalize(options, getDefaultOptions()); const hasFilename = ('filename' in options) && @@ -110,25 +113,25 @@ function validate(options) { isString(options.fileContent); if (!hasFilename && !hasFileContent) { - throw new Error(ErrorMessage.InputRequired); + throw new Error(OptionsError.InputRequired); } if ('encoding' in options) { if (!isString(options.encoding)) { - throw new Error(ErrorMessage.EncodingFormat); + throw new Error(OptionsError.EncodingFormat); } if (!ENCODINGS.includes(options.encoding)) { - throw new Error(ErrorMessage.EncodingNotSupported(options.encoding)); + throw new Error(OptionsError.EncodingNotSupported(options.encoding)); } } else { - // Sanity check. At this point, 'encoding' should be set. - throw new Error(ErrorMessage.EncodingMissing); + // Sanity check. At this point, 'encoding' must be set. + throw new Error(OptionsError.EncodingMissing); } if ('ignoredVisibilities' in options) { if (!Array.isArray(options.ignoredVisibilities)) { - throw new Error(ErrorMessage.IgnoredVisibilitiesFormat); + throw new Error(OptionsError.IgnoredVisibilitiesFormat); } if (!options.ignoredVisibilities.every(isVisibilitySupported)) { @@ -136,16 +139,114 @@ function validate(options) { (iv) => !isVisibilitySupported(iv) ); - throw new Error(ErrorMessage.IgnoredVisibilitiesNotSupported(notSupported)); + throw new Error(OptionsError.IgnoredVisibilitiesNotSupported(notSupported)); + } + } else { + // Sanity check. At this point, 'ignoredVisibilities' must be set. + throw new Error(OptionsError.IgnoredVisibilitiesMissing); + } + + if ('includeSourceLocations' in options) { + if (typeof options.includeSourceLocations !== 'boolean') { + throw new TypeError(OptionsError.IncludeSourceLocationsFormat); + } + } else { + // Sanity check. At this point, 'includeSourceLocations' must be set. + throw new Error(OptionsError.IncludeSourceLocationsMissing); + } +} + +const getSupportedFeaturesString = (supported) => `Supported features: ${printArray(supported)}`; + +const getFeaturesEmptyString = (supported) => { + return 'options.features must contain at least one feature. ' + + getSupportedFeaturesString(supported); +}; + +/** + * @param {string[]} notSupported + * @param {string[]} supported + */ +const getFeaturesNotSupportedString = (notSupported, supported) => { + return `Features [${printArray(notSupported)}] in ` + + 'options.features are not supported by this Parser. ' + + getSupportedFeaturesString(supported); +}; + +const ParserError = { + FeaturesMissing: 'Internal Error: options.features is not set.', + FeaturesFormat: 'options.features must be an array', + FeaturesEmpty: getFeaturesEmptyString, + FeaturesNotSupported: getFeaturesNotSupportedString, +}; + +/** + * + * @param {SvelteParserOptions} options + * @param {string[]} supported + * @throws if any validation fails for options.features + */ +function validateFeatures(options, supported) { + if ('features' in options) { + if (!Array.isArray(options.features)) { + throw new TypeError(ParserError.FeaturesFormat); + } + + if (options.features.length === 0) { + throw new Error(ParserError.FeaturesEmpty(supported)); + } + + const notSupported = options.features.filter((iv) => !supported.includes(iv)); + + if (notSupported.length > 0) { + throw new Error(ParserError.FeaturesNotSupported(notSupported, supported)); } } else { - // Sanity check. At this point, 'ignoredVisibilities' should be set. - throw new Error(ErrorMessage.IgnoredVisibilitiesMissing); + throw new Error(ParserError.FeaturesMissing); } } +/** + * @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: {} + }; +} + module.exports = { - ErrorMessage: ErrorMessage, - validate: validate, - retrieveFileOptions: retrieveFileOptions, + OptionsError, + ParserError, + normalize, + validate, + validateFeatures, + retrieveFileOptions, + getAstDefaultOptions, }; diff --git a/lib/parser.js b/lib/parser.js index 6137d8d..b578f93 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -5,31 +5,14 @@ const HtmlParser = require('htmlparser2-svelte').Parser; const path = require('path'); const utils = require('./utils'); const jsdoc = require('./jsdoc'); +const { + normalize: normalizeOptions, + validateFeatures, + getAstDefaultOptions +} = require('./options'); 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: { - } -}; - const SUPPORTED_FEATURES = [ 'name', 'data', @@ -49,71 +32,88 @@ const SUPPORTED_FEATURES = [ const EVENT_EMIT_RE = /\bfire\s*\(\s*((?:'[^']*')|(?:"[^"]*")|(?:`[^`]*`))/; -class Parser extends EventEmitter { - constructor(options) { - options = Object.assign({}, DEFAULT_OPTIONS, options); +function generateSourceCode(options) { + const generated = { + ast: null, + sourceCode: null, + scriptOffset: 0, + }; - Parser.validateOptions(options); + if (!(hasOwnProperty(options.source, 'script') && options.source.script)) { + return generated; + } - super(); + try { + generated.ast = espree.parse( + options.source.script, + getAstDefaultOptions() + ); - this.source = options.source; - this.features = options.features || SUPPORTED_FEATURES; + generated.sourceCode = new eslint.SourceCode({ + text: options.source.script, + ast: generated.ast + }); + } catch (e) { + const script = utils.escapeImportKeyword(options.source.script); - if (hasOwnProperty(options.source, 'script') && options.source.script) { - this.scriptOffset = options.source.scriptOffset || 0; + generated.ast = espree.parse(script, getAstDefaultOptions()); - try { - this.ast = espree.parse(options.source.script, options); + generated.sourceCode = new eslint.SourceCode({ + text: script, + ast: generated.ast + }); + } - this.sourceCode = new eslint.SourceCode({ - text: options.source.script, - ast: this.ast - }); - } catch (e) { - const script = utils.escapeImportKeyword(options.source.script); - - this.ast = espree.parse(script, Object.assign({}, options, { - loc: true, - range: true, - comment: true - })); - - this.sourceCode = new eslint.SourceCode({ - text: script, - ast: this.ast - }); - } - } else { - this.scriptOffset = 0; - this.ast = null; - this.sourceCode = null; - } + generated.scriptOffset = options.source.scriptOffset || 0; + + return generated; +} +class Parser extends EventEmitter { + constructor(options) { + super(); + + Parser.validateOptions(options); + + // External options + this.filename = options.filename; + this.source = options.source; + this.template = options.source.template; + this.features = options.features; this.includeSourceLocations = options.includeSourceLocations; + + // Internal Properties + this.defaultMethodVisibility = utils.DEFAULT_VISIBILITY; + this.defaultActionVisibility = utils.DEFAULT_VISIBILITY; this.componentName = null; - this.template = options.source.template; - this.filename = options.filename; - this.eventsEmmited = {}; - this.defaultMethodVisibility = options.defaultMethodVisibility; - this.defaultActionVisibility = options.defaultActionVisibility; + this.eventsEmitted = {}; this.identifiers = {}; this.imports = {}; + + // Generated Espree AST + const { ast, sourceCode, scriptOffset } = generateSourceCode(options); + + this.ast = ast; + this.sourceCode = sourceCode; + this.scriptOffset = scriptOffset; + } + + static getDefaultOptions() { + return { + includeSourceLocations: true, + features: [...SUPPORTED_FEATURES] + }; } static validateOptions(options) { - if (!options.source) { - throw new Error('options.source is required'); - } + normalizeOptions(options, Parser.getDefaultOptions()); - if (options.features) { - if (!Array.isArray(options.features)) { - throw new TypeError('options.features must be an array'); - } + validateFeatures(options, SUPPORTED_FEATURES); - options.features.forEach((feature) => { - if (!SUPPORTED_FEATURES.includes(feature)) { - throw new Error(`Unknow '${feature}' feature. Supported features: ` + JSON.stringify(SUPPORTED_FEATURES)); + if ('source' in options) { + ['script', 'scriptOffset'].forEach(key => { + if (!(key in options.source)) { + throw new TypeError('options.source must have keys \'script\' and \'scriptOffset\''); } }); } @@ -198,13 +198,13 @@ class Parser extends EventEmitter { keywords: entry.keywords }; - if (hasOwnProperty(this.eventsEmmited, event.name)) { - const emitedEvent = this.eventsEmmited[event.name]; + if (hasOwnProperty(this.eventsEmitted, event.name)) { + const emitedEvent = this.eventsEmitted[event.name]; if (emitedEvent.parent) { event.visibility = 'public'; - this.eventsEmmited[event.name] = event; + this.eventsEmitted[event.name] = event; this.parseKeywords(entry.keywords, event); this.emit('event', event); @@ -212,7 +212,7 @@ class Parser extends EventEmitter { // This event already defined } } else { - this.eventsEmmited[event.name] = event; + this.eventsEmitted[event.name] = event; this.parseKeywords(entry.keywords, event); this.emit('event', event); @@ -379,19 +379,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 +446,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'); @@ -547,15 +539,15 @@ class Parser extends EventEmitter { if (!event.name) { event.name = '****unhandled-event-name****'; } else { - if (hasOwnProperty(this.eventsEmmited, event.name)) { - const emitedEvent = this.eventsEmmited[event.name]; + if (hasOwnProperty(this.eventsEmitted, event.name)) { + const emitedEvent = this.eventsEmitted[event.name]; if (emitedEvent.visibility === 'public') { continue; } } - this.eventsEmmited[event.name] = event; + this.eventsEmitted[event.name] = event; } this.parseKeywords(event.keywords, event); @@ -699,8 +691,8 @@ class Parser extends EventEmitter { event.description = comment.description || ''; event.keywords = comment.keywords; - if (!hasOwnProperty(this.eventsEmmited, event.name)) { - this.eventsEmmited[event.name] = event; + if (!hasOwnProperty(this.eventsEmitted, event.name)) { + this.eventsEmitted[event.name] = event; this.parseKeywords(comment.keywords, event); this.emit('event', event); diff --git a/lib/v3/parser.js b/lib/v3/parser.js index 7f423b7..ddc3615 100644 --- a/lib/v3/parser.js +++ b/lib/v3/parser.js @@ -7,10 +7,13 @@ const HtmlParser = require('htmlparser2-svelte').Parser; const utils = require('./../utils'); const jsdoc = require('./../jsdoc'); +const { + normalize: normalizeOptions, + validateFeatures, + getAstDefaultOptions +} = require('../options'); -const hasOwnProperty = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop); - -const DEFAULT_OPTIONS = {}; +const hasOwnProperty = utils.hasOwnProperty; const SUPPORTED_FEATURES = [ 'name', @@ -24,7 +27,6 @@ const SUPPORTED_FEATURES = [ 'slots', 'refs' ]; - const SCOPE_DEFAULT = 'default'; const SCOPE_STATIC = 'static'; const SCOPE_MARKUP = 'markup'; @@ -43,19 +45,18 @@ class Parser extends EventEmitter { constructor(options) { super(); - this._options = Object.assign({}, DEFAULT_OPTIONS, options); + Parser.validateOptions(options); + // External options + this.filename = options.filename; this.structure = options.structure; - this.features = options.features || SUPPORTED_FEATURES; + this.features = options.features; this.includeSourceLocations = options.includeSourceLocations; + + // Internal properties this.componentName = null; - this.filename = options.filename; this.eventsEmitted = {}; - this.defaultMethodVisibility = options.defaultMethodVisibility; - this.defaultActionVisibility = options.defaultActionVisibility; - this.identifiers = {}; this.imports = {}; - this.dispatcherConstructorNames = []; this.dispatcherNames = []; } @@ -90,6 +91,19 @@ class Parser extends EventEmitter { this.emit('end'); } + static getDefaultOptions() { + return { + includeSourceLocations: true, + features: [...SUPPORTED_FEATURES], + }; + } + + static validateOptions(options) { + normalizeOptions(options, Parser.getDefaultOptions()); + + validateFeatures(options, SUPPORTED_FEATURES); + } + static getEventName(feature) { return feature.endsWith('s') ? feature.substring(0, feature.length - 1) @@ -531,23 +545,10 @@ class Parser extends EventEmitter { }); } - getAstParsingOptions() { - return { - tokens: true, - loc: true, - range: true, - ecmaVersion: 9, - sourceType: 'module', - comment: true, - ecmaFeatures: { - } - }; - } - parseScriptBlock(scriptBlock) { const ast = espree.parse( scriptBlock.content, - this.getAstParsingOptions() + getAstDefaultOptions() ); const sourceCode = new eslint.SourceCode({ @@ -583,7 +584,7 @@ class Parser extends EventEmitter { const ast = espree.parse( expression, - this.getAstParsingOptions() + getAstDefaultOptions() ); const sourceCode = new eslint.SourceCode({ diff --git a/test/integration/parse/parse.spec.js b/test/integration/parse/parse.spec.js index 46e67c2..274dab7 100644 --- a/test/integration/parse/parse.spec.js +++ b/test/integration/parse/parse.spec.js @@ -3,6 +3,14 @@ const chai = require('chai'); const expect = chai.expect; const parser = require('./../../../index'); +const { ParserError } = require('../../../lib/options'); +const { AssertionError } = require('chai'); +const { + SUPPORTED_FEATURES: V3_SUPPORTED_FEATURES +} = require('../../../lib/v3/parser'); +const { + SUPPORTED_FEATURES: V2_SUPPORTED_FEATURES +} = require('../../../lib/parser'); describe('parse - Integration', () => { it('should correctly auto-detect svelte V2 component', (done) => { @@ -23,7 +31,7 @@ describe('parse - Integration', () => { parser.parse({ filename: path.resolve(__dirname, 'basicV3.svelte'), }).then((doc) => { - expect(doc, 'Document should be provided').to.exist; + expect(doc, 'Document should exist').to.exist; // v3-parser converts component name to PascalCase expect(doc.name).to.equal('BasicV3'); @@ -32,4 +40,42 @@ describe('parse - Integration', () => { done(e); }); }); + + it('should throw when svelte V2 parser receives unsupported features', (done) => { + parser.parse({ + version: 2, + filename: path.resolve(__dirname, 'basicV2.svelte'), + features: ['data', 'unsupported'], + }).then(() => { + done(new AssertionError( + 'parser.parse should throw ParserError.FeaturesNotSupported' + )); + }).catch(e => { + expect(e.message).is.equal(ParserError.FeaturesNotSupported( + ['unsupported'], V2_SUPPORTED_FEATURES + )); + done(); + }).catch(e => { + done(e); + }); + }); + + it('should throw when svelte V3 parser receives unsupported features', (done) => { + parser.parse({ + version: 3, + filename: path.resolve(__dirname, 'basicV3.svelte'), + features: ['data', 'unsupported'], + }).then(() => { + done(new AssertionError( + 'parser.parse should throw ParserError.FeaturesNotSupported' + )); + }).catch(e => { + expect(e.message).is.equal(ParserError.FeaturesNotSupported( + ['unsupported'], V3_SUPPORTED_FEATURES + )); + done(); + }).catch(e => { + done(e); + }); + }); }); diff --git a/test/unit/options/options.spec.js b/test/unit/options/options.spec.js index 76d2f0a..ce00000 100644 --- a/test/unit/options/options.spec.js +++ b/test/unit/options/options.spec.js @@ -2,8 +2,10 @@ const expect = require('chai').expect; const { validate, + validateFeatures, retrieveFileOptions, - ErrorMessage + OptionsError, + ParserError } = require('../../../lib/options'); const baseOptions = { filename: 'empty.svelte' }; @@ -12,20 +14,20 @@ describe('Options Module', () => { describe('options.validate', () => { describe('Should throw when', () => { it('options object is missing', () => { - expect(() => validate()).to.throw(ErrorMessage.OptionsRequired); + expect(() => validate()).to.throw(OptionsError.OptionsRequired); }); it('input is missing, not a string, or an empty filename', () => { - expect(() => validate({})).to.throw(ErrorMessage.InputRequired); - expect(() => validate({ filename: {} })).to.throw(ErrorMessage.InputRequired); - expect(() => validate({ filename: '' })).to.throw(ErrorMessage.InputRequired); - expect(() => validate({ fileContent: {} })).to.throw(ErrorMessage.InputRequired); + expect(() => validate({})).to.throw(OptionsError.InputRequired); + expect(() => validate({ filename: {} })).to.throw(OptionsError.InputRequired); + expect(() => validate({ filename: '' })).to.throw(OptionsError.InputRequired); + expect(() => validate({ fileContent: {} })).to.throw(OptionsError.InputRequired); }); it('encoding is not a string', () => { const options = { ...baseOptions, encoding: true }; - expect(() => validate(options)).to.throw(ErrorMessage.EncodingFormat); + expect(() => validate(options)).to.throw(OptionsError.EncodingFormat); }); it('encoding is not supported', () => { @@ -33,7 +35,7 @@ describe('Options Module', () => { const options = { ...baseOptions, encoding: unsupported }; expect(() => validate(options)).to.throw( - ErrorMessage.EncodingNotSupported(unsupported) + OptionsError.EncodingNotSupported(unsupported) ); }); @@ -43,7 +45,7 @@ describe('Options Module', () => { const options = { ...baseOptions, ignoredVisibilities: mixed }; expect(() => validate(options)).to.throw( - ErrorMessage.IgnoredVisibilitiesNotSupported([unsupported]) + OptionsError.IgnoredVisibilitiesNotSupported([unsupported]) ); }); @@ -53,7 +55,15 @@ describe('Options Module', () => { const options = { ...baseOptions, ignoredVisibilities: mixed }; expect(() => validate(options)).to.throw( - ErrorMessage.IgnoredVisibilitiesNotSupported([unsupported]) + OptionsError.IgnoredVisibilitiesNotSupported([unsupported]) + ); + }); + + it('includeSourceLocations is not a boolean', () => { + const options = { ...baseOptions, includeSourceLocations: 'true' }; + + expect(() => validate(options)).to.throw( + OptionsError.IncludeSourceLocationsFormat ); }); }); @@ -85,6 +95,82 @@ describe('Options Module', () => { expect(() => validate(options)).to.not.throw(); }); + + it('includeSourceLocations is a boolean', () => { + const options1 = { ...baseOptions, includeSourceLocations: false }; + const options2 = { ...baseOptions, includeSourceLocations: true }; + + expect(() => validate(options1)).to.not.throw(); + expect(() => validate(options2)).to.not.throw(); + }); + }); + }); + + describe('options.validateFeatures', () => { + describe('Should pass when', () => { + it('only supported features are present', () => { + const supported = ['something', 'else']; + + const single = ['something']; + const options1 = { features: single }; + + expect(() => validateFeatures(options1, supported)).to.not.throw(); + + const all = ['else', 'something']; + const options2 = { features: all }; + + expect(() => validateFeatures(options2, supported)).to.not.throw(); + }); + }); + + describe('Should throw when', () => { + it('features is not an array', () => { + expect(() => validateFeatures({ features: {} }, [])) + .to.throw(ParserError.FeaturesFormat); + + expect(() => validateFeatures({ features: true }, [])) + .to.throw(ParserError.FeaturesFormat); + + expect(() => validateFeatures({ features: 'something' }, [])) + .to.throw(ParserError.FeaturesFormat); + }); + + it('features is an empty array', () => { + const supported = ['something', 'else']; + + expect(() => validateFeatures({ features: [] }, supported)) + .to.throw(ParserError.FeaturesEmpty(supported)); + }); + + it('one or more features are not supported', () => { + const supported = ['something', 'else']; + + const notSupported1 = ['other']; + const options1 = { features: notSupported1 }; + + expect(() => validateFeatures(options1, supported)).to.throw( + ParserError.FeaturesNotSupported(notSupported1, supported) + ); + + const notSupported2 = ['other', 'bad', 'trash']; + const options2 = { features: notSupported2 }; + + expect(() => validateFeatures(options2, supported)).to.throw( + ParserError.FeaturesNotSupported(notSupported2, supported) + ); + }); + + it('some features are not supported', () => { + const supported = ['something', 'else', 'stuff']; + const notSupported = ['other', 'thing']; + + const mixed = ['stuff', ...notSupported, 'something']; + const options2 = { features: mixed }; + + expect(() => validateFeatures(options2, supported)).to.throw( + ParserError.FeaturesNotSupported(notSupported, supported) + ); + }); }); });