Skip to content

Commit

Permalink
feat(prettier-plugin-jsdoc): add fn to format access tag
Browse files Browse the repository at this point in the history
  • Loading branch information
homer0 committed Oct 1, 2020
1 parent 0ed6123 commit 85d60b7
Show file tree
Hide file tree
Showing 6 changed files with 383 additions and 0 deletions.
23 changes: 23 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const TAGS_SYNONYMS = {
virtual: 'abstract',
extends: 'augments',
constructor: 'class',
const: 'constant',
defaultvalue: 'default',
desc: 'description',
host: 'external',
fileoverview: 'file',
overview: 'file',
emits: 'fires',
func: 'function',
method: 'function',
var: 'member',
arg: 'param',
argument: 'param',
prop: 'property',
return: 'returns',
exception: 'throws',
yield: 'yields',
};

module.exports.TAGS_SYNONYMS = TAGS_SYNONYMS;
115 changes: 115 additions & 0 deletions src/fns.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
const R = require('ramda');
const { TAGS_SYNONYMS } = require('./constants');
/**
* Replaces the tags synonyms for their "official" version.
*
* @param {CommentTag[]} tags The list of tags where the replacement should happen.
* @returns {CommentTag[]}
*/
const replaceTagsSynonyms = R.map((tag) => ({
...tag,
tag: R.propOr(tag.tag, tag.tag, TAGS_SYNONYMS),
}));
/**
* Ensures a given object is an array.
*
* @param {T|T[]} obj The object to validate.
* @returns {T[]}
* @template T
*/
const ensureArray = R.unless(R.is(Array), R.of);
/**
* Creates a reducer that finds the last index of a tag on a list and saves it on the
* accumulator using a custom property.
*
* @param {string|string[]} targetTag The name of the tag or tags the function should find.
* @param {string} propName The name of the property that will be used for the
* accumulator.
* @returns {Object.<string,number>}
*/
const findTagIndex = R.curry((targetTag, propName, step) => {
const targetTags = ensureArray(targetTag);
return (acc, tag, index) => {
const nextAcc = targetTags.includes(tag.tag) ?
R.assocPath([propName], index, acc) :
acc;
return step(nextAcc, tag, index);
};
});
/**
* Formats and normalizes the use of the access tag based on the plugin options.
*
* @param {CommentTag[]} tags The list of tags where the transformations should happen.
* @param {PJPAccessTagOptions} options The plugin options for the access tag.
* @return {CommentTag[]
*/
const formatAccessTag = (tags, options) => {
const indexes = tags.reduce(
R.compose(
findTagIndex('access', 'accessTag'),
findTagIndex(
['public', 'protected', 'private'],
'typeTag',
),
)(R.identity),
{
accessTag: -1,
typeTag: -1,
},
);

let result;
// If @access tags are allowed.
if (options.jsdocAllowAccessTag) {
// If @access tag needs to be enforced and there's a access type tag.
if (options.jsdocEnforceAccessTag && indexes.typeTag > -1) {
// If there's also an @access tag, just remove the access type tag.
if (indexes.accessTag > -1) {
result = R.remove(indexes.typeTag, 1, tags);
indexes.typeTag = -1;
} else {
// If there's a access type tag but no @access tag, replace the access type tag with one.
result = R.adjust(indexes.typeTag, (typeTag) => ({
tag: 'access',
type: '',
name: typeTag.tag,
description: '',
}), tags);
}
}
// If @access tags are not allowed but there's one.
} else if (indexes.accessTag > -1) {
// If there's also an access type tag, just remove the @access tag.
if (indexes.typeTag > -1) {
result = R.remove(indexes.accessTag, 1, tags);
indexes.accessTag = -1;
} else {
// If there's an @access tag but not access type tag, replace the @access tag with one.
result = R.adjust(indexes.accessTag, (accessTag) => ({
tag: accessTag.name,
type: '',
name: '',
description: '',
}), tags);
}
}

/**
* If for some reason, there's an access tag and an access type tag and the options allow them
* both, remove the first one declared.
*/
if (indexes.accessTag > -1 && indexes.typeTag > -1) {
const removeIndex = Math.min(indexes.accessTag, indexes.typeTag);
result = R.remove(removeIndex, 1, tags);
}

/**
* Return the modified object, if there's one, or just a clone of the tags list. The reason
* for the short circuit is so the implementation can always asume that the method returns
* a clone, no matter if no modification was made.
*/
return result || R.clone(tags);
};

module.exports.replaceTagsSynonyms = replaceTagsSynonyms;
module.exports.formatAccessTag = formatAccessTag;
20 changes: 20 additions & 0 deletions src/typedef.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,23 @@
* @typedef {PJPDescriptionTagOptions & PJPAccessTagOptions & PJPStringLiteralsOptions & PJPTypesOptions & PJPTagsOptions & PJPStyleOptions } PJPOptions
*/
/* eslint-enable max-len */

// =========================================
// Parser
// =========================================

/**
* @typedef {Object} CommentTag
* @property {string} tag The name of the tag.
* @property {string} type The type of what the tag represents, without the curly brackets.
* @property {string} name The name of what the tag represents.
* @property {string} description The description of what the tag represents.
* @property {boolean} optional Whether or not what the tag represents is optional.
* @property {string} [default] The default value of what the tag represents.
*/

/**
* @typedef {Object} CommentBlock
* @property {string} description The description on the body of the block.
* @property {CommentTag[]} tags The list of tags on the block.
*/
7 changes: 7 additions & 0 deletions test/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"root": true,
"plugins": ["@homer0"],
"extends": [
"plugin:@homer0/jest"
]
}
213 changes: 213 additions & 0 deletions test/fn.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
jest.unmock('../src/fns');
const {
replaceTagsSynonyms,
formatAccessTag,
} = require('../src/fns');

describe('functions', () => {
describe('replaceTagsSynonyms', () => {
it('should replace tags synonyms', () => {
// Given
const input = [
{
tag: 'virtual',
},
{
tag: 'param',
},
];
const output = [
{
tag: 'abstract',
},
{
tag: 'param',
},
];
let resultOne = null;
let resultTwo = null;
// When
resultOne = replaceTagsSynonyms(input);
resultTwo = replaceTagsSynonyms()(input);
// Then
expect(resultOne).toEqual(output);
expect(resultTwo).toEqual(output);
});
});

describe('formatAccessTag', () => {
it('should replace "@access public" with "@public"', () => {
// Given
const input = [
{
tag: 'access',
name: 'public',
},
{
tag: 'param',
},
];
const output = [
{
tag: 'public',
name: '',
description: '',
type: '',
},
{
tag: 'param',
},
];
let result = null;
// When
result = formatAccessTag(input, {
jsdocAllowAccessTag: false,
});
// Then
expect(result).toEqual(output);
});

it('should only keep one type of "access" tag', () => {
// Given
const input = [
{
tag: 'access',
name: 'public',
},
{
tag: 'protected',
},
{
tag: 'param',
},
];
const output = [
{
tag: 'protected',
},
{
tag: 'param',
},
];
let result = null;
// When
result = formatAccessTag(input, {
jsdocAllowAccessTag: true,
});
// Then
expect(result).toEqual(output);
});

it('should replace "@private" with "@access private"', () => {
// Given
const input = [
{
tag: 'private',
},
{
tag: 'param',
},
];
const output = [
{
tag: 'access',
name: 'private',
type: '',
description: '',
},
{
tag: 'param',
},
];
let result = null;
// When
result = formatAccessTag(input, {
jsdocAllowAccessTag: true,
jsdocEnforceAccessTag: true,
});
// Then
expect(result).toEqual(output);
});

it('should remove "@protected" if there\'s "@access protected"', () => {
// Given
const input = [
{
tag: 'protected',
},
{
tag: 'access',
name: 'protected',
},
{
tag: 'param',
},
];
const output = [
{
tag: 'access',
name: 'protected',
},
{
tag: 'param',
},
];
let result = null;
// When
result = formatAccessTag(input, {
jsdocAllowAccessTag: true,
jsdocEnforceAccessTag: true,
});
// Then
expect(result).toEqual(output);
});

it('should remove "@access public" if there\'s "@public"', () => {
// Given
const input = [
{
tag: 'public',
},
{
tag: 'access',
name: 'public',
},
{
tag: 'param',
},
];
const output = [
{
tag: 'public',
},
{
tag: 'param',
},
];
let result = null;
// When
result = formatAccessTag(input, {
jsdocAllowAccessTag: false,
});
// Then
expect(result).toEqual(output);
});

it('shouldn\'t do anything if there are no "access tags"', () => {
// Given
const input = [{
tag: 'param',
}];
const output = [{
tag: 'param',
}];
let result = null;
// When
result = formatAccessTag(input, {
jsdocAllowAccessTag: false,
});
// Then
expect(result).toEqual(output);
});
});
});
5 changes: 5 additions & 0 deletions test/jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"typeAcquisition": {
"include": ["jest"]
}
}

0 comments on commit 85d60b7

Please sign in to comment.