Skip to content

Commit 1415c5b

Browse files
authored
Feat/mark comments covered (#98)
* chore: add tests to prove updated ranges after prettify work * feat: always mark lines with comments as covered * add override * update format-css to 2.2.6
1 parent 8b2f995 commit 1415c5b

File tree

6 files changed

+143
-46
lines changed

6 files changed

+143
-46
lines changed

package-lock.json

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@
4141
"knip": "knip"
4242
},
4343
"dependencies": {
44-
"@projectwallace/format-css": "^2.2.0"
44+
"@projectwallace/css-parser": "^0.13.8",
45+
"@projectwallace/format-css": "^2.2.6"
4546
},
4647
"devDependencies": {
4748
"@codecov/vite-plugin": "^1.9.1",
@@ -56,6 +57,9 @@
5657
"tsdown": "^0.21.2",
5758
"typescript": "^5.9.3"
5859
},
60+
"overrides": {
61+
"@projectwallace/css-parser": "$@projectwallace/css-parser"
62+
},
5963
"engines": {
6064
"node": ">=20"
6165
},

src/lib/chunkify.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { tokenize } from '@projectwallace/css-parser/tokenizer'
12
import type { Coverage } from './parse-coverage'
23

34
type Chunk = {
@@ -51,6 +52,56 @@ function merge(stylesheet: ChunkedCoverage): ChunkedCoverage {
5152
}
5253
}
5354

55+
export function mark_comments_as_covered(stylesheet: ChunkedCoverage): ChunkedCoverage {
56+
let new_chunks: Chunk[] = []
57+
58+
for (let chunk of stylesheet.chunks) {
59+
if (chunk.is_covered) {
60+
new_chunks.push(chunk)
61+
continue
62+
}
63+
64+
let text = stylesheet.text.slice(chunk.start_offset, chunk.end_offset)
65+
let comments: Array<{ start: number; end: number }> = []
66+
67+
for (const _ of tokenize(text, ({ start, end }) => comments.push({ start, end }))) {
68+
// consume the generator to drive the on_comment callback
69+
}
70+
71+
if (comments.length === 0) {
72+
new_chunks.push(chunk)
73+
continue
74+
}
75+
76+
let last_end = 0
77+
for (let comment of comments) {
78+
if (comment.start > last_end) {
79+
new_chunks.push({
80+
start_offset: chunk.start_offset + last_end,
81+
end_offset: chunk.start_offset + comment.start,
82+
is_covered: false,
83+
})
84+
}
85+
new_chunks.push({
86+
start_offset: chunk.start_offset + comment.start,
87+
end_offset: chunk.start_offset + comment.end,
88+
is_covered: true,
89+
})
90+
last_end = comment.end
91+
}
92+
93+
if (last_end < text.length) {
94+
new_chunks.push({
95+
start_offset: chunk.start_offset + last_end,
96+
end_offset: chunk.end_offset,
97+
is_covered: false,
98+
})
99+
}
100+
}
101+
102+
return merge({ ...stylesheet, chunks: new_chunks })
103+
}
104+
54105
export function chunkify(stylesheet: Coverage): ChunkedCoverage {
55106
let chunks = []
56107
let offset = 0

src/lib/index.test.ts

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@ test.describe('from <style> tag', () => {
3030
let result = calculate_coverage(coverage)
3131
expect.soft(result.total_files_found).toBe(1)
3232
expect.soft(result.total_bytes).toBe(76)
33-
expect.soft(result.covered_bytes).toBe(39)
34-
expect.soft(result.uncovered_bytes).toBe(37)
33+
expect.soft(result.covered_bytes).toBe(57)
34+
expect.soft(result.uncovered_bytes).toBe(19)
3535
expect.soft(result.total_lines).toBe(12)
36-
expect.soft(result.covered_lines).toBe(8)
37-
expect.soft(result.uncovered_lines).toBe(4)
38-
expect.soft(result.line_coverage_ratio).toBe(8 / 12)
36+
expect.soft(result.covered_lines).toBe(9)
37+
expect.soft(result.uncovered_lines).toBe(3)
38+
expect.soft(result.line_coverage_ratio).toBe(9 / 12)
3939
expect.soft(result.total_stylesheets).toBe(1)
4040
})
4141

@@ -44,9 +44,9 @@ test.describe('from <style> tag', () => {
4444
let sheet = result.coverage_per_stylesheet.at(0)!
4545
expect.soft(sheet.url).toBe('http://localhost/test.html')
4646
expect.soft(sheet.total_lines).toBe(12)
47-
expect.soft(sheet.covered_lines).toBe(8)
48-
expect.soft(sheet.uncovered_lines).toBe(4)
49-
expect.soft(sheet.line_coverage_ratio).toBe(8 / 12)
47+
expect.soft(sheet.covered_lines).toBe(9)
48+
expect.soft(sheet.uncovered_lines).toBe(3)
49+
expect.soft(sheet.line_coverage_ratio).toBe(9 / 12)
5050
})
5151
})
5252

@@ -82,20 +82,20 @@ test.describe('from <link rel="stylesheet">', () => {
8282
let result = calculate_coverage(coverage)
8383
expect.soft(result.total_files_found).toBe(1)
8484
expect.soft(result.total_bytes).toBe(170)
85-
expect.soft(result.covered_bytes).toBe(96)
86-
expect.soft(result.uncovered_bytes).toBe(74)
85+
expect.soft(result.covered_bytes).toBe(132)
86+
expect.soft(result.uncovered_bytes).toBe(38)
8787
expect.soft(result.total_lines).toBe(23)
88-
expect.soft(result.covered_lines).toBe(15)
89-
expect.soft(result.uncovered_lines).toBe(8)
90-
expect.soft(result.line_coverage_ratio).toBe(15 / 23)
88+
expect.soft(result.covered_lines).toBe(17)
89+
expect.soft(result.uncovered_lines).toBe(6)
90+
expect.soft(result.line_coverage_ratio).toBe(17 / 23)
9191
expect.soft(result.total_stylesheets).toBe(1)
9292
})
9393

9494
test('calculates stats per stylesheet', () => {
9595
let result = calculate_coverage(coverage)
9696
let sheet = result.coverage_per_stylesheet.at(0)!
97-
expect.soft(sheet.covered_lines).toBe(15)
98-
expect.soft(sheet.uncovered_lines).toBe(8)
97+
expect.soft(sheet.covered_lines).toBe(17)
98+
expect.soft(sheet.uncovered_lines).toBe(6)
9999
expect.soft(sheet.total_lines).toBe(23)
100100
expect.soft(sheet.url).toEqual('http://localhost/style.css')
101101
expect
@@ -108,10 +108,10 @@ test.describe('from <link rel="stylesheet">', () => {
108108
)
109109
.toEqual([
110110
{ is_covered: true, start_line: 1, end_line: 4 },
111-
{ is_covered: false, start_line: 5, end_line: 8 },
112-
{ is_covered: true, start_line: 9, end_line: 13 },
113-
{ is_covered: false, start_line: 14, end_line: 17 },
114-
{ is_covered: true, start_line: 18, end_line: 23 },
111+
{ is_covered: false, start_line: 5, end_line: 7 },
112+
{ is_covered: true, start_line: 8, end_line: 13 },
113+
{ is_covered: false, start_line: 14, end_line: 16 },
114+
{ is_covered: true, start_line: 17, end_line: 23 },
115115
])
116116
})
117117
})
@@ -146,8 +146,8 @@ test.describe('from coverage data downloaded directly from the browser as JSON',
146146

147147
test('counts totals', () => {
148148
let result = calculate_coverage(coverage)
149-
expect.soft(result.covered_lines).toBe(11)
150-
expect.soft(result.uncovered_lines).toBe(4)
149+
expect.soft(result.covered_lines).toBe(12)
150+
expect.soft(result.uncovered_lines).toBe(3)
151151
expect.soft(result.total_lines).toBe(15)
152152
expect.soft(result.total_stylesheets).toBe(1)
153153
})
@@ -159,8 +159,8 @@ test.describe('from coverage data downloaded directly from the browser as JSON',
159159
color: blue;
160160
font-size: 24px;
161161
}
162-
163162
/* not covered */
163+
164164
p {
165165
color: red;
166166
}
@@ -184,8 +184,8 @@ p {
184184
total_lines,
185185
})),
186186
).toEqual([
187-
{ is_covered: true, start_line: 1, end_line: 5, total_lines: 5 },
188-
{ is_covered: false, start_line: 6, end_line: 9, total_lines: 4 },
187+
{ is_covered: true, start_line: 1, end_line: 6, total_lines: 6 },
188+
{ is_covered: false, start_line: 7, end_line: 9, total_lines: 3 },
189189
{ is_covered: true, start_line: 10, end_line: 15, total_lines: 6 },
190190
])
191191
})

src/lib/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { prettify, type PrettifiedChunk, type PrettifiedCoverage } from './prett
33
import { deduplicate_entries } from './decuplicate.js'
44
import { filter_coverage } from './filter-entries.js'
55
import { extend_ranges } from './extend-ranges.js'
6-
import { chunkify, type ChunkedCoverage } from './chunkify.js'
6+
import { chunkify, mark_comments_as_covered, type ChunkedCoverage } from './chunkify.js'
77

88
export type CoverageData = {
99
uncovered_bytes: number
@@ -81,7 +81,9 @@ export function calculate_coverage(coverage: Coverage[]): CoverageResult {
8181
)
8282
let deduplicated: Coverage[] = deduplicate_entries(filtered_coverage)
8383
let extended: Coverage[] = deduplicated.map((coverage) => extend_ranges(coverage))
84-
let chunkified: ChunkedCoverage[] = extended.map((sheet) => chunkify(sheet))
84+
let chunkified: ChunkedCoverage[] = extended.map((sheet) =>
85+
mark_comments_as_covered(chunkify(sheet)),
86+
)
8587
let prettified: PrettifiedCoverage[] = chunkified.map((sheet) => prettify(sheet))
8688
let coverage_per_stylesheet = prettified.map((stylesheet) =>
8789
calculate_stylesheet_coverage(stylesheet),

src/lib/test/kitchen-sink.test.ts

Lines changed: 57 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ test.describe('comment coverage', () => {
6666
</html>
6767
`
6868

69-
test('leading line comment is marked as uncovered', async () => {
69+
test('leading line comment is marked as covered', async () => {
7070
let css = `
7171
/* start comment */
7272
h1 { color: blue; }
@@ -81,13 +81,10 @@ test.describe('comment coverage', () => {
8181
end_line,
8282
total_lines,
8383
})),
84-
).toEqual([
85-
{ is_covered: false, start_line: 1, end_line: 1, total_lines: 1 },
86-
{ is_covered: true, start_line: 2, end_line: 5, total_lines: 4 },
87-
])
84+
).toEqual([{ is_covered: true, start_line: 1, end_line: 4, total_lines: 4 }])
8885
})
8986

90-
test('leading block comment is marked as uncovered', async () => {
87+
test('leading block comment is marked as covered', async () => {
9188
let css = `
9289
/*
9390
start comment
@@ -104,13 +101,10 @@ test.describe('comment coverage', () => {
104101
end_line,
105102
total_lines,
106103
})),
107-
).toEqual([
108-
{ is_covered: false, start_line: 1, end_line: 3, total_lines: 3 },
109-
{ is_covered: true, start_line: 4, end_line: 7, total_lines: 4 },
110-
])
104+
).toEqual([{ is_covered: true, start_line: 1, end_line: 6, total_lines: 6 }])
111105
})
112106

113-
test('trailing line comment is marked as uncovered', async () => {
107+
test('trailing line comment is marked as covered', async () => {
114108
let css = `
115109
h1 { color: blue; }
116110
/* start comment */
@@ -125,13 +119,10 @@ test.describe('comment coverage', () => {
125119
end_line,
126120
total_lines,
127121
})),
128-
).toEqual([
129-
{ is_covered: true, start_line: 1, end_line: 4, total_lines: 4 },
130-
{ is_covered: false, start_line: 5, end_line: 5, total_lines: 1 },
131-
])
122+
).toEqual([{ is_covered: true, start_line: 1, end_line: 4, total_lines: 4 }])
132123
})
133124

134-
test('trailing block comment is marked as uncovered', async () => {
125+
test('trailing block comment is marked as covered', async () => {
135126
let css = `
136127
h1 { color: blue; }
137128
/*
@@ -141,6 +132,25 @@ test.describe('comment coverage', () => {
141132
let coverage = (await generate_coverage(html, { link_css: css })) as Coverage[]
142133
let result = calculate_coverage(coverage)
143134
let sheet = result.coverage_per_stylesheet.at(0)!
135+
expect(
136+
sheet.chunks.map(({ is_covered, start_line, end_line, total_lines }) => ({
137+
is_covered,
138+
start_line,
139+
end_line,
140+
total_lines,
141+
})),
142+
).toEqual([{ is_covered: true, start_line: 1, end_line: 6, total_lines: 6 }])
143+
})
144+
145+
test('comment between covered and uncovered rule is marked as covered', async () => {
146+
let css = `
147+
h1 { color: blue; }
148+
/* middle comment */
149+
h2 { color: red; }
150+
`
151+
let coverage = (await generate_coverage(html, { link_css: css })) as Coverage[]
152+
let result = calculate_coverage(coverage)
153+
let sheet = result.coverage_per_stylesheet.at(0)!
144154
expect(
145155
sheet.chunks.map(({ is_covered, start_line, end_line, total_lines }) => ({
146156
is_covered,
@@ -149,10 +159,39 @@ test.describe('comment coverage', () => {
149159
total_lines,
150160
})),
151161
).toEqual([
152-
{ is_covered: true, start_line: 1, end_line: 4, total_lines: 4 },
153-
{ is_covered: false, start_line: 5, end_line: 7, total_lines: 3 },
162+
{ is_covered: true, start_line: 1, end_line: 5, total_lines: 5 },
163+
{ is_covered: false, start_line: 6, end_line: 8, total_lines: 3 },
154164
])
155165
})
166+
167+
test('multiple adjacent comments are each marked as covered', async () => {
168+
let css = `
169+
/* first comment */
170+
/* second comment */
171+
h1 { color: blue; }
172+
`
173+
let coverage = (await generate_coverage(html, { link_css: css })) as Coverage[]
174+
let result = calculate_coverage(coverage)
175+
let sheet = result.coverage_per_stylesheet.at(0)!
176+
expect(
177+
sheet.chunks.map(({ is_covered, start_line, end_line, total_lines }) => ({
178+
is_covered,
179+
start_line,
180+
end_line,
181+
total_lines,
182+
})),
183+
).toEqual([{ is_covered: true, start_line: 1, end_line: 5, total_lines: 5 }])
184+
})
185+
186+
test('stylesheet with only comments is fully covered', async () => {
187+
let css = `
188+
/* just a comment */
189+
`
190+
let coverage = (await generate_coverage(html, { link_css: css })) as Coverage[]
191+
let result = calculate_coverage(coverage)
192+
let sheet = result.coverage_per_stylesheet.at(0)!
193+
expect(sheet.chunks.every(({ is_covered }) => is_covered)).toBe(true)
194+
})
156195
})
157196

158197
test.describe('@rules', () => {

0 commit comments

Comments
 (0)