Skip to content

Commit

Permalink
Merge pull request KatChaotic#45 from soft-decay/feature/validate-int…
Browse files Browse the repository at this point in the history
…ernal-parsers-options

feat(options): Improve validation for parsers internal options (KatChaotic#29)
  • Loading branch information
alexprey authored Dec 21, 2020
2 parents 3bcb993 + 40d2a1a commit e1b80a0
Show file tree
Hide file tree
Showing 5 changed files with 375 additions and 149 deletions.
141 changes: 121 additions & 20 deletions lib/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const { isString, printArray, isVisibilitySupported, VISIBILITIES } = require('.

/**
* @typedef {import("../typings").SvelteParserOptions} SvelteParserOptions
* @typedef {import("../typings").JSVisibilityScope} JSVisibilityScope
*/

/** @type {BufferEncoding[]} */
Expand Down Expand Up @@ -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.',
Expand All @@ -43,19 +44,22 @@ 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} */
function getDefaultOptions() {
return {
encoding: DEFAULT_ENCODING,
ignoredVisibilities: [...DEFAULT_IGNORED_VISIBILITIES],
includeSourceLocations: false,
};
}

Expand All @@ -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.
Expand All @@ -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) &&
Expand All @@ -110,42 +113,140 @@ 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)) {
const notSupported = options.ignoredVisibilities.filter(
(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,
};
Loading

0 comments on commit e1b80a0

Please sign in to comment.