Skip to content

Commit 29d82c2

Browse files
committed
refactor(nest): Better nesting implementation
This nesting implementation uses a proper recursive tree algorithm Fixes #554
1 parent d8ec5da commit 29d82c2

File tree

3 files changed

+130
-144
lines changed

3 files changed

+130
-144
lines changed

Diff for: declarations/comment.js

+29-14
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,21 @@ declare type CommentContextGitHub = {
5858
url: string
5959
};
6060

61-
declare type CommentTag = {
61+
declare type CommentTagBase = {
62+
title: string
63+
};
64+
65+
declare type CommentTag = CommentTagBase & {
66+
name?: string,
67+
title: string,
68+
description?: Object,
69+
default?: any,
70+
lineNumber?: number,
71+
type?: DoctrineType,
72+
properties?: Array<CommentTag>
73+
};
74+
75+
declare type CommentTagNamed = CommentTag & {
6276
name?: string,
6377
title: string,
6478
description?: Object,
@@ -88,18 +102,19 @@ declare type Remark = {
88102

89103
declare type Access = 'private' | 'public' | 'protected';
90104
declare type Scope = 'instance' | 'static' | 'inner' | 'global';
91-
declare type Kind = 'class' |
92-
'constant' |
93-
'event' |
94-
'external' |
95-
'file' |
96-
'function' |
97-
'member' |
98-
'mixin' |
99-
'module' |
100-
'namespace' |
101-
'typedef' |
102-
'interface';
105+
declare type Kind =
106+
| 'class'
107+
| 'constant'
108+
| 'event'
109+
| 'external'
110+
| 'file'
111+
| 'function'
112+
| 'member'
113+
| 'mixin'
114+
| 'module'
115+
| 'namespace'
116+
| 'typedef'
117+
| 'interface';
103118

104119
declare type Comment = {
105120
errors: Array<CommentError>,
@@ -152,4 +167,4 @@ declare type ReducedComment = {
152167
name: string,
153168
kind: ?Kind,
154169
scope?: ?Scope
155-
}
170+
};

Diff for: lib/nest.js

+62-45
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,76 @@
11
/* @flow */
22
'use strict';
33

4+
var _ = require('lodash');
5+
6+
const PATH_SPLIT = /(?:\[])?\./g;
7+
8+
function rejectUnnamedTags(
9+
tags /*: Array<CommentTag> */
10+
) /*: Array<CommentTagNamed> */ {
11+
return tags.filter(tag => typeof tag.name === 'string');
12+
}
13+
414
/**
515
* Nest nestable tags, like param and property, into nested
616
* arrays that are suitable for output.
17+
*Okay, so we're building a tree of comments, with the tagName
18+
* being the indexer
19+
*
20+
* foo.abe
21+
* foo.bar.baz
22+
* foo.bar.a
23+
* foo.bar[].bax
24+
*
25+
* foo -> .abe
26+
* \-> .bar -> .baz
27+
* \-> .a
28+
* \-> [].baz
729
*
830
* @private
931
* @param {Object} comment a comment with tags
1032
* @param {string} tagTitle the tag to nest
1133
* @param {string} target the tag to nest into
1234
* @returns {Object} nested comment
1335
*/
14-
function nestTag(
15-
comment /*: Comment */,
16-
tagTitle /*: string */,
17-
target /*: string */
18-
) {
19-
if (!comment[target]) {
20-
return comment;
21-
}
36+
var nestTag = (tags /*: Array<CommentTag> */) =>
37+
rejectUnnamedTags(tags)
38+
.sort(
39+
(a, b) =>
40+
a.name.split(PATH_SPLIT).length - b.name.split(PATH_SPLIT).length
41+
)
42+
.reduce(
43+
(memo, tag) => {
44+
function insertTag(node, parts) {
45+
// The 'done' case: we're at parts.length === 1,
46+
// this is where the node should be placed. We reliably
47+
// get to this case because the recursive method
48+
// is always passed parts.slice(1)
49+
if (parts.length === 1) {
50+
_.assign(node, {
51+
properties: (node.properties || []).concat(tag)
52+
});
53+
} else {
54+
// The recursive case: try to find the child that owns
55+
// this tag.
56+
let child = node.properties &&
57+
node.properties.find(
58+
property => parts[0] === _.last(property.name.split(PATH_SPLIT))
59+
);
2260

23-
var result = [], index = {};
61+
if (!child) {
62+
throw new Error(`Parent of ${tag.name} not found `);
63+
}
2464

25-
comment[target].forEach(tag => {
26-
var tagName = tag.name;
27-
if (tagName) {
28-
index[tagName] = tag;
29-
var parts = tagName
30-
.split(/(\[\])?\./)
31-
.filter(part => part && part !== '[]');
32-
if (parts.length > 1) {
33-
var parent = index[parts.slice(0, -1).join('.')];
34-
if (parent === undefined) {
35-
comment.errors.push({
36-
message: '@' +
37-
tagTitle +
38-
' ' +
39-
tag.name +
40-
"'s parent " +
41-
parts[0] +
42-
' not found',
43-
commentLineNumber: tag.lineNumber
44-
});
45-
result.push(tag);
46-
return;
65+
insertTag(child, parts.slice(1));
66+
}
4767
}
48-
parent.properties = parent.properties || [];
49-
parent.properties.push(tag);
50-
} else {
51-
result.push(tag);
52-
}
53-
}
54-
});
55-
56-
comment[target] = result;
5768

58-
return comment;
59-
}
69+
insertTag(memo, tag.name.split(PATH_SPLIT));
70+
return memo;
71+
},
72+
{ properties: [] }
73+
).properties;
6074

6175
/**
6276
* Nests
@@ -70,8 +84,11 @@ function nestTag(
7084
* @param {Object} comment input comment
7185
* @return {Object} nested comment
7286
*/
73-
function nest(comment /*: Comment*/) {
74-
return nestTag(nestTag(comment, 'param', 'params'), 'property', 'properties');
75-
}
87+
var nest = (comment /*: Comment*/) =>
88+
_.assign(comment, {
89+
params: nestTag(comment.params),
90+
properties: nestTag(comment.properties)
91+
});
7692

7793
module.exports = nest;
94+
module.exports.nestTag = nestTag;

Diff for: test/lib/nest.js

+39-85
Original file line numberDiff line numberDiff line change
@@ -1,101 +1,55 @@
11
'use strict';
22

3-
var test = require('tap').test,
4-
parse = require('../../lib/parsers/javascript'),
5-
nest = require('../../lib/nest');
3+
var test = require('tap').test;
4+
var nestTag = require('../../lib/nest').nestTag;
65

7-
function toComment(fn, filename) {
8-
return parse(
9-
{
10-
file: filename,
11-
source: fn instanceof Function ? '(' + fn.toString() + ')' : fn
12-
},
13-
{}
14-
).map(nest);
15-
}
6+
// Print a tree of tags in a way that's easy to test.
7+
var printTree = indent =>
8+
node =>
9+
`${new Array(indent + 1).join(' ')}- ${node.name}${node.properties ? '\n' : ''}${(node.properties || [
10+
])
11+
.map(printTree(indent + 1))
12+
.join('\n')}`;
1613

17-
test('nest params - no params', function(t) {
18-
t.deepEqual(
19-
toComment(function() {
20-
/** @name foo */
21-
})[0].params,
22-
[],
23-
'no params'
24-
);
25-
t.end();
26-
});
27-
28-
test('nest params - no nesting', function(t) {
29-
var result = toComment(function() {
30-
/** @param {Object} foo */
31-
});
32-
t.equal(result[0].params.length, 1);
33-
t.equal(result[0].params[0].name, 'foo');
34-
t.equal(result[0].params[0].properties, undefined);
35-
t.end();
36-
});
14+
var printNesting = params =>
15+
printTree(0)({ properties: nestTag(params), name: 'root' });
3716

3817
test('nest params - basic', function(t) {
39-
var result = toComment(function() {
40-
/**
41-
* @param {Object} foo
42-
* @param {string} foo.bar
43-
* @param {string} foo.baz
44-
*/
45-
});
46-
t.equal(result[0].params.length, 1);
47-
t.equal(result[0].params[0].name, 'foo');
48-
t.equal(result[0].params[0].properties.length, 2);
49-
t.equal(result[0].params[0].properties[0].name, 'foo.bar');
50-
t.equal(result[0].params[0].properties[1].name, 'foo.baz');
51-
t.end();
52-
});
53-
54-
test('nest properties - basic', function(t) {
55-
var result = toComment(function() {
56-
/**
57-
* @property {Object} foo
58-
* @property {string} foo.bar
59-
* @property {string} foo.baz
60-
*/
61-
});
62-
t.equal(result[0].properties.length, 1);
63-
t.equal(result[0].properties[0].name, 'foo');
64-
t.equal(result[0].properties[0].properties.length, 2);
65-
t.equal(result[0].properties[0].properties[0].name, 'foo.bar');
66-
t.equal(result[0].properties[0].properties[1].name, 'foo.baz');
18+
var params = [
19+
'foo',
20+
'foo.bar',
21+
'foo.bar.third',
22+
'foo.third',
23+
'foo.third[].baz'
24+
].map(name => ({ name }));
25+
t.equal(
26+
printNesting(params),
27+
`- root
28+
- foo
29+
- foo.bar
30+
- foo.bar.third
31+
- foo.third
32+
- foo.third[].baz`
33+
);
6734
t.end();
6835
});
6936

70-
test('nest params - array', function(t) {
71-
var result = toComment(function() {
72-
/**
73-
* @param {Object[]} employees - The employees who are responsible for the project.
74-
* @param {string} employees[].name - The name of an employee.
75-
* @param {string} employees[].department - The employee's department.
76-
*/
77-
});
78-
t.equal(result[0].params.length, 1);
79-
t.equal(result[0].params[0].name, 'employees');
80-
t.equal(result[0].params[0].properties.length, 2);
81-
t.equal(result[0].params[0].properties[0].name, 'employees[].name');
82-
t.equal(result[0].params[0].properties[1].name, 'employees[].department');
37+
test('nest params - multiple roots', function(t) {
38+
var params = ['a', 'b', 'c'].map(name => ({ name }));
39+
t.equal(
40+
printNesting(params),
41+
`- root
42+
- a
43+
- b
44+
- c`
45+
);
8346
t.end();
8447
});
8548

8649
test('nest params - missing parent', function(t) {
87-
var result = toComment(function() {
88-
/** @param {string} foo.bar */
50+
var params = ['foo', 'foo.bar.third'].map(name => ({ name }));
51+
t.throws(() => {
52+
nestTag(params);
8953
});
90-
t.equal(result[0].params.length, 1);
91-
t.deepEqual(
92-
result[0].errors[0],
93-
{
94-
message: "@param foo.bar's parent foo not found",
95-
commentLineNumber: 0
96-
},
97-
'correct error message'
98-
);
99-
t.equal(result[0].params[0].name, 'foo.bar');
10054
t.end();
10155
});

0 commit comments

Comments
 (0)