Skip to content

Commit 42b342d

Browse files
committed
Add error messages to parse result, improve test coverage
1 parent 52a7f97 commit 42b342d

11 files changed

Lines changed: 304 additions & 64 deletions

File tree

packages/annotation-comments/README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ console.log('Some code');
4141

4242
const codeLines = code.trim().split(/\r?\n/);
4343

44-
const annotationComments = parseAnnotationComments({ codeLines });
44+
const { annotationComments, errorMessages } = parseAnnotationComments({ codeLines });
4545

4646
cleanCode({ annotationComments, codeLines });
4747
```
@@ -62,9 +62,14 @@ type ParseAnnotationCommentsOptions = {
6262
}
6363
```
6464
65-
Its return value is an array of `AnnotationComment` objects:
65+
Its return value is an object that contains both all parsed annotation comments and any error messages that occurred during parsing:
6666
6767
```ts
68+
export type ParseAnnotationCommentsResult = {
69+
annotationComments: AnnotationComment[]
70+
errorMessages: string[]
71+
}
72+
6873
type AnnotationComment = {
6974
tag: AnnotationTag
7075
contents: string[]
@@ -95,6 +100,9 @@ export type SourceLocation = {
95100
96101
### cleanCode()
97102
103+
> **Warning**
104+
> This function is not implemented yet.
105+
98106
This function prepares annotated code lines for display or copying to the clipboard by making it look like regular (non-annotated) code again.
99107
100108
It will collect all necessary edits and apply them to the code in reverse order (from the last edit location to the first) to avoid having to update the locations of all remaining annotations after each edit.

packages/annotation-comments/src/core/parse.ts

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,23 @@ import { parseAnnotationTags } from '../parsers/annotation-tags'
33
import { parseParentComment } from '../parsers/parent-comment'
44
import { secondRangeIsInFirst } from '../internal/ranges'
55
import { findAnnotationTargets } from './find-targets'
6+
import { coerceError } from '../internal/errors'
67

78
export type ParseAnnotationCommentsOptions = {
89
codeLines: string[]
910
}
1011

11-
export function parseAnnotationComments(options: ParseAnnotationCommentsOptions): AnnotationComment[] {
12+
export type ParseAnnotationCommentsResult = {
13+
annotationComments: AnnotationComment[]
14+
errorMessages: string[]
15+
}
16+
17+
export function parseAnnotationComments(options: ParseAnnotationCommentsOptions): ParseAnnotationCommentsResult {
1218
const { codeLines } = options
1319
const annotationComments: AnnotationComment[] = []
1420

1521
// Find annotation tags in the code
16-
const annotationTags = parseAnnotationTags({ codeLines })
22+
const { annotationTags, errorMessages } = parseAnnotationTags({ codeLines })
1723

1824
const ignoreCountPerTagName = new Map<string, number>()
1925
let tagsIgnoredUntilLineIndex = -1
@@ -36,21 +42,31 @@ export function parseAnnotationComments(options: ParseAnnotationCommentsOptions)
3642
ignoreCountPerTagName.set(tag.name, ignoreCount - 1)
3743
return
3844
}
39-
// Allow creating new ignores
45+
// Allow creating new ignores through an `ignore-tags` annotation comment
4046
if (tag.name === 'ignore-tags') {
41-
// By definition, `ignore-tags` must be on its own line
42-
if (comment.annotationRange.start.column || comment.annotationRange.end.column) return
43-
const ignoreRange = tag.relativeTargetRange ?? 1
44-
if (typeof tag.targetSearchQuery === 'string') {
45-
const targetTagNames = tag.targetSearchQuery.split(',').map((name) => name.trim())
46-
targetTagNames.forEach((name) => {
47-
ignoreCountPerTagName.set(name, ignoreRange)
48-
})
49-
} else if (ignoreRange > 0) {
50-
tagsIgnoredUntilLineIndex = tag.range.end.line + ignoreRange
47+
try {
48+
// Require ignore-tags to be on its own line
49+
if (comment.annotationRange.start.column || comment.annotationRange.end.column) {
50+
throw new Error('It must be on its own line.')
51+
}
52+
if (tag.relativeTargetRange !== undefined && !(tag.relativeTargetRange > 0)) {
53+
throw new Error('If given, the target range must be a positive number.')
54+
}
55+
const ignoreRange = tag.relativeTargetRange ?? 1
56+
if (typeof tag.targetSearchQuery === 'string') {
57+
const targetTagNames = tag.targetSearchQuery.split(',').map((name) => name.trim())
58+
targetTagNames.forEach((name) => {
59+
ignoreCountPerTagName.set(name, ignoreRange)
60+
})
61+
} else if (ignoreRange > 0) {
62+
tagsIgnoredUntilLineIndex = tag.range.end.line + ignoreRange
63+
}
64+
// We still want to add this valid ignore-tags comment to the result array
65+
// to allow it to be removed by `cleanCode()` later, so we don't return here
66+
} catch (error) {
67+
const errorMessage = coerceError(error).message
68+
errorMessages.push(`Invalid "ignore-tags" annotation in line ${comment.annotationRange.start.line + 1}: ${errorMessage}`)
5169
}
52-
// We still want to add this valid ignore-tags comment to the result array
53-
// to allow it to be removed by `cleanCode()` later, so we don't return here
5470
}
5571

5672
// If we arrive here, add the tag and comment to the list of annotation comments
@@ -61,5 +77,8 @@ export function parseAnnotationComments(options: ParseAnnotationCommentsOptions)
6177
// Find the target ranges for all annotations
6278
findAnnotationTargets({ codeLines, annotationComments })
6379

64-
return annotationComments
80+
return {
81+
annotationComments,
82+
errorMessages,
83+
}
6584
}
Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
11
export type * from './core/types'
22
export * from './core/parse'
33
// export * from './core/clean'
4-
5-
export * from './parsers/annotation-tags'
6-
export * from './parsers/parent-comment'

packages/annotation-comments/src/internal/errors.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66
*/
77
export function coerceError(error: unknown): { message: string; code?: string | undefined } {
88
if (typeof error === 'object' && error !== null && 'message' in error) {
9+
/* c8 ignore start */
910
return error as { message: string; code?: string | undefined }
11+
/* c8 ignore end */
1012
}
13+
/* c8 ignore start */
1114
return { message: error as string }
15+
/* c8 ignore end */
1216
}

packages/annotation-comments/src/internal/regexps.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export function getGroupIndicesFromRegExpMatch(match: RegExpMatchArray) {
3939
// Read the start and end ranges from the `indices` property,
4040
// which is made available through the RegExp flag `d`
4141
let groupIndices = match.indices as ([start: number, end: number] | null)[]
42+
/* v8 ignore start */
4243
if (groupIndices?.length) return groupIndices
4344

4445
// We could not access native group indices, so we need to use fallback logic
@@ -53,6 +54,7 @@ export function getGroupIndicesFromRegExpMatch(match: RegExpMatchArray) {
5354
})
5455

5556
return groupIndices
57+
/* v8 ignore end */
5658
}
5759

5860
/**

packages/annotation-comments/src/parsers/annotation-tags.ts

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import { AnnotationTag } from '../core/types'
2+
import { coerceError } from '../internal/errors'
23
import { getEscapeSequenceRegExp } from '../internal/escaping'
34
import { createGlobalRegExp } from '../internal/regexps'
45

56
export type ParseAnnotationTagsOptions = {
67
codeLines: string[]
78
}
89

10+
export type ParseAnnotationTagsResult = {
11+
annotationTags: AnnotationTag[]
12+
errorMessages: string[]
13+
}
14+
915
const annotationTagRegex = new RegExp(
1016
[
1117
// Opening sequence
@@ -98,30 +104,40 @@ function parseTargetSearchQuery(rawTargetSearchQuery: string | undefined): strin
98104
return unescapedQuery
99105
}
100106

101-
export function parseAnnotationTags(options: ParseAnnotationTagsOptions): AnnotationTag[] {
107+
export function parseAnnotationTags(options: ParseAnnotationTagsOptions): ParseAnnotationTagsResult {
102108
const { codeLines } = options
103109
const annotationTags: AnnotationTag[] = []
110+
const errorMessages: string[] = []
104111

105112
codeLines.forEach((line, lineIndex) => {
106113
const matches = [...line.matchAll(annotationTagRegex)]
107114
matches.forEach((match) => {
108-
const [, name, rawTargetSearchQuery, relativeTargetRange] = match
109-
const rawTag = match[0]
110-
const startColIndex = match.index
111-
const endColIndex = startColIndex + rawTag.length
112-
const targetSearchQuery = parseTargetSearchQuery(rawTargetSearchQuery)
113-
annotationTags.push({
114-
name,
115-
targetSearchQuery,
116-
relativeTargetRange: relativeTargetRange !== undefined ? Number(relativeTargetRange) : undefined,
117-
rawTag,
118-
range: {
119-
start: { line: lineIndex, column: startColIndex },
120-
end: { line: lineIndex, column: endColIndex },
121-
},
122-
})
115+
try {
116+
const [, name, rawTargetSearchQuery, relativeTargetRange] = match
117+
const rawTag = match[0]
118+
const startColIndex = match.index
119+
const endColIndex = startColIndex + rawTag.length
120+
121+
const targetSearchQuery = parseTargetSearchQuery(rawTargetSearchQuery)
122+
annotationTags.push({
123+
name,
124+
targetSearchQuery,
125+
relativeTargetRange: relativeTargetRange !== undefined ? Number(relativeTargetRange) : undefined,
126+
rawTag,
127+
range: {
128+
start: { line: lineIndex, column: startColIndex },
129+
end: { line: lineIndex, column: endColIndex },
130+
},
131+
})
132+
} catch (error) {
133+
const msg = coerceError(error).message
134+
errorMessages.push(`Failed to parse annotation tag in line ${lineIndex + 1}: ${msg}`)
135+
}
123136
})
124137
})
125138

126-
return annotationTags
139+
return {
140+
annotationTags,
141+
errorMessages,
142+
}
127143
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ export function parseSingleLineParentComment(options: ParseParentCommentOptions)
120120
// and expand the comment range to cover the additional full line
121121
const column = Math.min(possibleContentStart, line.length)
122122
contents.push('')
123-
contentRanges.push({ start: { line: lineIndex, column }, end: { line: lineIndex, column } })
123+
contentRanges.push(createRange({ codeLines, start: { line: lineIndex, column }, end: { line: lineIndex } }))
124124
commentRange.end = { line: lineIndex }
125125
}
126126
}

packages/annotation-comments/test/annotation-tags.test.ts

Lines changed: 74 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,76 @@ import { splitCodeLines } from './utils'
66

77
describe('parseAnnotationTags()', () => {
88
test('Returns an empty array when no annotation tags are found', () => {
9-
expect(
10-
getTags(`
11-
// This is a simple comment.
12-
console.log('Some code');
9+
const codeLines = [
10+
// Example code without an annotation tag
11+
'// This is a simple comment.',
12+
'console.log("Some code");',
13+
'',
14+
'/*',
15+
'\t[TODO] Not an annotation tag.',
16+
'*/',
17+
]
18+
const { annotationTags, errorMessages } = parseAnnotationTags({ codeLines })
1319

14-
/*
15-
[TODO] Not an annotation tag.
16-
*/
17-
`)
18-
).toEqual([])
20+
expect(annotationTags).toEqual([])
21+
expect(errorMessages).toEqual([])
22+
})
23+
24+
describe('Returns error messages when invalid annotation tags are found', () => {
25+
test('Single invalid regular expression', () => {
26+
const codeLines = [
27+
// The following regular expression should cause an error to be returned
28+
'// [!note:/(this|group|is|unclosed/] Note the invalid regexp in the search query.',
29+
'console.log("Some code");',
30+
]
31+
const { annotationTags, errorMessages } = parseAnnotationTags({ codeLines })
32+
33+
expect(annotationTags).toEqual([])
34+
expect(errorMessages).toEqual([
35+
// Expect the line number and regexp contents to be called out
36+
expect.stringMatching(/line 1.*\(this\|group/),
37+
])
38+
})
39+
test('Multiple invalid regular expressions', () => {
40+
const codeLines = [
41+
// The following regular expression should cause an error to be returned
42+
'// [!note:/(this|group|is|unclosed/] Note the invalid regexp in the search query.',
43+
'console.log("Some code");',
44+
'// [!note:/te[st/] This regexp is also invalid.',
45+
'console.log("Some more code");',
46+
]
47+
const { annotationTags, errorMessages } = parseAnnotationTags({ codeLines })
48+
49+
expect(annotationTags).toEqual([])
50+
expect(errorMessages).toEqual([
51+
// Expect both lines and regexp contents to be called out
52+
expect.stringMatching(/line 1.*\(this\|group/),
53+
expect.stringMatching(/line 3.*te\[st/),
54+
])
55+
})
56+
test('Valid tags are still parsed even if invalid ones are also present', () => {
57+
const codeLines = [
58+
// The following regular expression should cause an error to be returned
59+
'// [!note:/(this|group|is|unclosed/] Note the invalid regexp in the search query.',
60+
'console.log("Some code"); // [!tag] This is a valid tag.',
61+
'// [!note:/te[st/] This regexp is also invalid.',
62+
'console.log("Some more code");',
63+
]
64+
const { annotationTags, errorMessages } = parseAnnotationTags({ codeLines })
65+
66+
expect(annotationTags).toEqual([
67+
expect.objectContaining({
68+
name: 'tag',
69+
targetSearchQuery: undefined,
70+
relativeTargetRange: undefined,
71+
}),
72+
])
73+
expect(errorMessages).toEqual([
74+
// Expect both lines and regexp contents to be called out
75+
expect.stringMatching(/line 1.*\(this\|group/),
76+
expect.stringMatching(/line 3.*te\[st/),
77+
])
78+
})
1979
})
2080

2181
describe('Parses tag names', () => {
@@ -250,8 +310,8 @@ console.log('Some code'); // ${test.rawTag} Tag at the end of a line
250310
${test.rawTag} Tag in a multi-line comment
251311
*/
252312
`)
253-
// Build expected result by searching the raw tag in the code lines
254-
const expected = codeLines.reduce((acc, line, lineIndex) => {
313+
// Build array of expected tags by searching the raw tag in the code lines
314+
const expectedAnnotationTags = codeLines.reduce((acc, line, lineIndex) => {
255315
let startIndex = line.indexOf(test.rawTag)
256316
while (startIndex !== -1) {
257317
acc.push({
@@ -269,11 +329,8 @@ ${test.rawTag} Tag in a multi-line comment
269329
return acc
270330
}, [] as AnnotationTag[])
271331
// Perform test
272-
expect(parseAnnotationTags({ codeLines })).toEqual(expected)
273-
}
274-
275-
function getTags(code: string) {
276-
const codeLines = splitCodeLines(code)
277-
return parseAnnotationTags({ codeLines })
332+
const { annotationTags, errorMessages } = parseAnnotationTags({ codeLines })
333+
expect(annotationTags).toEqual(expectedAnnotationTags)
334+
expect(errorMessages).toEqual([])
278335
}
279336
})

0 commit comments

Comments
 (0)