Skip to content

Commit

Permalink
feat(validation): Validate options passed to the parse function
Browse files Browse the repository at this point in the history
- Validate input
- Validate encoding
- Validate ignoredVisibilities
- Add typings for parse options
  • Loading branch information
soft-decay committed Nov 30, 2020
1 parent afb6115 commit 2e959ff
Show file tree
Hide file tree
Showing 3 changed files with 236 additions and 19 deletions.
95 changes: 92 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,87 @@
const { loadFileStructureFromOptions } = require('./lib/helpers');
const SvelteVersionDetector = require('./lib/detector');

const { isString, printArray, isVisibilitySupported } = require('./lib/utils');

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

/** @type {BufferEncoding[]} */
const ENCODINGS = [
'ascii',
'utf8',
'utf-8',
'utf16le',
'ucs2',
'ucs-2',
'base64',
'latin1',
'binary',
'hex'
];

/** @type {SymbolVisibility[]} */
const VISIBILITIES = ['public', 'protected', 'private'];

/** @type {BufferEncoding} */
const DEFAULT_ENCODING = 'utf8';

/** @type {SymbolVisibility[]} */
const DEFAULT_IGNORED_VISIBILITIES = ['protected', 'private'];

const ERROR_OPTIONS_REQUIRED = 'An options object is required.';
const ERROR_INPUT_REQUIRED = 'One of options.filename or options.fileContent is required.';
const ERROR_ENCODING_NOT_SUPPORTED =
'options.encoding must be one of: ' + printArray(ENCODINGS);

const ERROR_IGNORED_VISIBILITIES_FORMAT =
'options.ignoredVisibilities must be an array of those strings: ' +
printArray(VISIBILITIES);

const ERROR_IGNORED_VISIBILITIES_NOT_SUPPORTED =
`options.ignoredVisibilities expected any of [${printArray(VISIBILITIES)}] ` +
'but found these instead: ';

/**
* @param {SvelteParserOptions} options
* @throws an error if any option is invalid
*/
function validateOptions(options) {
if (!options || (!options.filename && !options.fileContent)) {
throw new Error('One of options.filename or options.filecontent is required');
if (!options) {
throw new Error(ERROR_OPTIONS_REQUIRED);
}

if (!isString(options.filename) && !isString(options.fileContent)) {
throw new Error(ERROR_INPUT_REQUIRED);
}

if (options.encoding && !ENCODINGS.includes(options.encoding)) {
throw new Error(ERROR_ENCODING_NOT_SUPPORTED);
}

if (options.ignoredVisibilities) {
if (!Array.isArray(options.ignoredVisibilities)) {
throw new Error(ERROR_IGNORED_VISIBILITIES_FORMAT);
}

if (!options.ignoredVisibilities.every(isVisibilitySupported)) {
const notSupported = options.ignoredVisibilities.filter(
(iv) => !isVisibilitySupported(iv)
);

throw new Error(
ERROR_IGNORED_VISIBILITIES_NOT_SUPPORTED +
printArray(notSupported)
);
}
}
}

/**
* Applies default values to options.
* @param {SvelteParserOptions} options
*/
function normalizeOptions(options) {
options.encoding = options.encoding || DEFAULT_ENCODING;
options.ignoredVisibilities = options.ignoredVisibilities || DEFAULT_IGNORED_VISIBILITIES;
Expand Down Expand Up @@ -179,6 +251,20 @@ function subscribeOnParserEvents(parser, options, version, resolve, reject) {
});
}

/**
* Main parse function.
* @param {SvelteParserOptions} options
* @example
* const { parse } = require('sveltedoc-parser');
* const doc = await parse({
* filename: 'main.svelte',
* encoding: 'ascii',
* features: ['data', 'computed', 'methods'],
* ignoredVisibilities: ['private'],
* includeSourceLocations: true,
* version: 3
* });
*/
module.exports.parse = (options) => new Promise((resolve, reject) => {
try {
validateOptions(options);
Expand All @@ -198,6 +284,9 @@ module.exports.parse = (options) => new Promise((resolve, reject) => {
}
});

/**
* @param {SvelteParserOptions} options
*/
module.exports.detectVersion = (options) => {
validateOptions(options);

Expand Down
14 changes: 12 additions & 2 deletions lib/utils.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
const RE_VISIBILITY = /(public|protected|private)/;
const VISIBILITIES = ['public', 'protected', 'private'];
const RE_VISIBILITY = new RegExp(`^(${VISIBILITIES.join('|')})$`);
const RE_KEYWORDS = /@\**\s*([a-z0-9_-]+)(\s+(-\s+)?([\wÀ-ÿ\s*{}[\]()='"`_^$#&²~|\\£¤%µ,?;.:/!§<>+¨-]+))?/ig;

const DEFAULT_VISIBILITY = 'public';

const isVisibilitySupported = (v) => RE_VISIBILITY.test(v);

const getVisibility = (keywords, defaultVisibility) => {
const keyword = keywords.find((keyword) => RE_VISIBILITY.test(keyword.name));
const keyword = keywords.find((keyword) => isVisibilitySupported(keyword.name));

if (keyword) {
return keyword.name;
Expand Down Expand Up @@ -284,7 +287,12 @@ const isTopLevelComment = (comment) => {
return comment.keywords.some((keyword) => keyword.name === 'component');
};

const isString = (x) => typeof x === 'string' || x instanceof String;

const printArray = (array) => array.map(s => `'${s}'`).join(', ');

module.exports.DEFAULT_VISIBILITY = DEFAULT_VISIBILITY;
module.exports.isVisibilitySupported = isVisibilitySupported;
module.exports.getVisibility = getVisibility;
module.exports.parseComment = parseComment;
module.exports.getCommentFromSourceCode = getCommentFromSourceCode;
Expand All @@ -300,3 +308,5 @@ module.exports.escapeImportKeyword = escapeImportKeyword;
module.exports.inferTypeFromVariableDeclaration = inferTypeFromVariableDeclaration;
module.exports.isTopLevelComment = isTopLevelComment;
module.exports.buildCamelCase = buildCamelCase;
module.exports.isString = isString;
module.exports.printArray = printArray;
146 changes: 132 additions & 14 deletions typings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export interface JSDocType {
/**
* Kind of this type.
*/
kind: 'type'|'union'|'const',
kind: 'type' | 'union' | 'const',
/**
* The text representation of this type.
*/
Expand All @@ -28,7 +28,7 @@ export interface JSDocType {
* @see `'type'|'const'` in `kind` field, then this field provide the name of JS type.
* @see `'union'` in `kind` field, then this field provide the list of @see JSDocType
*/
type: string|JSDocType[],
type: string | JSDocType[],
/**
* The constant value related to this type, if can be provided.
*/
Expand All @@ -50,7 +50,7 @@ export interface JSDocTypeElement extends JSDocType {
/**
* Kind of this type.
*/
kind: 'type'|'const',
kind: 'type' | 'const',
/**
* The type representation of this item.
*/
Expand Down Expand Up @@ -87,11 +87,11 @@ export interface ISvelteItem {
/**
* The description of the item, provided from related comment.
*/
description?: string|null;
description?: string | null;
/**
* The visibility of item.
*/
visibility?: 'public'|'protected'|'private';
visibility?: 'public' | 'protected' | 'private';
/**
* The list of parsed JSDoc keywords from related comment.
*/
Expand Down Expand Up @@ -120,7 +120,7 @@ export interface SvelteDataItem extends ISvelteItem {
* @since Svelte V3
* @since {2.0.0}
*/
kind?: 'var'|'let'|'const';
kind?: 'var' | 'let' | 'const';
/**
* Provides information about property binding.
* @since Svelte V3
Expand Down Expand Up @@ -179,7 +179,7 @@ export interface SvelteComputedItem extends ISvelteItem {
/**
* The list of data or computed properties names, marked as depended to this property.
*/
dependencies: string[]
dependencies: string[]
}

export interface SvelteMethodParamItem {
Expand Down Expand Up @@ -259,13 +259,13 @@ export interface SvelteComponentItem extends ISvelteItem {
* @since Svelte V3
* @since {2.0.0}
*/
export type SvelteEventModificator = 'preventDefault'|'stopPropagation'|'passive'|'capture'|'once';
export type SvelteEventModificator = 'preventDefault' | 'stopPropagation' | 'passive' | 'capture' | 'once';

export interface SvelteEventItem extends ISvelteItem {
/**
* The name of HTML element if propagated standart JS Dom event or null.
*/
parent?: string|null;
parent?: string | null;

/**
* The list of event modificators.
Expand Down Expand Up @@ -295,7 +295,7 @@ export interface SvelteRefItem extends ISvelteItem {
/**
* The name of HTML element or component that binded with this ref name.
*/
parent?: string|null;
parent?: string | null;
}

/**
Expand All @@ -305,15 +305,15 @@ export interface SvelteComponentDoc {
/**
* The name of the parsed component.
*/
name?: string|null;
name?: string | null;
/**
* The Svelte compiler version that used for this document.
*/
version?: number,
/**
* The component description.
*/
description?: string|null;
description?: string | null;

/**
* The list of defined model properties.
Expand All @@ -323,7 +323,7 @@ export interface SvelteComponentDoc {
* The list of defined computed properties of component.
*/
computed?: SvelteComputedItem[];

/**
* The list of included components.
*/
Expand Down Expand Up @@ -364,4 +364,122 @@ export interface SvelteComponentDoc {
* @since {2.1.0}
*/
dispatchers?: SvelteMethodItem[];
}
}

/**
* Features supported by the Svelte 2 parser.
*/
export enum Svelte2Feature {
name = 'name',
data = 'data',
computed = 'computed',
methods = 'methods',
actions = 'actions',
helpers = 'helpers',
components = 'components',
description = 'description',
keywords = 'keywords',
events = 'events',
slots = 'slots',
transitions = 'transitions',
refs = 'refs',
store = 'store',
};

/**
* Features supported by the Svelte 3 parser.
*/
export enum Svelte3Feature {
name = 'name',
data = 'data',
computed = 'computed',
methods = 'methods',
components = 'components',
description = 'description',
keywords = 'keywords',
events = 'events',
slots = 'slots',
refs = 'refs',
};

type Svelte2FeatureKeys = keyof typeof Svelte2Feature;
type Svelte3FeatureKeys = keyof typeof Svelte3Feature;
type SvelteFeatureKeys = Svelte2FeatureKeys & Svelte3FeatureKeys;
type Svelte2ExclusiveFeature = Exclude<Svelte2FeatureKeys, Svelte3FeatureKeys>;
type Svelte3ExclusiveFeature = Exclude<Svelte3FeatureKeys, Svelte2FeatureKeys>;

/**
* Visibility of a Svelte item.
*/
export type SymbolVisibility = 'private' | 'protected' | 'public';

/**
* Supported Svelte versions.
*/
export type SvelteVersion = 2 | 3;

export interface ParserOptions<V extends SvelteVersion, F extends SvelteFeatureKeys> {
/**
* The filename to parse. Required, unless fileContent is passed.
*/
filename?: string;

/**
* The file content to parse. Required, unless filename is passed.
*/
fileContent?: string;

/**
* @default 'utf8'
*/
encoding?: BufferEncoding;

/**
* The component features to parse and extract.
* Uses all supported features by default.
* @see Svelte2Feature
* @see Svelte3Feature
*/
features?: F[];

/**
* The list of ignored visibilities. Use an empty array to export all
* visibilities.
* @default ['private','protected']
*/
ignoredVisibilities?: SymbolVisibility[];

/**
* Indicates that source locations should be provided for component symbols.
* @default false
*/
includeSourceLocations?: boolean

/**
* Optional. Use 2 or 3 to specify which svelte syntax should be used.
* When version is not provided, the parser tries to detect which version
* of the syntax to use.
*/
version?: V;

/**
* Optional. Specify which version of svelte syntax to fallback to if the
* parser can't identify the version used.
*/
defaultVersion?: V;
}

/**
* Options to pass to the main parse function.
*
* @example
* const options = {
* filename: 'main.svelte',
* encoding: 'ascii',
* features: ['data', 'computed', 'methods'],
* ignoredVisibilities: ['private'],
* includeSourceLocations: true,
* version: 3
* });
*/
export type SvelteParserOptions = ParserOptions<3, Svelte3FeatureKeys> | ParserOptions<2, Svelte2FeatureKeys>;

0 comments on commit 2e959ff

Please sign in to comment.