Skip to content

Commit 921874b

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

File tree

3 files changed

+188
-138
lines changed

3 files changed

+188
-138
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

+63-44
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,78 @@
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 = (
37+
tags /*: Array<CommentTag> */
38+
// Use lodash here both for brevity and also because, unlike JavaScript's
39+
// sort, it's stable.
40+
) =>
41+
_.sortBy(
42+
rejectUnnamedTags(tags),
43+
tag => tag.name.split(PATH_SPLIT).length
44+
).reduce(
45+
(memo, tag) => {
46+
function insertTag(node, parts) {
47+
// The 'done' case: we're at parts.length === 1,
48+
// this is where the node should be placed. We reliably
49+
// get to this case because the recursive method
50+
// is always passed parts.slice(1)
51+
if (parts.length === 1) {
52+
_.assign(node, {
53+
properties: (node.properties || []).concat(tag)
54+
});
55+
} else {
56+
// The recursive case: try to find the child that owns
57+
// this tag.
58+
let child = node.properties &&
59+
node.properties.find(
60+
property => parts[0] === _.last(property.name.split(PATH_SPLIT))
61+
);
2262

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

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;
67+
insertTag(child, parts.slice(1));
4768
}
48-
parent.properties = parent.properties || [];
49-
parent.properties.push(tag);
50-
} else {
51-
result.push(tag);
5269
}
53-
}
54-
});
55-
56-
comment[target] = result;
5770

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

6177
/**
6278
* Nests
@@ -70,8 +86,11 @@ function nestTag(
7086
* @param {Object} comment input comment
7187
* @return {Object} nested comment
7288
*/
73-
function nest(comment /*: Comment*/) {
74-
return nestTag(nestTag(comment, 'param', 'params'), 'property', 'properties');
75-
}
89+
var nest = (comment /*: Comment*/) =>
90+
_.assign(comment, {
91+
params: nestTag(comment.params),
92+
properties: nestTag(comment.properties)
93+
});
7694

7795
module.exports = nest;
96+
module.exports.nestTag = nestTag;

Diff for: test/lib/nest.js

+96-80
Original file line numberDiff line numberDiff line change
@@ -1,101 +1,117 @@
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-
});
14+
var printNesting = params =>
15+
printTree(0)({ properties: nestTag(params), name: 'root' });
2716

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);
17+
test('nest params - basic', function(t) {
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+
);
3534
t.end();
3635
});
3736

38-
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');
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+
);
5146
t.end();
5247
});
5348

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-
*/
49+
test('nest params - missing parent', function(t) {
50+
var params = ['foo', 'foo.bar.third'].map(name => ({ name }));
51+
t.throws(() => {
52+
nestTag(params);
6153
});
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');
6754
t.end();
6855
});
6956

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');
57+
test('nest params - #658', function(t) {
58+
var params = [
59+
'state',
60+
'payload',
61+
'payload.input_meter_levels',
62+
'payload.input_meter_levels[].peak',
63+
'payload.input_meter_levels[].rms',
64+
'payload.output_meter_levels',
65+
'payload.output_meter_levels[].peak',
66+
'payload.output_meter_levels[].rms'
67+
].map(name => ({ name }));
68+
t.equal(
69+
printNesting(params),
70+
`- root
71+
- state
72+
- payload
73+
- payload.input_meter_levels
74+
- payload.input_meter_levels[].peak
75+
- payload.input_meter_levels[].rms
76+
- payload.output_meter_levels
77+
- payload.output_meter_levels[].peak
78+
- payload.output_meter_levels[].rms`
79+
);
8380
t.end();
8481
});
8582

86-
test('nest params - missing parent', function(t) {
87-
var result = toComment(function() {
88-
/** @param {string} foo.bar */
89-
});
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'
83+
test('nest params - #554', function(t) {
84+
var params = [
85+
'x',
86+
'yIn',
87+
'options',
88+
'options.sgOption',
89+
'options.minMaxRatio',
90+
'options.broadRatio',
91+
'options.noiseLevel',
92+
'options.maxCriteria',
93+
'options.smoothY',
94+
'options.realTopDetection',
95+
'options.heightFactor',
96+
'options.boundaries',
97+
'options.derivativeThreshold'
98+
].map(name => ({ name }));
99+
t.equal(
100+
printNesting(params),
101+
`- root
102+
- x
103+
- yIn
104+
- options
105+
- options.sgOption
106+
- options.minMaxRatio
107+
- options.broadRatio
108+
- options.noiseLevel
109+
- options.maxCriteria
110+
- options.smoothY
111+
- options.realTopDetection
112+
- options.heightFactor
113+
- options.boundaries
114+
- options.derivativeThreshold`
98115
);
99-
t.equal(result[0].params[0].name, 'foo.bar');
100116
t.end();
101117
});

0 commit comments

Comments
 (0)