Skip to content

Commit

Permalink
feat: add the extendTypes feature and its tests
Browse files Browse the repository at this point in the history
  • Loading branch information
homer0 committed Jul 13, 2020
1 parent 032a018 commit 01294a0
Show file tree
Hide file tree
Showing 3 changed files with 325 additions and 1 deletion.
178 changes: 178 additions & 0 deletions src/features/extendTypes.js
@@ -0,0 +1,178 @@
// @ts-check
/**
* @typedef {Object} ExtendTypesCommentWithProperties
* @property {string} name
* @property {string} augments
* @property {string} comment
* @property {number} linesCount
* @property {number} usedLines
* @property {string[]} lines
*/

class ExtendTypes {
/**
* @param {EventEmitter} events
* @param {EVENT_NAMES} EVENT_NAMES
*/
constructor(events, EVENT_NAMES) {
/**
* @type {string[]}
* @access protected
* @ignore
*/
this._commentsWithIntersections = [];
/**
* @type {ExtendTypesCommentWithProperties[]}
* @access protected
* @ignore
*/
this._commentsWithProperties = [];
/**
* @typedef {RegExp}
* @access protected
* @ignore
*/
this._intersectionExpression = /\*\s*@typedef\s+\{\s*\w+\s*&\s*\w+/i;
/**
* @typedef {RegExp}
* @access protected
* @ignore
*/
this._typeDefExpression = /\*\s*@typedef\s+\{/i;
/**
* @typedef {RegExp}
* @access protected
* @ignore
*/
this._extendsTypeExpression = /\*\s*@(?:augments|extends)\s+\w+/i;
events.on(EVENT_NAMES.newComment, this._readComment.bind(this));
events.on(EVENT_NAMES.commentsReady, this._replaceComments.bind(this));
}
/**
* @param {string} name
* @param {string[]} types
* @returns {?ExtendTypesCommentWithProperties}
*/
_getCommentWithProperties(name, types) {
const comments = types
.map((type) => this._commentsWithProperties.find((comment) => (
comment.name === type &&
comment.augments === name
)))
.filter((comment) => comment);
return comments.length === 1 ? comments[0] : null;
}
/**
* @param {string} comment
* @returns {ExtendTypesCommentWithProperties}
*/
_getCommentWithPropertiesInfo(comment) {
const [, name] = /@typedef\s+\{[^\}]+\}\s*(.*?)\s/i.exec(comment);
const [, augments] = /@(?:augments|extends)\s+(.*?)\s/i.exec(comment);
const allLines = comment.split('\n');
const linesCount = allLines.length;
const lines = allLines.filter((line) => (
line.match(/\w/) &&
!line.match(/^\s*\*\s*@(?:typedef|augments|extends)\s+/i)
));
return {
name: name.trim(),
augments: augments.trim(),
comment,
linesCount,
usedLines: 0,
lines,
};
}
/**
* @param {string} comment
*/
_readComment(comment) {
if (comment.match(this._intersectionExpression)) {
this._commentsWithIntersections.push(comment);
} else if (
comment.match(this._typeDefExpression) &&
comment.match(this._extendsTypeExpression)
) {
this._commentsWithProperties.push(this._getCommentWithPropertiesInfo(comment));
}
}
/**
* @param {string} source
* @returns {string}
*/
_removeCommentsWithProperties(source) {
return this._commentsWithProperties.reduce(
(acc, comment) => acc.replace(
comment.comment,
new Array(comment.linesCount - comment.usedLines).fill('').join('\n'),
),
source,
);
}
/**
* @param {string} source
* @returns {string}
*/
_replaceComments(source) {
let result = this._replaceDefinitions(source);
result = this._removeCommentsWithProperties(result);
this._commentsWithIntersections = [];
this._commentsWithProperties = [];
return result;
}
/**
* @param {string} source
* @returns {string}
*/
_replaceDefinitions(source) {
return this._commentsWithIntersections.reduce(
(acc, comment) => {
const [typedefLine, rawTypes, name] = /@typedef\s*\{([^\}]+)\}\s*(.*?)\s/i.exec(comment);
const types = rawTypes
.split('&')
.map((type) => type.trim());

let replacement;
const commentWithProps = this._getCommentWithProperties(name, types);
if (commentWithProps) {
const [baseType] = types.filter((type) => type !== commentWithProps.name);
const newTypedefLine = typedefLine.replace(rawTypes, baseType);
const newComment = comment.replace(typedefLine, newTypedefLine);
const lines = newComment.split('\n');
const closingLine = lines.pop();
const info = commentWithProps.lines.reduce(
(infoAcc, line) => {
let nextInfoAcc;
if (infoAcc.lines.includes(line)) {
nextInfoAcc = infoAcc;
} else {
nextInfoAcc = {
lines: [...infoAcc.lines, line],
count: infoAcc.count + 1,
};
}

return nextInfoAcc;
},
{
lines,
count: commentWithProps.usedLines,
},
);
info.lines.push(closingLine);
replacement = info.lines.join('\n');
commentWithProps.usedLines = info.count;
} else {
const newTypedefLine = typedefLine.replace(rawTypes, types.join('|'));
replacement = comment.replace(typedefLine, newTypedefLine);
}

return acc.replace(comment, replacement);
},
source,
);
}
}

module.exports.ExtendTypes = ExtendTypes;
2 changes: 1 addition & 1 deletion src/plugin.js
Expand Up @@ -11,7 +11,7 @@ const features = require('./features');
*/
const options = {
typedefImports: true,
intersections: true,
extendTypes: true,
modulesOnMemberOf: true,
modulesTypesShortName: true,
typeScriptUtilityTypes: true,
Expand Down
146 changes: 146 additions & 0 deletions tests/features/extendTypes.test.js
@@ -0,0 +1,146 @@
jest.unmock('../../src/features/extendTypes');
const { ExtendTypes } = require('../../src/features/extendTypes');
const { EVENT_NAMES } = require('../../src/constants');

describe('features:extendTypes', () => {
it('should register the listeners when instantiated', () => {
// Given
const events = {
on: jest.fn(),
};
let sut = null;
// When
sut = new ExtendTypes(events, EVENT_NAMES);
// Then
expect(sut).toBeInstanceOf(ExtendTypes);
expect(events.on).toHaveBeenCalledTimes(2);
expect(events.on).toHaveBeenCalledWith(EVENT_NAMES.newComment, expect.any(Function));
expect(events.on).toHaveBeenCalledWith(EVENT_NAMES.commentsReady, expect.any(Function));
});

it('should ignore comments that don\'t extend types nor use intersection', () => {
// Given
const comment = [
'/**',
' * @typedef {Daughter} Rosario',
' * @typedef {Daughter} Pilar',
' */',
].join('\n');
const source = `${comment} Something`;
const events = {
on: jest.fn(),
};
let sut = null;
let onComment = null;
let onCommentsReady = null;
let result = null;
// When
sut = new ExtendTypes(events, EVENT_NAMES);
[[, onComment], [, onCommentsReady]] = events.on.mock.calls;
onComment(comment);
result = onCommentsReady(source);
// Then
expect(sut).toBeInstanceOf(ExtendTypes); // to avoid `no-new`.
expect(result).toBe(source);
});

it('should transform an itersection into a union', () => {
// Given
const firstType = 'Object';
const secondType = 'SomeOtherType';
const definitionName = 'Child';
const comment = [
'/**',
` * @typedef {${firstType} & ${secondType}} ${definitionName}`,
' */',
].join('\n');
const content = ' Some other code';
const source = `${comment}${content}`;
const events = {
on: jest.fn(),
};
let sut = null;
let onComment = null;
let onCommentsReady = null;
let result = null;
const newComment = [
'/**',
` * @typedef {${firstType}|${secondType}} ${definitionName}`,
' */',
].join('\n');
// When
sut = new ExtendTypes(events, EVENT_NAMES);
[[, onComment], [, onCommentsReady]] = events.on.mock.calls;
onComment(comment);
result = onCommentsReady(source);
// Then
expect(sut).toBeInstanceOf(ExtendTypes); // to avoid `no-new`.
expect(result).toBe(`${newComment}${content}`);
});

it('should transform an intersection into an extension', () => {
// Given
const sharedLines = [
' * @memberof module:people',
];
const extendedProperiesLines = [
' * @property {number} name',
' * @property {number} age',
' * @property {number} height',
];
const extendedType = 'Human';
const baseType = 'Entity';
const comment = [
'/**',
` * @typedef {${baseType} & ${extendedType}Properties} ${extendedType}`,
...sharedLines,
' */',
].join('\n');
const propertiesLines = [
'/**',
` * @typedef {Object} ${extendedType}Properties`,
...extendedProperiesLines,
...sharedLines,
` * @augments ${extendedType}`,
' */',
];
const propertiesComment = propertiesLines.join('\n');
const content = 'Some other code';
const source = [
comment,
propertiesComment,
content,
].join('\n');
const events = {
on: jest.fn(),
};
let sut = null;
let onComment = null;
let onCommentsReady = null;
let result = null;
const newComment = [
'/**',
` * @typedef {${baseType}} ${extendedType}`,
...sharedLines,
...extendedProperiesLines,
' */',
].join('\n');
const emptyBlock = new Array(propertiesLines.length - extendedProperiesLines.length)
.fill('')
.join('\n');
const expectedResult = [
newComment,
emptyBlock,
content,
].join('\n');
// When
sut = new ExtendTypes(events, EVENT_NAMES);
[[, onComment], [, onCommentsReady]] = events.on.mock.calls;
onComment(comment);
onComment(propertiesComment);
result = onCommentsReady(source);
// Then
expect(sut).toBeInstanceOf(ExtendTypes); // to avoid `no-new`.
expect(result).toBe(expectedResult);
});
});

0 comments on commit 01294a0

Please sign in to comment.