Skip to content

Commit 2e670ec

Browse files
committed
Consider docblocks above decorators (fixes #18)
I wasn't able to create failing test cases at first. The following example passed just fine: /** * Test */ @decorator class Foo {} The comment was attached to the class declaration. However, the example in the issue clearly showed that it doesn't work in practice. After spending some trying to hunt down where/how the comment was attached to the class declaration in my example, but not in the issue's example, I remembered that recast attaches the (block?) comment to the first statement in the body if it is the first node in the AST. After adding something above the docblock (in this case an `import` statement), the issue reproduced as expected.
1 parent ff8d2a7 commit 2e670ec

File tree

3 files changed

+102
-18
lines changed

3 files changed

+102
-18
lines changed

src/handlers/__tests__/componentDocblockHandler-test.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ describe('componentDocblockHandler', () => {
3535
function test(definitionSrc, parse) { // eslint-disable-line no-shadow
3636
it('finds docblocks for component definitions', () => {
3737
var definition = parse(`
38+
import something from 'somewhere';
39+
3840
/**
3941
* Component description
4042
*/
@@ -47,6 +49,8 @@ describe('componentDocblockHandler', () => {
4749

4850
it('ignores other types of comments', () => {
4951
var definition = parse(`
52+
import something from 'somewhere';
53+
5054
/*
5155
* This is not a docblock',
5256
*/
@@ -67,6 +71,8 @@ describe('componentDocblockHandler', () => {
6771

6872
it('only considers the docblock directly above the definition', () => {
6973
var definition = parse(`
74+
import something from 'somewhere';
75+
7076
/**
7177
* This is the wrong docblock
7278
*/
@@ -79,6 +85,47 @@ describe('componentDocblockHandler', () => {
7985
});
8086
}
8187

88+
/**
89+
* Decorates can only be assigned to class and therefore only make sense for
90+
* class declarations and export declarations.
91+
*/
92+
function testDecorators(definitionSrc, parse) { // eslint-disable-line no-shadow
93+
describe('decorators', () => {
94+
it('uses the docblock above the decorator if it\'s the only one', () => {
95+
var definition = parse(`
96+
import something from 'somewhere';
97+
/**
98+
* Component description
99+
*/
100+
@Decorator1
101+
@Decorator2
102+
${definitionSrc}
103+
`);
104+
105+
componentDocblockHandler(documentation, definition);
106+
expect(documentation.description).toBe('Component description');
107+
});
108+
109+
it('uses the component docblock if present', () => {
110+
var definition = parse(`
111+
import something from 'somewhere';
112+
/**
113+
* Decorator description
114+
*/
115+
@Decorator1
116+
@Decorator2
117+
/**
118+
* Component description
119+
*/
120+
${definitionSrc}
121+
`);
122+
123+
componentDocblockHandler(documentation, definition);
124+
expect(documentation.description).toBe('Component description');
125+
});
126+
});
127+
}
128+
82129
describe('React.createClass', () => {
83130
test(
84131
'var Component = React.createClass({})',
@@ -91,6 +138,10 @@ describe('componentDocblockHandler', () => {
91138
'class Component {}',
92139
src => lastStatement(src)
93140
);
141+
testDecorators(
142+
'class Component {}',
143+
src => lastStatement(src)
144+
);
94145
});
95146

96147
describe('ClassExpression', () => {
@@ -114,13 +165,21 @@ describe('componentDocblockHandler', () => {
114165
'export default class Component {}',
115166
src => lastStatement(src).get('declaration')
116167
);
168+
testDecorators(
169+
'export default class Component {}',
170+
src => lastStatement(src).get('declaration')
171+
);
117172
});
118173

119174
describe('Default class expression export', () => {
120175
test(
121176
'export default class {}',
122177
src => lastStatement(src).get('declaration')
123178
);
179+
testDecorators(
180+
'export default class {}',
181+
src => lastStatement(src).get('declaration')
182+
);
124183
});
125184

126185
});
@@ -141,6 +200,10 @@ describe('componentDocblockHandler', () => {
141200
'export class Component {}',
142201
src => lastStatement(src).get('declaration')
143202
);
203+
testDecorators(
204+
'export class Component {}',
205+
src => lastStatement(src).get('declaration')
206+
);
144207
});
145208

146209
});

src/handlers/componentDocblockHandler.js

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ import {getDocblock} from '../utils/docblock';
1717

1818
var {types: {namedTypes: types}} = recast;
1919

20+
function isClassDefinition(nodePath) {
21+
var node = nodePath.node;
22+
return types.ClassDeclaration.check(node) ||
23+
types.ClassExpression.check(node);
24+
}
25+
2026
/**
2127
* Finds the nearest block comment before the component definition.
2228
*/
@@ -26,30 +32,38 @@ export default function componentDocblockHandler(
2632
) {
2733
var description = null;
2834
// Find parent statement (e.g. var Component = React.createClass(<path>);)
29-
while (path && !types.Statement.check(path.node)) {
30-
path = path.parent;
35+
var searchPath = path;
36+
while (searchPath && !types.Statement.check(searchPath.node)) {
37+
searchPath = searchPath.parent;
3138
}
32-
if (path) {
39+
if (searchPath) {
3340
// Class declarations are statements but can be part of default
3441
// export declarations
35-
if (types.ClassDeclaration.check(path.node) &&
36-
types.ExportDefaultDeclaration.check(path.parentPath.node)) {
37-
path = path.parentPath;
42+
if (types.ClassDeclaration.check(searchPath.node) &&
43+
types.ExportDefaultDeclaration.check(searchPath.parentPath.node)) {
44+
searchPath = searchPath.parentPath;
3845
}
3946
// If the parent is an export statement, we have to traverse one more up
40-
if (types.ExportNamedDeclaration.check(path.parentPath.node)) {
41-
path = path.parentPath;
47+
if (types.ExportNamedDeclaration.check(searchPath.parentPath.node)) {
48+
searchPath = searchPath.parentPath;
49+
}
50+
description = getDocblock(searchPath);
51+
}
52+
if (description == null && isClassDefinition(path)) {
53+
// If we have a class declaration or expression, then the comment might be
54+
// attached to the first decorator instead.
55+
if (path.node.decorators && path.node.decorators.length > 0) {
56+
description = getDocblock(path.get('decorators', 0));
4257
}
43-
description = getDocblock(path);
4458
}
4559
if (description == null) {
4660
// If this is the first statement in the module body, the comment is attached
4761
// to the program node
48-
var programPath = path;
62+
var programPath = searchPath;
4963
while (programPath && !types.Program.check(programPath.node)) {
5064
programPath = programPath.parent;
5165
}
52-
if (programPath.get('body', 0) === path) {
66+
if (programPath.get('body', 0) === searchPath) {
5367
description = getDocblock(programPath);
5468
}
5569
}

src/utils/docblock.js

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,22 @@ let DOCBLOCK_HEADER = /^\*\s/;
3030
* exists.
3131
*/
3232
export function getDocblock(path: NodePath): ?string {
33+
var comments = [];
3334
if (path.node.comments) {
34-
var comments = path.node.comments.filter(function(comment) {
35-
return comment.leading &&
35+
comments = path.node.comments.filter(
36+
comment => comment.leading &&
3637
comment.type === 'CommentBlock' &&
37-
DOCBLOCK_HEADER.test(comment.value);
38-
});
39-
if (comments.length > 0) {
40-
return parseDocblock(comments[comments.length - 1].value);
41-
}
38+
DOCBLOCK_HEADER.test(comment.value)
39+
);
40+
} else if (path.node.leadingComments) {
41+
comments = path.node.leadingComments.filter(
42+
comment => comment.type === 'CommentBlock' &&
43+
DOCBLOCK_HEADER.test(comment.value)
44+
);
45+
}
46+
47+
if (comments.length > 0) {
48+
return parseDocblock(comments[comments.length - 1].value);
4249
}
4350
return null;
4451
}

0 commit comments

Comments
 (0)