Skip to content

Commit 27aa323

Browse files
committed
Add mixed multi-line comment logic and tests
1 parent 4e4c30e commit 27aa323

2 files changed

Lines changed: 148 additions & 11 deletions

File tree

packages/annotation-comments/src/parsers/comment-types/multi-line.ts

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,8 @@ function getCommentFromMatchingSyntaxPair(options: {
232232
// so build the AnnotationComment object and return it
233233
const match = commentSyntaxMatches[bestMatchIndex] as MultiLineCommentSyntaxMatch
234234
const syntax = multiLineCommentSyntaxes[bestMatchIndex]
235-
const isOnSingleLineBeforeCode = match.openingRange.start.line === match.closingRange.end.line && match.closingRangeWithWhitespace.end.column
235+
const isOnSingleLine = match.openingRange.start.line === match.closingRange.end.line
236+
const isOnSingleLineBeforeCode = isOnSingleLine && match.closingRangeWithWhitespace.end.column
236237
const commentRange: SourceRange = {
237238
start: isOnSingleLineBeforeCode ? match.openingRange.start : match.openingRangeWithWhitespace.start,
238239
end: match.closingRangeWithWhitespace.end,
@@ -241,13 +242,20 @@ function getCommentFromMatchingSyntaxPair(options: {
241242
start: match.openingRangeWithWhitespace.end,
242243
end: match.closingRangeWithWhitespace.start,
243244
}
245+
// If the opening sequence ends at a line boundary, adjust the inner range to exclude it
246+
if (!innerRange.start.column && !isOnSingleLine) {
247+
innerRange.start = { line: innerRange.start.line + 1 }
248+
}
249+
// If the closing sequence starts on a line boundary, adjust the inner range to exclude it
250+
if (!innerRange.end.column && !isOnSingleLine) {
251+
innerRange.end = { line: innerRange.end.line - 1 }
252+
}
244253
const contents: string[] = []
245254
const contentRanges: SourceRange[] = []
246255

247-
for (let lineIndex = tag.range.end.line; lineIndex <= innerRange.end.line; lineIndex++) {
248-
const line = codeLines[lineIndex]
249-
const startColumn = lineIndex === tag.range.end.line ? tag.range.end.column : lineIndex === innerRange.start.line ? (innerRange.start.column ?? line.length) : 0
250-
const endColumn = lineIndex === innerRange.end.line ? (innerRange.end.column ?? 0) : line.length
256+
for (let lineIndex = innerRange.start.line; lineIndex <= innerRange.end.line; lineIndex++) {
257+
const startColumn = lineIndex === tag.range.end.line ? tag.range.end.column : lineIndex === innerRange.start.line ? innerRange.start.column : undefined
258+
const endColumn = lineIndex === innerRange.end.line ? innerRange.end.column : undefined
251259

252260
const lineContent = getTextContentInLine({
253261
codeLines,
@@ -256,8 +264,33 @@ function getCommentFromMatchingSyntaxPair(options: {
256264
endColumn,
257265
continuationLineStart: syntax.continuationLineStart,
258266
})
259-
contents.push(lineContent.content)
260-
contentRanges.push(lineContent.contentRange)
267+
if (lineIndex < tag.range.end.line) {
268+
// If the current comment line has content and is located before the annotation tag,
269+
// we need to reduce the comment range to exclude any non-annotation content
270+
// including the opening and closing comment syntaxes, so removing the annotation
271+
// later doesn't break the commment
272+
if (lineContent.content.length) {
273+
commentRange.start = { line: tag.range.start.line }
274+
commentRange.end = { ...innerRange.end }
275+
}
276+
} else if (lineIndex >= tag.range.end.line && lineContent.content === '---') {
277+
// We encountered a separator line after the annotation tag, so this is a mixed
278+
// comment with multiple pieces of content and we must limit the comment range
279+
// to the current annotation (however, we still include the separator line)
280+
commentRange.start = { line: tag.range.start.line }
281+
commentRange.end = { line: lineIndex }
282+
break
283+
} else if (lineIndex >= tag.range.end.line && lineContent.content.startsWith('[!')) {
284+
// We encountered the beginning of another annotation tag, so this is a mixed
285+
// comment with multiple pieces of content and we must limit the comment range
286+
// to the current annotation
287+
commentRange.start = { line: tag.range.start.line }
288+
commentRange.end = { line: lineIndex - 1 }
289+
break
290+
} else {
291+
contents.push(lineContent.content)
292+
contentRanges.push(lineContent.contentRange)
293+
}
261294
}
262295

263296
// Remove empty lines from the beginning and end of the content arrays

packages/annotation-comments/test/parent-comment.test.ts

Lines changed: 108 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -448,8 +448,8 @@ describe('parseParentComment()', () => {
448448
])
449449
})
450450
})
451-
describe('JSDoc-style comments', () => {
452-
test('Covers an entire JSDoc comment if the annotation is its only content', () => {
451+
describe('Handles special syntax requirements', () => {
452+
test('Excludes JSDoc continuation line syntax "*" from annotation content', () => {
453453
const lines = [
454454
// JSDoc example with the entire block being indented by a tab
455455
'\t/**',
@@ -469,21 +469,125 @@ describe('parseParentComment()', () => {
469469
{ start: { line: 5, column: 4 }, end: { line: 5 } },
470470
])
471471
})
472-
test.todo('Only covers the annotation lines if the JSDoc contains other content', () => {
472+
})
473+
describe('Handles mixed comments with other content besides the annotation', () => {
474+
test('Excludes the outer comment syntax and all non-annotation lines', () => {
473475
const lines = [
476+
// Opening syntax that should not be included in the comment range
477+
// as the comment also contains non-annotation content
474478
'\t/**',
475479
// Non-annotation content
476480
'\t * Some JSDoc that is not part of the annotation comment.',
481+
'\t *',
482+
// Annotation content - these are the only lines that should be included
483+
'\t * [!note] Annotation content',
484+
'\t * that spans multiple lines',
485+
'\t * until the comment ends',
486+
// Closing syntax that should not be included in the comment range
487+
// as the comment also contains non-annotation content
488+
'\t */',
489+
'\tsomeCode()',
490+
]
491+
const comment = getParentComment(getTestCode(lines.join('\n'))) as AnnotationComment
492+
expect(comment.contents).toEqual(['Annotation content', 'that spans multiple lines', 'until the comment ends'])
493+
expect(comment.commentRange).toEqual({ start: { line: 5 }, end: { line: 7 } })
494+
expect(comment.contentRanges).toEqual([
495+
{ start: { line: 5, column: lines[3].indexOf('Annotation') }, end: { line: 5 } },
496+
{ start: { line: 6, column: 4 }, end: { line: 6 } },
497+
{ start: { line: 7, column: 4 }, end: { line: 7 } },
498+
])
499+
})
500+
test('Excludes non-annotation content even without a line break before it', () => {
501+
const lines = [
502+
// Non-annotation content directly after the opening syntax
503+
'\t/** Some JSDoc that is not part of the annotation comment.',
477504
// Annotation content
478505
'\t * [!note] Annotation content',
479506
'\t * that spans multiple lines',
480507
'\t * until the comment ends',
508+
// Closing syntax that should not be included in the comment range
509+
// as the comment also contains non-annotation content
481510
'\t */',
482511
'\tsomeCode()',
483512
]
484513
const comment = getParentComment(getTestCode(lines.join('\n'))) as AnnotationComment
485514
expect(comment.contents).toEqual(['Annotation content', 'that spans multiple lines', 'until the comment ends'])
486-
expect(comment.commentRange).toEqual({ start: { line: 4 }, end: { line: 6 } })
515+
expect(comment.commentRange).toEqual({ start: { line: 3 }, end: { line: 5 } })
516+
expect(comment.contentRanges).toEqual([
517+
{ start: { line: 3, column: lines[1].indexOf('Annotation') }, end: { line: 3 } },
518+
{ start: { line: 4, column: 4 }, end: { line: 4 } },
519+
{ start: { line: 5, column: 4 }, end: { line: 5 } },
520+
])
521+
})
522+
test('Excludes the closing syntax even without a line break before it', () => {
523+
const lines = [
524+
'\t/**',
525+
// Non-annotation content
526+
'\t * Some JSDoc that is not part of the annotation comment.',
527+
// Annotation content
528+
'\t * [!note] Annotation content',
529+
'\t * that spans multiple lines',
530+
'\t * until the comment ends */',
531+
'\tsomeCode()',
532+
]
533+
const comment = getParentComment(getTestCode(lines.join('\n'))) as AnnotationComment
534+
expect(comment.contents).toEqual(['Annotation content', 'that spans multiple lines', 'until the comment ends'])
535+
// Expect the closing syntax not to be included in the comment range
536+
// as the comment also contains non-annotation content
537+
expect(comment.commentRange).toEqual({ start: { line: 4 }, end: { line: 6, column: lines[4].indexOf(' */') } })
538+
expect(comment.contentRanges).toEqual([
539+
{ start: { line: 4, column: lines[2].indexOf('Annotation') }, end: { line: 4 } },
540+
{ start: { line: 5, column: 4 }, end: { line: 5 } },
541+
{ start: { line: 6, column: 4 }, end: { line: 6, column: lines[4].indexOf(' */') } },
542+
])
543+
})
544+
test('Ends the annotation when encountering another annotation tag', () => {
545+
const lines = [
546+
// Opening syntax that should not be included in the comment range
547+
// as the comment also contains non-annotation content
548+
'\t/**',
549+
// Annotation content - these are the lines that should be included
550+
'\t * [!note] Annotation content',
551+
'\t * that spans multiple lines',
552+
'\t * until a new tag is encountered',
553+
// Second annotation
554+
'\t * [!test] Yet another annotation',
555+
// Closing syntax that should not be included in the comment range
556+
// as the comment also contains non-annotation content
557+
'\t */',
558+
'\tsomeCode()',
559+
]
560+
const comment = getParentComment(getTestCode(lines.join('\n'))) as AnnotationComment
561+
expect(comment.contents).toEqual(['Annotation content', 'that spans multiple lines', 'until a new tag is encountered'])
562+
// Do not include the second annotation in the comment range
563+
expect(comment.commentRange).toEqual({ start: { line: 3 }, end: { line: 5 } })
564+
expect(comment.contentRanges).toEqual([
565+
{ start: { line: 3, column: lines[1].indexOf('Annotation') }, end: { line: 3 } },
566+
{ start: { line: 4, column: 4 }, end: { line: 4 } },
567+
{ start: { line: 5, column: 4 }, end: { line: 5 } },
568+
])
569+
})
570+
test('Ends the annotation when encountering "---" on its own line', () => {
571+
const lines = [
572+
// Opening syntax that should not be included in the comment range
573+
// as the comment also contains non-annotation content
574+
'\t/**',
575+
// Annotation content - these are the lines that should be included
576+
'\t * [!note] Annotation content',
577+
'\t * that spans multiple lines',
578+
'\t * until "---" is encountered',
579+
// Separator that is also considered part of the annotation comment range
580+
'\t * ---',
581+
// Non-annotation content
582+
'\t * Some JSDoc that is not part of the annotation comment.',
583+
// Closing syntax that should not be included in the comment range
584+
// as the comment also contains non-annotation content
585+
'\t */',
586+
'\tsomeCode()',
587+
]
588+
const comment = getParentComment(getTestCode(lines.join('\n'))) as AnnotationComment
589+
expect(comment.contents).toEqual(['Annotation content', 'that spans multiple lines', 'until "---" is encountered'])
590+
expect(comment.commentRange).toEqual({ start: { line: 3 }, end: { line: 6 } })
487591
expect(comment.contentRanges).toEqual([
488592
{ start: { line: 3, column: lines[1].indexOf('Annotation') }, end: { line: 3 } },
489593
{ start: { line: 4, column: 4 }, end: { line: 4 } },

0 commit comments

Comments
 (0)