Skip to content

Commit 727926e

Browse files
committed
fix(models): issue file positions must be positive integers
1 parent 36d3eea commit 727926e

12 files changed

+108
-129
lines changed

packages/models/docs/models-reference.md

+8-8
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ _Object containing the following properties:_
4141
| **`slug`** (\*) | Reference to audit | `string` (_regex: `/^[a-z\d]+(?:-[a-z\d]+)*$/`, max length: 128_) |
4242
| **`title`** (\*) | Descriptive name | `string` (_max length: 256_) |
4343
| `description` | Description (markdown) | `string` (_max length: 65536_) |
44-
| `docsUrl` | Link to documentation (rationale) | `string` (_url_) (_optional_) _or_ `string` (_max length: 0_) |
44+
| `docsUrl` | Link to documentation (rationale) | `string` (_url_) (_optional_) _or_ `''` |
4545
| `displayValue` | Formatted value (e.g. '0.9 s', '2.1 MB') | `string` |
4646
| **`value`** (\*) | Raw numeric value | `number` (_int, ≥0_) |
4747
| **`score`** (\*) | Value between 0 and 1 | `number` (_≥0, ≤1_) |
@@ -58,7 +58,7 @@ _Object containing the following properties:_
5858
| **`slug`** (\*) | ID (unique within plugin) | `string` (_regex: `/^[a-z\d]+(?:-[a-z\d]+)*$/`, max length: 128_) |
5959
| **`title`** (\*) | Descriptive name | `string` (_max length: 256_) |
6060
| `description` | Description (markdown) | `string` (_max length: 65536_) |
61-
| `docsUrl` | Link to documentation (rationale) | `string` (_url_) (_optional_) _or_ `string` (_max length: 0_) |
61+
| `docsUrl` | Link to documentation (rationale) | `string` (_url_) (_optional_) _or_ `''` |
6262

6363
_(\*) Required._
6464

@@ -72,7 +72,7 @@ _Object containing the following properties:_
7272
| **`refs`** (\*) | | _Array of at least 1 [CategoryRef](#categoryref) items_ |
7373
| **`title`** (\*) | Category Title | `string` (_max length: 256_) |
7474
| `description` | Category description | `string` (_max length: 65536_) |
75-
| `docsUrl` | Category docs URL | `string` (_url_) (_optional_) _or_ `string` (_max length: 0_) |
75+
| `docsUrl` | Category docs URL | `string` (_url_) (_optional_) _or_ `''` |
7676
| `isBinary` | Is this a binary category (i.e. only a perfect score considered a "pass")? | `boolean` |
7777

7878
_(\*) Required._
@@ -133,7 +133,7 @@ _Object containing the following properties:_
133133
| **`refs`** (\*) | | _Array of at least 1 [GroupRef](#groupref) items_ |
134134
| **`title`** (\*) | Descriptive name for the group | `string` (_max length: 256_) |
135135
| `description` | Description of the group (markdown) | `string` (_max length: 65536_) |
136-
| `docsUrl` | Group documentation site | `string` (_url_) (_optional_) _or_ `string` (_max length: 0_) |
136+
| `docsUrl` | Group documentation site | `string` (_url_) (_optional_) _or_ `''` |
137137

138138
_(\*) Required._
139139

@@ -147,7 +147,7 @@ _Object containing the following properties:_
147147
| :------------------ | :------------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
148148
| **`message`** (\*) | Descriptive error message | `string` (_max length: 1024_) |
149149
| **`severity`** (\*) | Severity level | [IssueSeverity](#issueseverity) |
150-
| `source` | Source file location | _Object with properties:_<ul><li>`file`: `string` (_min length: 1_) - Relative path to source file in Git repo</li><li>`position`: _Object with properties:_<ul><li>`startLine`: `number` (_int, 0_) - Start line</li><li>`startColumn`: `number` (_int, 0_) - Start column</li><li>`endLine`: `number` (_int, 0_) - End line</li><li>`endColumn`: `number` (_int, 0_) - End column</li></ul> - Location in file</li></ul> |
150+
| `source` | Source file location | _Object with properties:_<ul><li>`file`: `string` (_min length: 1_) - Relative path to source file in Git repo</li><li>`position`: _Object with properties:_<ul><li>`startLine`: `number` (_int, >0_) - Start line</li><li>`startColumn`: `number` (_int, >0_) - Start column</li><li>`endLine`: `number` (_int, >0_) - End line</li><li>`endColumn`: `number` (_int, >0_) - End column</li></ul> - Location in file</li></ul> |
151151

152152
_(\*) Required._
153153

@@ -1046,7 +1046,7 @@ _Object containing the following properties:_
10461046
| `version` | NPM version of the package | `string` |
10471047
| **`title`** (\*) | Descriptive name | `string` (_max length: 256_) |
10481048
| `description` | Description (markdown) | `string` (_max length: 65536_) |
1049-
| `docsUrl` | Plugin documentation site | `string` (_url_) (_optional_) _or_ `string` (_max length: 0_) |
1049+
| `docsUrl` | Plugin documentation site | `string` (_url_) (_optional_) _or_ `''` |
10501050
| **`slug`** (\*) | Unique plugin slug within core config | `string` (_regex: `/^[a-z\d]+(?:-[a-z\d]+)*$/`, max length: 128_) |
10511051
| **`icon`** (\*) | Icon from VSCode Material Icons extension | [MaterialIcon](#materialicon) |
10521052
| **`runner`** (\*) | | [RunnerConfig](#runnerconfig) _or_ [RunnerFunction](#runnerfunction) |
@@ -1065,7 +1065,7 @@ _Object containing the following properties:_
10651065
| `version` | NPM version of the package | `string` |
10661066
| **`title`** (\*) | Descriptive name | `string` (_max length: 256_) |
10671067
| `description` | Description (markdown) | `string` (_max length: 65536_) |
1068-
| `docsUrl` | Plugin documentation site | `string` (_url_) (_optional_) _or_ `string` (_max length: 0_) |
1068+
| `docsUrl` | Plugin documentation site | `string` (_url_) (_optional_) _or_ `''` |
10691069
| **`slug`** (\*) | Unique plugin slug within core config | `string` (_regex: `/^[a-z\d]+(?:-[a-z\d]+)*$/`, max length: 128_) |
10701070
| **`icon`** (\*) | Icon from VSCode Material Icons extension | [MaterialIcon](#materialicon) |
10711071

@@ -1081,7 +1081,7 @@ _Object containing the following properties:_
10811081
| `version` | NPM version of the package | `string` |
10821082
| **`title`** (\*) | Descriptive name | `string` (_max length: 256_) |
10831083
| `description` | Description (markdown) | `string` (_max length: 65536_) |
1084-
| `docsUrl` | Plugin documentation site | `string` (_url_) (_optional_) _or_ `string` (_max length: 0_) |
1084+
| `docsUrl` | Plugin documentation site | `string` (_url_) (_optional_) _or_ `''` |
10851085
| **`slug`** (\*) | Unique plugin slug within core config | `string` (_regex: `/^[a-z\d]+(?:-[a-z\d]+)*$/`, max length: 128_) |
10861086
| **`icon`** (\*) | Icon from VSCode Material Icons extension | [MaterialIcon](#materialicon) |
10871087
| **`date`** (\*) | Start date and time of plugin run | `string` |

packages/models/src/index.ts

+1-7
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,7 @@ export {
3030
MAX_SLUG_LENGTH,
3131
MAX_TITLE_LENGTH,
3232
} from './lib/implementation/limits';
33-
export {
34-
MaterialIcon,
35-
fileNameSchema,
36-
filePathSchema,
37-
materialIconSchema,
38-
urlSchema,
39-
} from './lib/implementation/schemas';
33+
export { MaterialIcon, materialIconSchema } from './lib/implementation/schemas';
4034
export { exists } from './lib/implementation/utils';
4135
export {
4236
Issue,

packages/models/src/lib/audit-output.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { z } from 'zod';
2-
import { positiveIntSchema, slugSchema } from './implementation/schemas';
2+
import { nonnegativeIntSchema, slugSchema } from './implementation/schemas';
33
import { errorItems, hasDuplicateStrings } from './implementation/utils';
44
import { issueSchema } from './issue';
55

@@ -13,11 +13,11 @@ export type AuditDetails = z.infer<typeof auditDetailsSchema>;
1313

1414
export const auditOutputSchema = z.object(
1515
{
16-
slug: slugSchema('Reference to audit'),
16+
slug: slugSchema.describe('Reference to audit'),
1717
displayValue: z
1818
.string({ description: "Formatted value (e.g. '0.9 s', '2.1 MB')" })
1919
.optional(),
20-
value: positiveIntSchema('Raw numeric value'),
20+
value: nonnegativeIntSchema.describe('Raw numeric value'),
2121
score: z
2222
.number({
2323
description: 'Value between 0 and 1',

packages/models/src/lib/audit.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { errorItems, hasDuplicateStrings } from './implementation/utils';
44

55
export const auditSchema = z
66
.object({
7-
slug: slugSchema('ID (unique within plugin)'),
7+
slug: slugSchema.describe('ID (unique within plugin)'),
88
})
99
.merge(
1010
metaSchema({

packages/models/src/lib/category-config.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export const categoryRefSchema = weightedRefSchema(
1616
description:
1717
'Discriminant for reference kind, affects where `slug` is looked up',
1818
}),
19-
plugin: slugSchema(
19+
plugin: slugSchema.describe(
2020
'Plugin slug (plugin should contain referenced audit or group)',
2121
),
2222
}),

packages/models/src/lib/implementation/schemas.ts

+64-94
Original file line numberDiff line numberDiff line change
@@ -25,55 +25,36 @@ export function executionMetaSchema(
2525
});
2626
}
2727

28-
/**
29-
* Schema for a slug of a categories, plugins or audits.
30-
* @param description
31-
*/
32-
export function slugSchema(
33-
description = 'Unique ID (human-readable, URL-safe)',
34-
) {
35-
return z
36-
.string({ description })
37-
.regex(slugRegex, {
38-
message:
39-
'The slug has to follow the pattern [0-9a-z] followed by multiple optional groups of -[0-9a-z]. e.g. my-slug',
40-
})
41-
.max(MAX_SLUG_LENGTH, {
42-
message: `slug can be max ${MAX_SLUG_LENGTH} characters long`,
43-
});
44-
}
28+
/** Schema for a slug of a categories, plugins or audits. */
29+
export const slugSchema = z
30+
.string({ description: 'Unique ID (human-readable, URL-safe)' })
31+
.regex(slugRegex, {
32+
message:
33+
'The slug has to follow the pattern [0-9a-z] followed by multiple optional groups of -[0-9a-z]. e.g. my-slug',
34+
})
35+
.max(MAX_SLUG_LENGTH, {
36+
message: `slug can be max ${MAX_SLUG_LENGTH} characters long`,
37+
});
4538

46-
/**
47-
* Schema for a general description property
48-
* @param description
49-
*/
50-
export function descriptionSchema(description = 'Description (markdown)') {
51-
return z.string({ description }).max(MAX_DESCRIPTION_LENGTH).optional();
52-
}
39+
/** Schema for a general description property */
40+
export const descriptionSchema = z
41+
.string({ description: 'Description (markdown)' })
42+
.max(MAX_DESCRIPTION_LENGTH)
43+
.optional();
5344

54-
/**
55-
* Schema for a docsUrl
56-
* @param description
57-
*/
58-
export function docsUrlSchema(description = 'Documentation site') {
59-
return urlSchema(description).optional().or(z.string().max(0)); // allow empty string (no URL validation)
60-
}
45+
/* Schema for a URL */
46+
export const urlSchema = z.string().url();
6147

62-
/**
63-
* Schema for a URL
64-
* @param description
65-
*/
66-
export function urlSchema(description: string) {
67-
return z.string({ description }).url();
68-
}
48+
/** Schema for a docsUrl */
49+
export const docsUrlSchema = urlSchema
50+
.optional()
51+
.or(z.literal(''))
52+
.describe('Documentation site'); // allow empty string (no URL validation)
6953

70-
/**
71-
* Schema for a title of a plugin, category and audit
72-
* @param description
73-
*/
74-
export function titleSchema(description = 'Descriptive name') {
75-
return z.string({ description }).max(MAX_TITLE_LENGTH);
76-
}
54+
/** Schema for a title of a plugin, category and audit */
55+
export const titleSchema = z
56+
.string({ description: 'Descriptive name' })
57+
.max(MAX_TITLE_LENGTH);
7758

7859
/**
7960
* Used for categories, plugins and audits
@@ -93,46 +74,39 @@ export function metaSchema(options?: {
9374
} = options ?? {};
9475
return z.object(
9576
{
96-
title: titleSchema(titleDescription),
97-
description: descriptionSchema(descriptionDescription),
98-
docsUrl: docsUrlSchema(docsUrlDescription),
77+
title: titleDescription
78+
? titleSchema.describe(titleDescription)
79+
: titleSchema,
80+
description: descriptionDescription
81+
? descriptionSchema.describe(descriptionDescription)
82+
: descriptionSchema,
83+
docsUrl: docsUrlDescription
84+
? docsUrlSchema.describe(docsUrlDescription)
85+
: docsUrlSchema,
9986
},
10087
{ description },
10188
);
10289
}
10390

104-
/**
105-
* Schema for a generalFilePath
106-
* @param description
107-
*/
108-
export function filePathSchema(description: string) {
109-
return z
110-
.string({ description })
111-
.trim()
112-
.min(1, { message: 'path is invalid' });
113-
}
91+
/** Schema for a generalFilePath */
92+
export const filePathSchema = z
93+
.string()
94+
.trim()
95+
.min(1, { message: 'path is invalid' });
11496

115-
/**
116-
* Schema for a fileNameSchema
117-
* @param description
118-
*/
119-
export function fileNameSchema(description: string) {
120-
return z
121-
.string({ description })
122-
.trim()
123-
.regex(filenameRegex, {
124-
message: `The filename has to be valid`,
125-
})
126-
.min(1, { message: 'file name is invalid' });
127-
}
97+
/** Schema for a fileNameSchema */
98+
export const fileNameSchema = z
99+
.string()
100+
.trim()
101+
.regex(filenameRegex, {
102+
message: `The filename has to be valid`,
103+
})
104+
.min(1, { message: 'file name is invalid' });
128105

129-
/**
130-
* Schema for a positiveInt
131-
* @param description
132-
*/
133-
export function positiveIntSchema(description: string) {
134-
return z.number({ description }).int().nonnegative();
135-
}
106+
/** Schema for a positiveInt */
107+
export const positiveIntSchema = z.number().int().positive();
108+
109+
export const nonnegativeIntSchema = z.number().int().nonnegative();
136110

137111
export function packageVersionSchema<TRequired extends boolean>(options?: {
138112
versionDescription?: string;
@@ -154,24 +128,19 @@ export function packageVersionSchema<TRequired extends boolean>(options?: {
154128
}>;
155129
}
156130

157-
/**
158-
* Schema for a weight
159-
* @param description
160-
*/
161-
export function weightSchema(
162-
description = 'Coefficient for the given score (use weight 0 if only for display)',
163-
) {
164-
return positiveIntSchema(description);
165-
}
131+
/** Schema for a weight */
132+
export const weightSchema = nonnegativeIntSchema.describe(
133+
'Coefficient for the given score (use weight 0 if only for display)',
134+
);
166135

167136
export function weightedRefSchema(
168137
description: string,
169138
slugDescription: string,
170139
) {
171140
return z.object(
172141
{
173-
slug: slugSchema(slugDescription),
174-
weight: weightSchema('Weight used to calculate score'),
142+
slug: slugSchema.describe(slugDescription),
143+
weight: weightSchema.describe('Weight used to calculate score'),
175144
},
176145
{ description },
177146
);
@@ -187,7 +156,7 @@ export function scorableSchema<T extends ReturnType<typeof weightedRefSchema>>(
187156
) {
188157
return z.object(
189158
{
190-
slug: slugSchema('Human-readable unique ID, e.g. "performance"'),
159+
slug: slugSchema.describe('Human-readable unique ID, e.g. "performance"'),
191160
refs: z
192161
.array(refSchema)
193162
.min(1)
@@ -199,8 +168,9 @@ export function scorableSchema<T extends ReturnType<typeof weightedRefSchema>>(
199168
}),
200169
)
201170
// categories weights are correct
202-
.refine(hasWeightedRefsInCategories, () => ({
203-
message: `In a category there has to be at least one ref with weight > 0`,
171+
.refine(hasNonZeroWeightedRef, () => ({
172+
message:
173+
'In a category there has to be at least one ref with weight > 0',
204174
})),
205175
},
206176
{ description },
@@ -214,6 +184,6 @@ export type MaterialIcon = z.infer<typeof materialIconSchema>;
214184

215185
type Ref = { weight: number };
216186

217-
function hasWeightedRefsInCategories(categoryRefs: Ref[]) {
218-
return categoryRefs.reduce((acc, { weight }) => weight + acc, 0) !== 0;
187+
function hasNonZeroWeightedRef(refs: Ref[]) {
188+
return refs.reduce((acc, { weight }) => weight + acc, 0) !== 0;
219189
}

packages/models/src/lib/issue.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ import { filePathSchema, positiveIntSchema } from './implementation/schemas';
44

55
const sourceFileLocationSchema = z.object(
66
{
7-
file: filePathSchema('Relative path to source file in Git repo'),
7+
file: filePathSchema.describe('Relative path to source file in Git repo'),
88
position: z
99
.object(
1010
{
11-
startLine: positiveIntSchema('Start line'),
12-
startColumn: positiveIntSchema('Start column').optional(),
13-
endLine: positiveIntSchema('End line').optional(),
14-
endColumn: positiveIntSchema('End column').optional(),
11+
startLine: positiveIntSchema.describe('Start line'),
12+
startColumn: positiveIntSchema.describe('Start column').optional(),
13+
endLine: positiveIntSchema.describe('End line').optional(),
14+
endColumn: positiveIntSchema.describe('End column').optional(),
1515
},
1616
{ description: 'Location in file' },
1717
)

packages/models/src/lib/issue.unit.test.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ describe('issueSchema', () => {
1818
severity: 'error',
1919
source: {
2020
file: 'my/code/index.ts',
21-
position: { startLine: 0, startColumn: 4, endLine: 1, endColumn: 10 },
21+
position: { startLine: 1, startColumn: 4, endLine: 1, endColumn: 10 },
2222
},
2323
} satisfies Issue),
2424
).not.toThrow();
@@ -41,4 +41,17 @@ describe('issueSchema', () => {
4141
}),
4242
).toThrow('Invalid enum value');
4343
});
44+
45+
it('should throw for invalid file position', () => {
46+
expect(() =>
47+
issueSchema.parse({
48+
message: 'Use const instead of let.',
49+
severity: 'warning',
50+
source: {
51+
file: 'src/utils.ts',
52+
position: { startLine: 0, endLine: 3 },
53+
},
54+
} satisfies Issue),
55+
).toThrow('Number must be greater than 0');
56+
});
4457
});

0 commit comments

Comments
 (0)