@orkestrel/scaffold 0.0.63 → 0.0.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -104
- package/dist/bin/main.js +95 -27
- package/dist/bin/main.js.map +1 -1
- package/dist/host/AGENTS.md +2 -2
- package/dist/host/CLAUDE.md +6 -0
- package/dist/host/agents/orchestration.md +23 -15
- package/dist/host/agents/skills/enterprise-bootstrap/SKILL.md +184 -177
- package/dist/host/agents/skills/enterprise-bootstrap/references/bootstrap-reference.md +314 -91
- package/dist/host/agents/skills/enterprise-bootstrap/references/color-modes.md +241 -0
- package/dist/host/agents/skills/enterprise-bootstrap/references/components.md +83 -36
- package/dist/host/agents/skills/enterprise-bootstrap/references/frontend-design.md +297 -98
- package/dist/host/agents/skills/enterprise-bootstrap/references/inputs.md +25 -14
- package/dist/host/agents/skills/enterprise-bootstrap/references/inspection.md +216 -128
- package/dist/host/agents/skills/enterprise-bootstrap/references/responsive-layout.md +187 -0
- package/dist/host/agents/skills/enterprise-bootstrap/references/utilities.md +109 -20
- package/dist/host/agents/skills/orkestrel-debrief/references/field-testing.md +5 -5
- package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +1 -1
- package/dist/host/agents/skills/orkestrel-publish/SKILL.md +15 -15
- package/dist/host/agents/skills/orkestrel-publish/references/wave.md +43 -17
- package/dist/host/agents/skills/orkestrel-publish/references/window.md +41 -16
- package/dist/host/claude/agents/orkestrel.md +56 -56
- package/dist/host/claude/agents/reviewer.md +13 -0
- package/dist/host/claude/rules/architecture.md +51 -45
- package/dist/host/claude/rules/documentation.md +18 -1
- package/dist/host/claude/rules/portability.md +2 -0
- package/dist/host/claude/rules/quality.md +1 -1
- package/dist/host/claude/rules/tests.md +12 -11
- package/dist/host/claude/rules/typescript.md +5 -0
- package/dist/host/claude/rules/workspace.md +25 -20
- package/dist/host/claude/rules/writing.md +4 -0
- package/dist/host/claude/settings.json +1 -1
- package/dist/host/claude/skills/enterprise-bootstrap/SKILL.md +10 -9
- package/dist/host/codex/agents/orkestrel.toml +3 -3
- package/dist/host/codex/agents/reviewer.toml +4 -2
- package/dist/host/configs/helpers.ts +311 -2
- package/dist/host/configs/policy.ts +1100 -51
- package/dist/host/dotfiles/oxlintrc.json +72 -1
- package/dist/host/guides/guide.md +749 -222
- package/dist/host/guides/scaffold.md +529 -394
- package/dist/host/manifest.json +53 -40
- package/dist/host/scripts/ollama.sh +322 -13
- package/dist/host/tests/config.test.ts +1200 -16
- package/dist/host/tests/policy.test.ts +157 -173
- package/dist/host/tests/setupPolicy.ts +522 -1007
- package/dist/src/core/index.cjs +402 -287
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +160 -128
- package/dist/src/core/index.d.ts +160 -128
- package/dist/src/core/index.js +400 -286
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +28 -21
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +38 -33
- package/dist/src/server/index.d.ts +38 -33
- package/dist/src/server/index.js +28 -21
- package/dist/src/server/index.js.map +1 -1
- package/package.json +18 -19
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Describes the syntax-node fields supplied to every policy visitor. */
|
|
2
2
|
export interface PolicyNode {
|
|
3
3
|
readonly type: string
|
|
4
4
|
readonly range: [number, number]
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
/**
|
|
7
|
+
/** Describes the expression fields the policy rules inspect. */
|
|
8
8
|
export interface PolicyExpression extends PolicyNode {
|
|
9
9
|
readonly parent?: PolicyExpression | null
|
|
10
10
|
readonly name?: unknown
|
|
11
11
|
readonly value?: unknown
|
|
12
12
|
readonly key?: PolicyExpression
|
|
13
|
-
readonly id?:
|
|
13
|
+
readonly id?: PolicyExpression | null
|
|
14
14
|
readonly method?: boolean
|
|
15
|
-
readonly kind?: 'get' | 'init' | 'set'
|
|
15
|
+
readonly kind?: 'const' | 'get' | 'init' | 'let' | 'set' | 'var'
|
|
16
16
|
readonly expression?: boolean
|
|
17
17
|
readonly object?: PolicyExpression
|
|
18
18
|
readonly property?: PolicyExpression
|
|
@@ -20,59 +20,484 @@ export interface PolicyExpression extends PolicyNode {
|
|
|
20
20
|
readonly callee?: PolicyExpression
|
|
21
21
|
readonly argument?: PolicyExpression | null
|
|
22
22
|
readonly arguments?: readonly PolicyExpression[]
|
|
23
|
-
readonly body?: PolicyExpression
|
|
23
|
+
readonly body?: PolicyExpression | readonly PolicyExpression[]
|
|
24
24
|
readonly quasis?: readonly PolicyExpression[]
|
|
25
25
|
readonly expressions?: readonly PolicyExpression[]
|
|
26
26
|
readonly accessibility?: 'private' | 'protected' | 'public' | null
|
|
27
|
+
readonly declaration?: PolicyExpression
|
|
28
|
+
readonly declarations?: readonly PolicyExpression[]
|
|
29
|
+
readonly init?: PolicyExpression | null
|
|
30
|
+
readonly source?: PolicyExpression
|
|
31
|
+
readonly specifiers?: readonly PolicyExpression[]
|
|
32
|
+
readonly imported?: PolicyExpression
|
|
27
33
|
}
|
|
28
34
|
|
|
29
|
-
/**
|
|
30
|
-
export interface
|
|
35
|
+
/** Pairs one declared module function with the name a prefix rule reads. */
|
|
36
|
+
export interface PolicyBinding {
|
|
31
37
|
readonly node: PolicyExpression
|
|
38
|
+
readonly name: string | undefined
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Describes one comment the comment rules read out of a linted file. */
|
|
42
|
+
export interface PolicyComment extends PolicyNode {
|
|
43
|
+
readonly type: 'Block' | 'Line' | 'Shebang'
|
|
44
|
+
readonly value: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Lists the Oxlint source-text operations the comment rules read. */
|
|
48
|
+
export interface PolicySourceCode {
|
|
49
|
+
readonly text: string
|
|
50
|
+
getAllComments(): readonly PolicyComment[]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Pairs one doc block with the declared name its first sentence must not repeat. */
|
|
54
|
+
export interface PolicyDoc {
|
|
55
|
+
readonly comment: PolicyComment
|
|
56
|
+
readonly name: string | undefined
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Describes one banned term, the prose it matches, and the replacement its row names. */
|
|
60
|
+
export interface PolicyTerm {
|
|
61
|
+
readonly term: string
|
|
62
|
+
readonly pattern: RegExp
|
|
63
|
+
readonly replacement: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Pairs one banned-term match with the offset it starts at. */
|
|
67
|
+
export interface PolicyHit {
|
|
68
|
+
readonly term: PolicyTerm
|
|
69
|
+
readonly index: number
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Describes one diagnostic a policy rule emits. */
|
|
73
|
+
export interface PolicyDiagnostic {
|
|
74
|
+
readonly node: PolicyNode
|
|
32
75
|
readonly messageId: string
|
|
33
76
|
readonly data?: Readonly<Record<string, string>>
|
|
34
77
|
}
|
|
35
78
|
|
|
36
|
-
/**
|
|
79
|
+
/** Lists the Oxlint context operations the policy rules use. */
|
|
37
80
|
export interface PolicyContext {
|
|
81
|
+
readonly filename: string
|
|
82
|
+
/** Names the directory Oxlint resolves `filename` against. */
|
|
83
|
+
readonly cwd: string
|
|
84
|
+
readonly sourceCode: PolicySourceCode
|
|
38
85
|
report(diagnostic: PolicyDiagnostic): void
|
|
39
86
|
}
|
|
40
87
|
|
|
41
|
-
/**
|
|
88
|
+
/** Describes the rule documentation fields supplied to Oxlint. */
|
|
42
89
|
export interface PolicyDocs {
|
|
43
90
|
readonly [key: string]: unknown
|
|
44
91
|
readonly description: string
|
|
45
92
|
}
|
|
46
93
|
|
|
47
|
-
/**
|
|
94
|
+
/** Describes the rule metadata fields supplied to Oxlint. */
|
|
48
95
|
export interface PolicyMeta {
|
|
49
96
|
readonly type: 'problem'
|
|
50
97
|
readonly docs: PolicyDocs
|
|
51
98
|
readonly messages: Readonly<Record<string, string>>
|
|
52
99
|
}
|
|
53
100
|
|
|
54
|
-
/**
|
|
101
|
+
/** Lists the Oxlint visitor entries the policy rules use. */
|
|
55
102
|
export interface PolicyVisitor {
|
|
56
103
|
readonly [key: string]: ((node: PolicyNode) => void) | undefined
|
|
104
|
+
readonly Program?: (node: PolicyNode) => void
|
|
57
105
|
readonly CallExpression?: (node: PolicyNode) => void
|
|
106
|
+
readonly ClassDeclaration?: (node: PolicyNode) => void
|
|
58
107
|
readonly FunctionDeclaration?: (node: PolicyNode) => void
|
|
59
108
|
readonly FunctionExpression?: (node: PolicyNode) => void
|
|
60
109
|
readonly ArrowFunctionExpression?: (node: PolicyNode) => void
|
|
110
|
+
readonly ImportDeclaration?: (node: PolicyNode) => void
|
|
111
|
+
readonly MemberExpression?: (node: PolicyNode) => void
|
|
61
112
|
readonly MethodDefinition?: (node: PolicyNode) => void
|
|
62
113
|
readonly PropertyDefinition?: (node: PolicyNode) => void
|
|
63
114
|
readonly AccessorProperty?: (node: PolicyNode) => void
|
|
64
115
|
readonly TSAbstractMethodDefinition?: (node: PolicyNode) => void
|
|
65
116
|
readonly TSAbstractPropertyDefinition?: (node: PolicyNode) => void
|
|
66
117
|
readonly TSAbstractAccessorProperty?: (node: PolicyNode) => void
|
|
118
|
+
readonly TSDeclareFunction?: (node: PolicyNode) => void
|
|
119
|
+
readonly TSEnumDeclaration?: (node: PolicyNode) => void
|
|
120
|
+
readonly TSInterfaceDeclaration?: (node: PolicyNode) => void
|
|
121
|
+
readonly TSModuleDeclaration?: (node: PolicyNode) => void
|
|
122
|
+
readonly TSTypeAliasDeclaration?: (node: PolicyNode) => void
|
|
123
|
+
readonly VariableDeclaration?: (node: PolicyNode) => void
|
|
67
124
|
}
|
|
68
125
|
|
|
69
|
-
/**
|
|
126
|
+
/** Describes the complete behavior one policy rule exposes. */
|
|
70
127
|
export interface PolicyRuleInterface {
|
|
71
128
|
readonly meta: PolicyMeta
|
|
72
129
|
create(context: PolicyContext): PolicyVisitor
|
|
73
130
|
}
|
|
74
131
|
|
|
75
|
-
/**
|
|
132
|
+
/** Lists every centralized module the architecture kind table names. */
|
|
133
|
+
export const CENTRAL_SOURCE_FILES: readonly string[] = Object.freeze([
|
|
134
|
+
'cloners.ts',
|
|
135
|
+
'combinators.ts',
|
|
136
|
+
'compilers.ts',
|
|
137
|
+
'constants.ts',
|
|
138
|
+
'contracts.ts',
|
|
139
|
+
'errors.ts',
|
|
140
|
+
'factories.ts',
|
|
141
|
+
'handlers.ts',
|
|
142
|
+
'helpers.ts',
|
|
143
|
+
'index.ts',
|
|
144
|
+
'inferers.ts',
|
|
145
|
+
'middlewares.ts',
|
|
146
|
+
'parsers.ts',
|
|
147
|
+
'relations.ts',
|
|
148
|
+
'routes.ts',
|
|
149
|
+
'schemas.ts',
|
|
150
|
+
'seeders.ts',
|
|
151
|
+
'shapers.ts',
|
|
152
|
+
'templates.ts',
|
|
153
|
+
'types.ts',
|
|
154
|
+
'validators.ts',
|
|
155
|
+
])
|
|
156
|
+
|
|
157
|
+
/** Lists the exhaustive centralized-file set that permits module functions. */
|
|
158
|
+
export const FUNCTION_SOURCE_FILES: readonly string[] = Object.freeze([
|
|
159
|
+
'cloners.ts',
|
|
160
|
+
'combinators.ts',
|
|
161
|
+
'compilers.ts',
|
|
162
|
+
'errors.ts',
|
|
163
|
+
'factories.ts',
|
|
164
|
+
'handlers.ts',
|
|
165
|
+
'helpers.ts',
|
|
166
|
+
'inferers.ts',
|
|
167
|
+
'middlewares.ts',
|
|
168
|
+
'parsers.ts',
|
|
169
|
+
'relations.ts',
|
|
170
|
+
'schemas.ts',
|
|
171
|
+
'seeders.ts',
|
|
172
|
+
'shapers.ts',
|
|
173
|
+
'validators.ts',
|
|
174
|
+
])
|
|
175
|
+
|
|
176
|
+
/** Lists the centralized files that permit module data by declaration syntax. */
|
|
177
|
+
export const DATA_SOURCE_FILES: readonly string[] = Object.freeze([
|
|
178
|
+
'combinators.ts',
|
|
179
|
+
'constants.ts',
|
|
180
|
+
'contracts.ts',
|
|
181
|
+
'relations.ts',
|
|
182
|
+
'routes.ts',
|
|
183
|
+
'schemas.ts',
|
|
184
|
+
'shapers.ts',
|
|
185
|
+
'templates.ts',
|
|
186
|
+
'validators.ts',
|
|
187
|
+
])
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Lists the files excluded from the module-data rule because their namespace values hold helper
|
|
191
|
+
* behavior. This exclusion also permits unrelated module data such as `export const RETRIES = 3`.
|
|
192
|
+
*/
|
|
193
|
+
export const DATA_EXEMPT_FILES: readonly string[] = Object.freeze(['helpers.ts'])
|
|
194
|
+
|
|
195
|
+
/** Lists the fleet-registered folders whose direct modules each contain one named function. */
|
|
196
|
+
export const FUNCTION_DOMAIN_FOLDERS: readonly string[] = Object.freeze([
|
|
197
|
+
'app/browser/composables',
|
|
198
|
+
'src/server/execution',
|
|
199
|
+
])
|
|
200
|
+
|
|
201
|
+
/** Lists the registered function-domain names no source file may take as its stem. */
|
|
202
|
+
export const FUNCTION_DOMAIN_NAMES: readonly string[] = Object.freeze(
|
|
203
|
+
FUNCTION_DOMAIN_FOLDERS.map((folder) => folder.slice(folder.lastIndexOf('/') + 1)),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
/** Lists every ambient declaration suffix the placement and line-ending rules leave uninspected. */
|
|
207
|
+
export const POLICY_AMBIENT_SUFFIXES: readonly string[] = Object.freeze([
|
|
208
|
+
'.d.cts',
|
|
209
|
+
'.d.mts',
|
|
210
|
+
'.d.ts',
|
|
211
|
+
])
|
|
212
|
+
|
|
213
|
+
/** Lists the TypeScript source extensions whose declaration syntax the placement rules read. */
|
|
214
|
+
export const POLICY_SOURCE_EXTENSIONS: readonly string[] = Object.freeze([
|
|
215
|
+
'cts',
|
|
216
|
+
'mts',
|
|
217
|
+
'ts',
|
|
218
|
+
'tsx',
|
|
219
|
+
])
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Matches the lint populations the placement rules run over, as the Oxlint configuration declares
|
|
223
|
+
* them.
|
|
224
|
+
*/
|
|
225
|
+
export const POLICY_PLACEMENT_GLOBS: readonly string[] = Object.freeze([
|
|
226
|
+
`app/**/*.{${POLICY_SOURCE_EXTENSIONS.join(',')}}`,
|
|
227
|
+
`src/**/*.{${POLICY_SOURCE_EXTENSIONS.join(',')}}`,
|
|
228
|
+
])
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Matches the lint population the line-ending rule runs over, as the Oxlint configuration declares
|
|
232
|
+
* it.
|
|
233
|
+
*/
|
|
234
|
+
export const POLICY_ENDING_GLOBS: readonly string[] = Object.freeze([
|
|
235
|
+
'app/**/*.ts',
|
|
236
|
+
'configs/**/*.ts',
|
|
237
|
+
'src/**/*.ts',
|
|
238
|
+
])
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Matches the file name shape an implementation file takes, holding the class that matches its
|
|
242
|
+
* stem.
|
|
243
|
+
*/
|
|
244
|
+
export const POLICY_CLASS_PATTERN = /^[A-Z][A-Za-z0-9]*\.ts$/u
|
|
245
|
+
|
|
246
|
+
/** Matches the name shape every constants.ts declaration takes. */
|
|
247
|
+
export const POLICY_CONSTANT_PATTERN = /^[A-Z][A-Z0-9_]*$/u
|
|
248
|
+
|
|
249
|
+
/** Matches the file name shape a direct module of a registered function domain takes. */
|
|
250
|
+
export const POLICY_DOMAIN_PATTERN = /^[a-z][A-Za-z0-9]*\.ts$/u
|
|
251
|
+
|
|
252
|
+
/** Matches a first word that reads as a third-person verb. */
|
|
253
|
+
export const POLICY_VOICE_PATTERN = /^[A-Z][a-z]*s$/u
|
|
254
|
+
|
|
255
|
+
/** Matches the boundary a description paragraph's first sentence ends at. */
|
|
256
|
+
export const POLICY_SENTENCE_PATTERN = /\.\s|\.$/u
|
|
257
|
+
|
|
258
|
+
/** Matches the continuation marker a doc block repeats on each line after its opening. */
|
|
259
|
+
export const POLICY_MARKER_PATTERN = /^\s*\*\s?/u
|
|
260
|
+
|
|
261
|
+
/** Matches one line break in any host's form. */
|
|
262
|
+
export const POLICY_BREAK_PATTERN = /\r\n|\r|\n/u
|
|
263
|
+
|
|
264
|
+
/** Matches a fenced code block, opening run through closing run. */
|
|
265
|
+
export const POLICY_FENCE_PATTERN = /^ {0,3}(`{3,}|~{3,})[^\n]*\n[\s\S]*?^ {0,3}\1[^\n]*$/gmu
|
|
266
|
+
|
|
267
|
+
/** Matches an inline code span, including one a line break runs through. */
|
|
268
|
+
export const POLICY_SPAN_PATTERN = /(`+)(?!`)[\s\S]*?[^`]\1(?!`)/gu
|
|
269
|
+
|
|
270
|
+
/** Matches a link or inherited-documentation tag, whose target is a symbol rather than prose. */
|
|
271
|
+
export const POLICY_TAG_PATTERN = /\{@(?:linkcode|linkplain|link|inheritDoc)\b[^}]*\}/giu
|
|
272
|
+
|
|
273
|
+
/** Matches a URL, whose segments are an address rather than prose. */
|
|
274
|
+
export const POLICY_URL_PATTERN = /https?:\/\/\S+/gu
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Lists the words ending in `s` that open a sentence without being a third-person verb.
|
|
278
|
+
*
|
|
279
|
+
* @remarks
|
|
280
|
+
* The voice rule reads a first word rather than a parsed verb, so a demonstrative, a pronoun, an
|
|
281
|
+
* adverb, and a singular noun ending in `s` each need naming here to stay refused.
|
|
282
|
+
*/
|
|
283
|
+
export const POLICY_VOICE_STOPWORDS: readonly string[] = Object.freeze([
|
|
284
|
+
'Access',
|
|
285
|
+
'Across',
|
|
286
|
+
'Address',
|
|
287
|
+
'Alias',
|
|
288
|
+
'Always',
|
|
289
|
+
'Analysis',
|
|
290
|
+
'Assess',
|
|
291
|
+
'Basis',
|
|
292
|
+
'Bias',
|
|
293
|
+
'Bus',
|
|
294
|
+
'Business',
|
|
295
|
+
'Canvas',
|
|
296
|
+
'Chaos',
|
|
297
|
+
'Class',
|
|
298
|
+
'Compress',
|
|
299
|
+
'Cross',
|
|
300
|
+
'Discuss',
|
|
301
|
+
'Dismiss',
|
|
302
|
+
'Express',
|
|
303
|
+
'Focus',
|
|
304
|
+
'Gas',
|
|
305
|
+
'Guess',
|
|
306
|
+
'Harness',
|
|
307
|
+
'Its',
|
|
308
|
+
'Lens',
|
|
309
|
+
'Miss',
|
|
310
|
+
'Numerous',
|
|
311
|
+
'Pass',
|
|
312
|
+
'Perhaps',
|
|
313
|
+
'Plus',
|
|
314
|
+
'Press',
|
|
315
|
+
'Previous',
|
|
316
|
+
'Process',
|
|
317
|
+
'Progress',
|
|
318
|
+
'Series',
|
|
319
|
+
'Sometimes',
|
|
320
|
+
'Status',
|
|
321
|
+
'Success',
|
|
322
|
+
'This',
|
|
323
|
+
'Thus',
|
|
324
|
+
'Unless',
|
|
325
|
+
'Various',
|
|
326
|
+
'Was',
|
|
327
|
+
'Whereas',
|
|
328
|
+
'Witness',
|
|
329
|
+
'Yes',
|
|
330
|
+
])
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Lists every substitution-table row whose ban is unconditional, beside its replacement.
|
|
334
|
+
*
|
|
335
|
+
* @remarks
|
|
336
|
+
* Each pattern is case-insensitive, word-bounded, and global, and carries the inflections its row
|
|
337
|
+
* reaches.
|
|
338
|
+
*/
|
|
339
|
+
export const POLICY_BANNED_TERMS: readonly PolicyTerm[] = Object.freeze([
|
|
340
|
+
{ term: 'should', pattern: /\bshould\b/giu, replacement: 'must, can, might, or the imperative' },
|
|
341
|
+
{ term: 'simply', pattern: /\bsimply\b/giu, replacement: 'delete' },
|
|
342
|
+
{ term: 'easy', pattern: /\beas(?:y|ier|iest|ily)\b/giu, replacement: 'delete' },
|
|
343
|
+
{ term: 'just', pattern: /\bjust\b/giu, replacement: 'delete' },
|
|
344
|
+
{ term: 'currently', pattern: /\bcurrently\b/giu, replacement: 'delete, or give the date' },
|
|
345
|
+
{ term: 'utilize', pattern: /\butiliz(?:e|es|ed|ing|ation)\b/giu, replacement: 'use' },
|
|
346
|
+
{ term: 'leverage', pattern: /\bleverag(?:e|es|ed|ing)\b/giu, replacement: 'use' },
|
|
347
|
+
{ term: 'via', pattern: /\bvia\b/giu, replacement: 'through, by using' },
|
|
348
|
+
{ term: 'in order to', pattern: /\bin order to\b/giu, replacement: 'to' },
|
|
349
|
+
{ term: 'e.g.', pattern: /\be\.g\./giu, replacement: 'for example' },
|
|
350
|
+
{ term: 'i.e.', pattern: /\bi\.e\./giu, replacement: 'that is' },
|
|
351
|
+
{ term: 'etc.', pattern: /\betc\./giu, replacement: 'bound the list, or recast the sentence' },
|
|
352
|
+
{ term: 'performant', pattern: /\bperformant\b/giu, replacement: 'the measured property' },
|
|
353
|
+
{ term: 'robust', pattern: /\brobust(?:ly|ness)?\b/giu, replacement: 'the measured property' },
|
|
354
|
+
{ term: 'allows you to', pattern: /\ballows you to\b/giu, replacement: 'lets you' },
|
|
355
|
+
{ term: 'and/or', pattern: /\band\/or\b/giu, replacement: 'and, or, or both' },
|
|
356
|
+
{ term: 'please', pattern: /\bplease\b/giu, replacement: 'delete' },
|
|
357
|
+
{ term: 'sanity check', pattern: /\bsanity[ -]check/giu, replacement: 'quick check' },
|
|
358
|
+
{ term: 'dummy', pattern: /\bdumm(?:y|ies)\b/giu, replacement: 'placeholder' },
|
|
359
|
+
{ term: 'blacklist', pattern: /\bblacklist(?:s|ed|ing)?\b/giu, replacement: 'denylist' },
|
|
360
|
+
{ term: 'whitelist', pattern: /\bwhitelist(?:s|ed|ing)?\b/giu, replacement: 'allowlist' },
|
|
361
|
+
{ term: 'slave', pattern: /\bslave\b/giu, replacement: 'replica' },
|
|
362
|
+
])
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Lists every substitution-table row a reader rules by sense, which no pattern matches.
|
|
366
|
+
*
|
|
367
|
+
* @remarks
|
|
368
|
+
* Each row carries a permitted sense: a date value, a version value, a causal clause, and the name
|
|
369
|
+
* a replication topology takes. The currency check proves each row is registered here.
|
|
370
|
+
*/
|
|
371
|
+
export const POLICY_JUDGED_TERMS: readonly string[] = Object.freeze([
|
|
372
|
+
'now',
|
|
373
|
+
'new',
|
|
374
|
+
'latest',
|
|
375
|
+
'once',
|
|
376
|
+
'since',
|
|
377
|
+
'master',
|
|
378
|
+
])
|
|
379
|
+
|
|
380
|
+
/** Returns the file name a policy rule keys on, read from either host separator. */
|
|
381
|
+
export function pathToPolicyFile(filename: string): string {
|
|
382
|
+
const normalized = filename.replaceAll('\\', '/')
|
|
383
|
+
return normalized.slice(normalized.lastIndexOf('/') + 1)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** Returns the folder path a policy rule keys on, read from either host separator. */
|
|
387
|
+
export function pathToPolicyFolder(filename: string): string {
|
|
388
|
+
const normalized = filename.replaceAll('\\', '/')
|
|
389
|
+
const boundary = normalized.lastIndexOf('/')
|
|
390
|
+
return boundary === -1 ? '' : normalized.slice(0, boundary)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Returns the workspace-relative path when the file sits under the directory the linter resolved it
|
|
395
|
+
* against, else the path as given. The linter resolves each file against its own directory, the
|
|
396
|
+
* workspace root under the `lint` scripts and its package directory under `RuleTester`, so a file
|
|
397
|
+
* outside that directory keeps its path and matches no registered domain folder, because a
|
|
398
|
+
* registered folder is workspace-relative. For a workspace at the filesystem root the prefix is the
|
|
399
|
+
* separator alone. A drive-letter case difference between the two arguments is not folded.
|
|
400
|
+
*/
|
|
401
|
+
export function pathToPolicyRelative(filename: string, cwd: string): string {
|
|
402
|
+
const normalizedFile = filename.replaceAll('\\', '/')
|
|
403
|
+
const normalizedCwd = cwd.replaceAll('\\', '/').replace(/\/+$/u, '')
|
|
404
|
+
const prefix = `${normalizedCwd}/`
|
|
405
|
+
return normalizedFile.startsWith(prefix) ? normalizedFile.slice(prefix.length) : normalizedFile
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** Returns the extensionless stem of one policy file name. */
|
|
409
|
+
export function fileToPolicyStem(file: string): string {
|
|
410
|
+
const boundary = file.lastIndexOf('.')
|
|
411
|
+
return boundary <= 0 ? file : file.slice(0, boundary)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** Reports whether a path names an ambient declaration file, which no policy rule inspects. */
|
|
415
|
+
export function isPolicyAmbient(filename: string): boolean {
|
|
416
|
+
const file = pathToPolicyFile(filename)
|
|
417
|
+
return POLICY_AMBIENT_SUFFIXES.some((suffix) => file.endsWith(suffix))
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Reports whether a path is a direct module of a fleet-registered function domain.
|
|
422
|
+
*
|
|
423
|
+
* The registered folder is a workspace-relative path, compared by equality after the linter's own
|
|
424
|
+
* directory is stripped from the given path.
|
|
425
|
+
*/
|
|
426
|
+
export function isPolicyDomain(filename: string, cwd: string): boolean {
|
|
427
|
+
const relative = pathToPolicyRelative(filename, cwd)
|
|
428
|
+
const file = pathToPolicyFile(relative)
|
|
429
|
+
const folder = pathToPolicyFolder(relative)
|
|
430
|
+
return (
|
|
431
|
+
FUNCTION_DOMAIN_FOLDERS.some((registered) => folder === registered) &&
|
|
432
|
+
POLICY_DOMAIN_PATTERN.test(file) &&
|
|
433
|
+
file !== 'index.ts' &&
|
|
434
|
+
file !== 'main.ts' &&
|
|
435
|
+
!CENTRAL_SOURCE_FILES.includes(file)
|
|
436
|
+
)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** Returns the identifier name a node carries, or `undefined` for any other syntax. */
|
|
440
|
+
export function identifierToPolicyName(
|
|
441
|
+
node: PolicyExpression | null | undefined,
|
|
442
|
+
): string | undefined {
|
|
443
|
+
if (node === undefined || node === null || node.type !== 'Identifier') return undefined
|
|
444
|
+
return typeof node.name === 'string' ? node.name : undefined
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** Returns the literal text a node carries, through a single-quasi template literal. */
|
|
448
|
+
export function expressionToPolicyText(
|
|
449
|
+
node: PolicyExpression | null | undefined,
|
|
450
|
+
): string | undefined {
|
|
451
|
+
if (node === undefined || node === null) return undefined
|
|
452
|
+
if (node.type === 'Literal') return typeof node.value === 'string' ? node.value : undefined
|
|
453
|
+
if (
|
|
454
|
+
node.type !== 'TemplateLiteral' ||
|
|
455
|
+
node.quasis?.length !== 1 ||
|
|
456
|
+
node.expressions?.length !== 0
|
|
457
|
+
) {
|
|
458
|
+
return undefined
|
|
459
|
+
}
|
|
460
|
+
const value = node.quasis[0]?.value
|
|
461
|
+
if (typeof value !== 'object' || value === null) return undefined
|
|
462
|
+
const cooked: unknown = Object.getOwnPropertyDescriptor(value, 'cooked')?.value
|
|
463
|
+
const raw: unknown = Object.getOwnPropertyDescriptor(value, 'raw')?.value
|
|
464
|
+
return typeof cooked === 'string' ? cooked : typeof raw === 'string' ? raw : undefined
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** Returns the single body expression a node holds, excluding a statement list. */
|
|
468
|
+
export function expressionToPolicyBody(node: PolicyExpression): PolicyExpression | undefined {
|
|
469
|
+
const body = node.body
|
|
470
|
+
if (body === undefined) return undefined
|
|
471
|
+
return 'type' in body ? body : undefined
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** Returns the top-level statements a program holds. */
|
|
475
|
+
export function programToPolicyStatements(node: PolicyExpression): readonly PolicyExpression[] {
|
|
476
|
+
const body = node.body
|
|
477
|
+
if (body === undefined || 'type' in body) return []
|
|
478
|
+
return body
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** Returns the declaration a top-level statement holds, through either export form. */
|
|
482
|
+
export function statementToPolicyDeclaration(node: PolicyExpression): PolicyExpression | undefined {
|
|
483
|
+
if (node.type === 'ExportNamedDeclaration' || node.type === 'ExportDefaultDeclaration') {
|
|
484
|
+
return node.declaration
|
|
485
|
+
}
|
|
486
|
+
return node
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/** Reports whether a statement sits at module scope, through either export form. */
|
|
490
|
+
export function isPolicyTop(node: PolicyExpression): boolean {
|
|
491
|
+
const parent = node.parent
|
|
492
|
+
if (parent === undefined || parent === null) return false
|
|
493
|
+
if (parent.type === 'Program') return true
|
|
494
|
+
return (
|
|
495
|
+
(parent.type === 'ExportNamedDeclaration' || parent.type === 'ExportDefaultDeclaration') &&
|
|
496
|
+
parent.parent?.type === 'Program'
|
|
497
|
+
)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** Reports whether a policy expression is runtime function syntax. */
|
|
76
501
|
export function isPolicyFunction(node: PolicyExpression): boolean {
|
|
77
502
|
return (
|
|
78
503
|
node.type === 'FunctionDeclaration' ||
|
|
@@ -81,12 +506,17 @@ export function isPolicyFunction(node: PolicyExpression): boolean {
|
|
|
81
506
|
)
|
|
82
507
|
}
|
|
83
508
|
|
|
84
|
-
/**
|
|
509
|
+
/** Reports whether a node declares a module function, including a signature without a body. */
|
|
510
|
+
export function isPolicyDeclaredFunction(node: PolicyExpression): boolean {
|
|
511
|
+
return node.type === 'FunctionDeclaration' || node.type === 'TSDeclareFunction'
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/** Reports whether a policy function is anonymous. */
|
|
85
515
|
export function isPolicyAnonymous(node: PolicyExpression): boolean {
|
|
86
516
|
return node.type === 'ArrowFunctionExpression' || node.id === null
|
|
87
517
|
}
|
|
88
518
|
|
|
89
|
-
/**
|
|
519
|
+
/** Returns the outermost parenthesized expression holding a policy function. */
|
|
90
520
|
export function functionToPolicyPosition(node: PolicyExpression): PolicyExpression {
|
|
91
521
|
let position = node
|
|
92
522
|
while (position.parent?.type === 'ParenthesizedExpression') {
|
|
@@ -95,7 +525,7 @@ export function functionToPolicyPosition(node: PolicyExpression): PolicyExpressi
|
|
|
95
525
|
return position
|
|
96
526
|
}
|
|
97
527
|
|
|
98
|
-
/**
|
|
528
|
+
/** Reports whether a policy function is an anonymous callback passed directly as an argument. */
|
|
99
529
|
export function isPolicyCallback(node: PolicyExpression): boolean {
|
|
100
530
|
if (!isPolicyAnonymous(node)) return false
|
|
101
531
|
const position = functionToPolicyPosition(node)
|
|
@@ -106,7 +536,7 @@ export function isPolicyCallback(node: PolicyExpression): boolean {
|
|
|
106
536
|
)
|
|
107
537
|
}
|
|
108
538
|
|
|
109
|
-
/**
|
|
539
|
+
/** Reports whether a policy function is an anonymous function returned directly as a result. */
|
|
110
540
|
export function isPolicyResult(node: PolicyExpression): boolean {
|
|
111
541
|
if (!isPolicyAnonymous(node)) return false
|
|
112
542
|
const position = functionToPolicyPosition(node)
|
|
@@ -117,7 +547,7 @@ export function isPolicyResult(node: PolicyExpression): boolean {
|
|
|
117
547
|
)
|
|
118
548
|
}
|
|
119
549
|
|
|
120
|
-
/**
|
|
550
|
+
/** Reports whether an Oxlint function expression represents method syntax. */
|
|
121
551
|
export function isPolicyMethod(node: PolicyExpression): boolean {
|
|
122
552
|
const parent = node.parent
|
|
123
553
|
return (
|
|
@@ -129,7 +559,10 @@ export function isPolicyMethod(node: PolicyExpression): boolean {
|
|
|
129
559
|
)
|
|
130
560
|
}
|
|
131
561
|
|
|
132
|
-
/**
|
|
562
|
+
/**
|
|
563
|
+
* Reports whether a policy function sits inside another function before any class-expression
|
|
564
|
+
* boundary.
|
|
565
|
+
*/
|
|
133
566
|
export function hasPolicyFunctionAncestor(node: PolicyExpression): boolean {
|
|
134
567
|
let parent = node.parent
|
|
135
568
|
let method = false
|
|
@@ -145,15 +578,16 @@ export function hasPolicyFunctionAncestor(node: PolicyExpression): boolean {
|
|
|
145
578
|
return method
|
|
146
579
|
}
|
|
147
580
|
|
|
148
|
-
/**
|
|
581
|
+
/** Reports whether an arrow is the policy plugin's sanctioned visitor-table delegation. */
|
|
149
582
|
export function isPolicyVisitor(node: PolicyExpression): boolean {
|
|
583
|
+
const body = expressionToPolicyBody(node)
|
|
150
584
|
if (
|
|
151
585
|
node.type !== 'ArrowFunctionExpression' ||
|
|
152
586
|
node.expression !== true ||
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
typeof
|
|
156
|
-
!
|
|
587
|
+
body?.type !== 'CallExpression' ||
|
|
588
|
+
body.callee?.type !== 'Identifier' ||
|
|
589
|
+
typeof body.callee.name !== 'string' ||
|
|
590
|
+
!body.callee.name.startsWith('report')
|
|
157
591
|
) {
|
|
158
592
|
return false
|
|
159
593
|
}
|
|
@@ -180,7 +614,237 @@ export function isPolicyVisitor(node: PolicyExpression): boolean {
|
|
|
180
614
|
)
|
|
181
615
|
}
|
|
182
616
|
|
|
183
|
-
/**
|
|
617
|
+
/**
|
|
618
|
+
* Returns the module-scope statement that owns a policy function, or `undefined` when none does.
|
|
619
|
+
*
|
|
620
|
+
* A class declaration or class expression on the way up ends the search, because the placement law
|
|
621
|
+
* reads module regions rather than class members.
|
|
622
|
+
*/
|
|
623
|
+
export function functionToPolicyRegion(node: PolicyExpression): PolicyExpression | undefined {
|
|
624
|
+
let current = node
|
|
625
|
+
let parent = current.parent
|
|
626
|
+
while (parent !== undefined && parent !== null) {
|
|
627
|
+
if (parent.type === 'ClassDeclaration' || parent.type === 'ClassExpression') return undefined
|
|
628
|
+
if (parent.type === 'Program') return statementToPolicyDeclaration(current)
|
|
629
|
+
current = parent
|
|
630
|
+
parent = parent.parent
|
|
631
|
+
}
|
|
632
|
+
return undefined
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/** Lists every module function a top-level statement declares, paired with its declared name. */
|
|
636
|
+
export function statementToPolicyBindings(node: PolicyExpression): readonly PolicyBinding[] {
|
|
637
|
+
if (isPolicyDeclaredFunction(node)) {
|
|
638
|
+
return [{ node, name: identifierToPolicyName(node.id) }]
|
|
639
|
+
}
|
|
640
|
+
if (node.type !== 'VariableDeclaration') return []
|
|
641
|
+
const bindings: PolicyBinding[] = []
|
|
642
|
+
for (const declarator of node.declarations ?? []) {
|
|
643
|
+
const init = declarator.init
|
|
644
|
+
if (init === undefined || init === null || !isPolicyFunction(init)) continue
|
|
645
|
+
const name = identifierToPolicyName(declarator.id)
|
|
646
|
+
if (name === undefined) continue
|
|
647
|
+
bindings.push({ node: declarator, name })
|
|
648
|
+
}
|
|
649
|
+
return bindings
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/** Reports whether a call trims a whole payload before splitting it on a line feed. */
|
|
653
|
+
export function isPolicySplit(node: PolicyExpression): boolean {
|
|
654
|
+
if (node.type !== 'CallExpression' || node.arguments?.length !== 1) return false
|
|
655
|
+
const split = node.callee
|
|
656
|
+
if (split?.type !== 'MemberExpression' || split.computed === true) return false
|
|
657
|
+
if (identifierToPolicyName(split.property) !== 'split') return false
|
|
658
|
+
if (expressionToPolicyText(node.arguments[0]) !== '\n') return false
|
|
659
|
+
const trim = split.object
|
|
660
|
+
if (trim?.type !== 'CallExpression' || trim.arguments?.length !== 0) return false
|
|
661
|
+
const read = trim.callee
|
|
662
|
+
return (
|
|
663
|
+
read?.type === 'MemberExpression' &&
|
|
664
|
+
read.computed !== true &&
|
|
665
|
+
identifierToPolicyName(read.property) === 'trim'
|
|
666
|
+
)
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/** Reports whether an expression reads the host line ending from a binding named os. */
|
|
670
|
+
export function isPolicyTerminator(node: PolicyExpression): boolean {
|
|
671
|
+
return (
|
|
672
|
+
node.type === 'MemberExpression' &&
|
|
673
|
+
node.computed !== true &&
|
|
674
|
+
identifierToPolicyName(node.property) === 'EOL' &&
|
|
675
|
+
identifierToPolicyName(node.object) === 'os'
|
|
676
|
+
)
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/** Reports whether an import declaration takes the EOL member from the host module. */
|
|
680
|
+
export function importsPolicyTerminator(node: PolicyExpression): boolean {
|
|
681
|
+
const specifier = expressionToPolicyText(node.source)
|
|
682
|
+
if (specifier !== 'node:os' && specifier !== 'os') return false
|
|
683
|
+
return (node.specifiers ?? []).some(
|
|
684
|
+
(element) =>
|
|
685
|
+
element.type === 'ImportSpecifier' && identifierToPolicyName(element.imported) === 'EOL',
|
|
686
|
+
)
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Blanks every character of a matched region, holding its length and its line breaks.
|
|
691
|
+
*
|
|
692
|
+
* @param text - The matched region to blank.
|
|
693
|
+
* @returns The region with each character outside a line break replaced by a space.
|
|
694
|
+
*/
|
|
695
|
+
export function blankPolicyText(text: string): string {
|
|
696
|
+
return text.replace(/[^\n]/gu, ' ')
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Blanks the regions of a text whose content is code, an address, or a symbol rather than prose.
|
|
701
|
+
*
|
|
702
|
+
* @remarks
|
|
703
|
+
* A fenced block, an inline code span a line break runs through, a link tag, and a URL each carry
|
|
704
|
+
* tokens a reader is meant to copy rather than read, so a banned term inside one is not prose. Each
|
|
705
|
+
* region is blanked in place, so every offset the caller reports stays the offset in the original
|
|
706
|
+
* text.
|
|
707
|
+
*
|
|
708
|
+
* @param text - The prose to strip, with any continuation marker already removed.
|
|
709
|
+
* @returns The same text with every code, tag, and address region replaced by spaces.
|
|
710
|
+
*/
|
|
711
|
+
export function stripPolicyCode(text: string): string {
|
|
712
|
+
const fenced = text.replace(POLICY_FENCE_PATTERN, blankPolicyText)
|
|
713
|
+
const spanned = fenced.replace(POLICY_SPAN_PATTERN, blankPolicyText)
|
|
714
|
+
const tagged = spanned.replace(POLICY_TAG_PATTERN, blankPolicyText)
|
|
715
|
+
return tagged.replace(POLICY_URL_PATTERN, blankPolicyText)
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Reads every banned term a stripped text carries, in offset order.
|
|
720
|
+
*
|
|
721
|
+
* @param text - The prose to read, already stripped of its code regions.
|
|
722
|
+
* @returns One hit per match, each naming its row and the offset the match starts at.
|
|
723
|
+
*/
|
|
724
|
+
export function textToPolicyHits(text: string): readonly PolicyHit[] {
|
|
725
|
+
const hits: PolicyHit[] = []
|
|
726
|
+
for (const term of POLICY_BANNED_TERMS) {
|
|
727
|
+
for (const match of text.matchAll(term.pattern)) hits.push({ term, index: match.index })
|
|
728
|
+
}
|
|
729
|
+
return hits.sort((left, right) => left.index - right.index)
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Reads one doc block's description paragraph, which ends at its first block tag.
|
|
734
|
+
*
|
|
735
|
+
* @param comment - The doc block to read.
|
|
736
|
+
* @returns The description with continuation markers removed and whitespace collapsed.
|
|
737
|
+
*/
|
|
738
|
+
export function commentToPolicyParagraph(comment: PolicyComment): string {
|
|
739
|
+
const description: string[] = []
|
|
740
|
+
for (const line of comment.value.split(POLICY_BREAK_PATTERN)) {
|
|
741
|
+
const text = line.replace(POLICY_MARKER_PATTERN, '')
|
|
742
|
+
if (text.trimStart().startsWith('@')) break
|
|
743
|
+
description.push(text)
|
|
744
|
+
}
|
|
745
|
+
return description.join(' ').replace(/\s+/gu, ' ').trim()
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Reads the opening word of a description paragraph, punctuation removed.
|
|
750
|
+
*
|
|
751
|
+
* @param paragraph - The collapsed description paragraph.
|
|
752
|
+
* @returns The paragraph's first word reduced to its letters, empty where it has none.
|
|
753
|
+
*/
|
|
754
|
+
export function paragraphToPolicyOpener(paragraph: string): string {
|
|
755
|
+
const first = paragraph.match(/^\S+/u)?.[0] ?? ''
|
|
756
|
+
return first.replace(/[^A-Za-z]/gu, '')
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Reports whether an opening word reads as a third-person verb.
|
|
761
|
+
*
|
|
762
|
+
* @param word - The opening word to judge.
|
|
763
|
+
* @returns True if the word ends in `s` and names no registered non-verb; false otherwise.
|
|
764
|
+
*/
|
|
765
|
+
export function isPolicyVoiced(word: string): boolean {
|
|
766
|
+
return POLICY_VOICE_PATTERN.test(word) && !POLICY_VOICE_STOPWORDS.includes(word)
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Pairs every exported top-level statement with the doc block written directly above it.
|
|
771
|
+
*
|
|
772
|
+
* @remarks
|
|
773
|
+
* A statement takes the last comment that closes before it, and takes it only where that comment is
|
|
774
|
+
* a doc block and nothing but whitespace separates the two. A blank line between them is still
|
|
775
|
+
* whitespace, so the pairing survives one.
|
|
776
|
+
*
|
|
777
|
+
* @param node - The program node whose top-level statements are read.
|
|
778
|
+
* @param sourceCode - The source-text reader supplying the comments and the text between them.
|
|
779
|
+
* @returns One entry per documented export, each naming its block and its declared symbol.
|
|
780
|
+
*/
|
|
781
|
+
export function programToPolicyDocs(
|
|
782
|
+
node: PolicyExpression,
|
|
783
|
+
sourceCode: PolicySourceCode,
|
|
784
|
+
): readonly PolicyDoc[] {
|
|
785
|
+
const comments = sourceCode.getAllComments()
|
|
786
|
+
const docs: PolicyDoc[] = []
|
|
787
|
+
for (const statement of programToPolicyStatements(node)) {
|
|
788
|
+
if (!statement.type.startsWith('Export')) continue
|
|
789
|
+
const start = statement.range[0]
|
|
790
|
+
let previous: PolicyComment | undefined
|
|
791
|
+
for (const comment of comments) {
|
|
792
|
+
if (comment.range[1] <= start) previous = comment
|
|
793
|
+
}
|
|
794
|
+
if (previous === undefined || previous.type !== 'Block') continue
|
|
795
|
+
if (!previous.value.startsWith('*')) continue
|
|
796
|
+
if (sourceCode.text.slice(previous.range[1], start).trim() !== '') continue
|
|
797
|
+
const declaration = statement.declaration
|
|
798
|
+
docs.push({
|
|
799
|
+
comment: previous,
|
|
800
|
+
name:
|
|
801
|
+
identifierToPolicyName(declaration?.id) ??
|
|
802
|
+
identifierToPolicyName(declaration?.declarations?.[0]?.id),
|
|
803
|
+
})
|
|
804
|
+
}
|
|
805
|
+
return docs
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/** Reports a doc block whose first sentence is not a third-person summary of its own symbol. */
|
|
809
|
+
export function reportVoice(context: PolicyContext, doc: PolicyDoc): void {
|
|
810
|
+
const paragraph = commentToPolicyParagraph(doc.comment)
|
|
811
|
+
if (!isPolicyVoiced(paragraphToPolicyOpener(paragraph))) {
|
|
812
|
+
context.report({ node: doc.comment, messageId: 'voice' })
|
|
813
|
+
}
|
|
814
|
+
const name = doc.name
|
|
815
|
+
if (name === undefined) return
|
|
816
|
+
const sentence = paragraph.split(POLICY_SENTENCE_PATTERN)[0] ?? ''
|
|
817
|
+
const repeat = new RegExp(`(?<![\\w$])${name.replaceAll('$', '\\$')}(?![\\w$])`, 'u')
|
|
818
|
+
if (repeat.test(sentence)) {
|
|
819
|
+
context.report({ node: doc.comment, messageId: 'name', data: { name } })
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/** Reports every voice failure among the doc blocks one program's exports carry. */
|
|
824
|
+
export function reportDocs(context: PolicyContext, node: PolicyExpression): void {
|
|
825
|
+
for (const doc of programToPolicyDocs(node, context.sourceCode)) reportVoice(context, doc)
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/** Reports every banned term one comment's prose carries. */
|
|
829
|
+
export function reportTerm(context: PolicyContext, comment: PolicyComment): void {
|
|
830
|
+
const lines = comment.value
|
|
831
|
+
.split(POLICY_BREAK_PATTERN)
|
|
832
|
+
.map((line) => line.replace(POLICY_MARKER_PATTERN, ''))
|
|
833
|
+
for (const hit of textToPolicyHits(stripPolicyCode(lines.join('\n')))) {
|
|
834
|
+
context.report({
|
|
835
|
+
node: comment,
|
|
836
|
+
messageId: 'term',
|
|
837
|
+
data: { term: hit.term.term, replacement: hit.term.replacement },
|
|
838
|
+
})
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/** Reports every banned term the comments of one linted file carry. */
|
|
843
|
+
export function reportComments(context: PolicyContext): void {
|
|
844
|
+
for (const comment of context.sourceCode.getAllComments()) reportTerm(context, comment)
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/** Reports function syntax nested inside another function body. */
|
|
184
848
|
export function reportNested(context: PolicyContext, node: PolicyExpression): void {
|
|
185
849
|
if (
|
|
186
850
|
!hasPolicyFunctionAncestor(node) ||
|
|
@@ -194,7 +858,7 @@ export function reportNested(context: PolicyContext, node: PolicyExpression): vo
|
|
|
194
858
|
context.report({ node, messageId: 'nested' })
|
|
195
859
|
}
|
|
196
860
|
|
|
197
|
-
/**
|
|
861
|
+
/** Reports banned calls on the named Vitest and Jest framework objects. */
|
|
198
862
|
export function reportMocking(context: PolicyContext, node: PolicyExpression): void {
|
|
199
863
|
const callee = node.callee
|
|
200
864
|
if (
|
|
@@ -213,26 +877,9 @@ export function reportMocking(context: PolicyContext, node: PolicyExpression): v
|
|
|
213
877
|
}
|
|
214
878
|
|
|
215
879
|
const property = callee.property
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
member = property.value
|
|
220
|
-
} else if (
|
|
221
|
-
property.type === 'TemplateLiteral' &&
|
|
222
|
-
property.quasis?.length === 1 &&
|
|
223
|
-
property.expressions?.length === 0
|
|
224
|
-
) {
|
|
225
|
-
const quasi = property.quasis[0]
|
|
226
|
-
const value = quasi?.value
|
|
227
|
-
if (typeof value === 'object' && value !== null) {
|
|
228
|
-
const cooked: unknown = Object.getOwnPropertyDescriptor(value, 'cooked')?.value
|
|
229
|
-
const raw: unknown = Object.getOwnPropertyDescriptor(value, 'raw')?.value
|
|
230
|
-
member = typeof cooked === 'string' ? cooked : typeof raw === 'string' ? raw : undefined
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
} else if (property.type === 'Identifier' && typeof property.name === 'string') {
|
|
234
|
-
member = property.name
|
|
235
|
-
}
|
|
880
|
+
const member = callee.computed
|
|
881
|
+
? expressionToPolicyText(property)
|
|
882
|
+
: identifierToPolicyName(property)
|
|
236
883
|
|
|
237
884
|
switch (member) {
|
|
238
885
|
case 'mock':
|
|
@@ -255,7 +902,7 @@ export function reportMocking(context: PolicyContext, node: PolicyExpression): v
|
|
|
255
902
|
}
|
|
256
903
|
}
|
|
257
904
|
|
|
258
|
-
/**
|
|
905
|
+
/** Reports TypeScript privacy keywords on class members. */
|
|
259
906
|
export function reportPrivacy(context: PolicyContext, node: PolicyExpression): void {
|
|
260
907
|
if (node.accessibility === 'private' || node.accessibility === 'protected') {
|
|
261
908
|
context.report({
|
|
@@ -266,7 +913,145 @@ export function reportPrivacy(context: PolicyContext, node: PolicyExpression): v
|
|
|
266
913
|
}
|
|
267
914
|
}
|
|
268
915
|
|
|
269
|
-
/**
|
|
916
|
+
/** Reports a centralized declaration that carries no export. */
|
|
917
|
+
export function reportHidden(context: PolicyContext, node: PolicyExpression): void {
|
|
918
|
+
if (isPolicyAmbient(context.filename)) return
|
|
919
|
+
if (!CENTRAL_SOURCE_FILES.includes(pathToPolicyFile(context.filename))) return
|
|
920
|
+
if (node.parent?.type !== 'Program') return
|
|
921
|
+
context.report({ node, messageId: 'hidden' })
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/** Reports a type declaration outside types.ts. */
|
|
925
|
+
export function reportType(context: PolicyContext, node: PolicyExpression): void {
|
|
926
|
+
if (isPolicyAmbient(context.filename) || !isPolicyTop(node)) return
|
|
927
|
+
if (pathToPolicyFile(context.filename) === 'types.ts') return
|
|
928
|
+
context.report({ node, messageId: 'type' })
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/** Reports a class whose file neither names it nor collects errors. */
|
|
932
|
+
export function reportClass(context: PolicyContext, node: PolicyExpression): void {
|
|
933
|
+
if (isPolicyAmbient(context.filename) || !isPolicyTop(node)) return
|
|
934
|
+
const file = pathToPolicyFile(context.filename)
|
|
935
|
+
if (file === 'errors.ts') return
|
|
936
|
+
if (
|
|
937
|
+
POLICY_CLASS_PATTERN.test(file) &&
|
|
938
|
+
identifierToPolicyName(node.id) === fileToPolicyStem(file)
|
|
939
|
+
) {
|
|
940
|
+
return
|
|
941
|
+
}
|
|
942
|
+
context.report({ node, messageId: 'class' })
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/** Reports module data outside a data-kind file. */
|
|
946
|
+
export function reportData(context: PolicyContext, node: PolicyExpression): void {
|
|
947
|
+
if (isPolicyAmbient(context.filename) || !isPolicyTop(node)) return
|
|
948
|
+
const file = pathToPolicyFile(context.filename)
|
|
949
|
+
if (DATA_SOURCE_FILES.includes(file) || DATA_EXEMPT_FILES.includes(file)) return
|
|
950
|
+
for (const declarator of node.declarations ?? []) {
|
|
951
|
+
const init = declarator.init
|
|
952
|
+
if (init !== undefined && init !== null && isPolicyFunction(init)) continue
|
|
953
|
+
context.report({ node: declarator, messageId: 'data' })
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/** Reports module function syntax outside a function-kind file. */
|
|
958
|
+
export function reportFunction(context: PolicyContext, node: PolicyExpression): void {
|
|
959
|
+
if (isPolicyAmbient(context.filename)) return
|
|
960
|
+
if (FUNCTION_SOURCE_FILES.includes(pathToPolicyFile(context.filename))) return
|
|
961
|
+
if (isPolicyDomain(context.filename, context.cwd)) return
|
|
962
|
+
const region = functionToPolicyRegion(node)
|
|
963
|
+
if (region === undefined) return
|
|
964
|
+
if (isPolicyDeclaredFunction(region)) {
|
|
965
|
+
if (region === node) context.report({ node, messageId: 'function' })
|
|
966
|
+
return
|
|
967
|
+
}
|
|
968
|
+
if (region.type !== 'VariableDeclaration') return
|
|
969
|
+
if (isPolicyCallback(node) || isPolicyResult(node)) return
|
|
970
|
+
context.report({ node, messageId: 'function' })
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/** Reports a constants.ts declaration that is mutable, misnamed, or a bare collection. */
|
|
974
|
+
export function reportConstant(context: PolicyContext, node: PolicyExpression): void {
|
|
975
|
+
if (isPolicyAmbient(context.filename) || !isPolicyTop(node)) return
|
|
976
|
+
if (pathToPolicyFile(context.filename) !== 'constants.ts') return
|
|
977
|
+
if (node.kind !== 'const') context.report({ node, messageId: 'mutable' })
|
|
978
|
+
for (const declarator of node.declarations ?? []) {
|
|
979
|
+
const name = identifierToPolicyName(declarator.id)
|
|
980
|
+
if (name === undefined || !POLICY_CONSTANT_PATTERN.test(name)) {
|
|
981
|
+
context.report({ node: declarator, messageId: 'naming' })
|
|
982
|
+
}
|
|
983
|
+
const init = declarator.init
|
|
984
|
+
if (init?.type === 'ArrayExpression' || init?.type === 'ObjectExpression') {
|
|
985
|
+
context.report({ node: declarator, messageId: 'collection' })
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/** Reports a parsers.ts function whose name lacks the parse prefix. */
|
|
991
|
+
export function reportParser(context: PolicyContext, node: PolicyExpression): void {
|
|
992
|
+
if (isPolicyAmbient(context.filename) || !isPolicyTop(node)) return
|
|
993
|
+
if (pathToPolicyFile(context.filename) !== 'parsers.ts') return
|
|
994
|
+
for (const binding of statementToPolicyBindings(node)) {
|
|
995
|
+
if (binding.name === undefined || !binding.name.startsWith('parse')) {
|
|
996
|
+
context.report({ node: binding.node, messageId: 'parser' })
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/** Reports a factories.ts function whose name lacks the create prefix. */
|
|
1002
|
+
export function reportFactory(context: PolicyContext, node: PolicyExpression): void {
|
|
1003
|
+
if (isPolicyAmbient(context.filename) || !isPolicyTop(node)) return
|
|
1004
|
+
if (pathToPolicyFile(context.filename) !== 'factories.ts') return
|
|
1005
|
+
for (const binding of statementToPolicyBindings(node)) {
|
|
1006
|
+
if (binding.name === undefined || !binding.name.startsWith('create')) {
|
|
1007
|
+
context.report({ node: binding.node, messageId: 'factory' })
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/** Reports a registered function domain taken as a file, or a malformed module inside one. */
|
|
1013
|
+
export function reportDomain(context: PolicyContext, node: PolicyExpression): void {
|
|
1014
|
+
if (isPolicyAmbient(context.filename)) return
|
|
1015
|
+
const file = pathToPolicyFile(context.filename)
|
|
1016
|
+
if (FUNCTION_DOMAIN_NAMES.includes(fileToPolicyStem(file))) {
|
|
1017
|
+
context.report({ node, messageId: 'file' })
|
|
1018
|
+
}
|
|
1019
|
+
if (!isPolicyDomain(context.filename, context.cwd)) return
|
|
1020
|
+
const expected = fileToPolicyStem(file)
|
|
1021
|
+
let implementations = 0
|
|
1022
|
+
let malformed = 0
|
|
1023
|
+
for (const statement of programToPolicyStatements(node)) {
|
|
1024
|
+
if (statement.type === 'ImportDeclaration') continue
|
|
1025
|
+
const declaration = statementToPolicyDeclaration(statement)
|
|
1026
|
+
if (declaration === undefined || !isPolicyDeclaredFunction(declaration)) {
|
|
1027
|
+
malformed += 1
|
|
1028
|
+
continue
|
|
1029
|
+
}
|
|
1030
|
+
if (declaration.type === 'FunctionDeclaration') implementations += 1
|
|
1031
|
+
if (
|
|
1032
|
+
statement.type !== 'ExportNamedDeclaration' ||
|
|
1033
|
+
identifierToPolicyName(declaration.id) !== expected
|
|
1034
|
+
) {
|
|
1035
|
+
malformed += 1
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
if (implementations !== 1 || malformed > 0) context.report({ node, messageId: 'module' })
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
/** Reports source that reads the host line ending or splits arrived text before trimming it. */
|
|
1042
|
+
export function reportEnding(context: PolicyContext, node: PolicyExpression): void {
|
|
1043
|
+
if (isPolicyAmbient(context.filename)) return
|
|
1044
|
+
if (isPolicySplit(node)) context.report({ node, messageId: 'split' })
|
|
1045
|
+
if (isPolicyTerminator(node)) context.report({ node, messageId: 'terminator' })
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/** Reports an import that takes the host line ending from the operating-system module. */
|
|
1049
|
+
export function reportEndingImport(context: PolicyContext, node: PolicyExpression): void {
|
|
1050
|
+
if (isPolicyAmbient(context.filename)) return
|
|
1051
|
+
if (importsPolicyTerminator(node)) context.report({ node, messageId: 'terminator' })
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
/** Bans function declarations and assignments inside another function body. */
|
|
270
1055
|
export const NESTED_RULE: PolicyRuleInterface = {
|
|
271
1056
|
meta: {
|
|
272
1057
|
type: 'problem',
|
|
@@ -287,7 +1072,7 @@ export const NESTED_RULE: PolicyRuleInterface = {
|
|
|
287
1072
|
},
|
|
288
1073
|
}
|
|
289
1074
|
|
|
290
|
-
/**
|
|
1075
|
+
/** Bans framework mocking, spying, fake clocks, and global or environment stubs. */
|
|
291
1076
|
export const MOCKING_RULE: PolicyRuleInterface = {
|
|
292
1077
|
meta: {
|
|
293
1078
|
type: 'problem',
|
|
@@ -310,7 +1095,7 @@ export const MOCKING_RULE: PolicyRuleInterface = {
|
|
|
310
1095
|
},
|
|
311
1096
|
}
|
|
312
1097
|
|
|
313
|
-
/**
|
|
1098
|
+
/** Bans compile-time-only TypeScript privacy keywords on class members. */
|
|
314
1099
|
export const PRIVACY_RULE: PolicyRuleInterface = {
|
|
315
1100
|
meta: {
|
|
316
1101
|
type: 'problem',
|
|
@@ -334,12 +1119,276 @@ export const PRIVACY_RULE: PolicyRuleInterface = {
|
|
|
334
1119
|
},
|
|
335
1120
|
}
|
|
336
1121
|
|
|
337
|
-
/**
|
|
1122
|
+
/** Bans a centralized declaration that no export reaches. */
|
|
1123
|
+
export const HIDDEN_RULE: PolicyRuleInterface = {
|
|
1124
|
+
meta: {
|
|
1125
|
+
type: 'problem',
|
|
1126
|
+
docs: {
|
|
1127
|
+
description: 'Disallow an unexported declaration in a centralized module.',
|
|
1128
|
+
},
|
|
1129
|
+
messages: {
|
|
1130
|
+
hidden:
|
|
1131
|
+
'Export this declaration or fold it into its caller; a centralized module hides nothing.',
|
|
1132
|
+
},
|
|
1133
|
+
},
|
|
1134
|
+
create(context) {
|
|
1135
|
+
return {
|
|
1136
|
+
ClassDeclaration: (node) => reportHidden(context, node),
|
|
1137
|
+
FunctionDeclaration: (node) => reportHidden(context, node),
|
|
1138
|
+
TSDeclareFunction: (node) => reportHidden(context, node),
|
|
1139
|
+
TSEnumDeclaration: (node) => reportHidden(context, node),
|
|
1140
|
+
TSInterfaceDeclaration: (node) => reportHidden(context, node),
|
|
1141
|
+
TSModuleDeclaration: (node) => reportHidden(context, node),
|
|
1142
|
+
TSTypeAliasDeclaration: (node) => reportHidden(context, node),
|
|
1143
|
+
VariableDeclaration: (node) => reportHidden(context, node),
|
|
1144
|
+
}
|
|
1145
|
+
},
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
/** Bans a type declaration outside its module's types.ts. */
|
|
1149
|
+
export const TYPE_RULE: PolicyRuleInterface = {
|
|
1150
|
+
meta: {
|
|
1151
|
+
type: 'problem',
|
|
1152
|
+
docs: {
|
|
1153
|
+
description: 'Disallow an interface, type alias, enum, or namespace outside types.ts.',
|
|
1154
|
+
},
|
|
1155
|
+
messages: {
|
|
1156
|
+
type: 'Move this declaration to the module types.ts file, which holds every reusable type.',
|
|
1157
|
+
},
|
|
1158
|
+
},
|
|
1159
|
+
create(context) {
|
|
1160
|
+
return {
|
|
1161
|
+
TSEnumDeclaration: (node) => reportType(context, node),
|
|
1162
|
+
TSInterfaceDeclaration: (node) => reportType(context, node),
|
|
1163
|
+
TSModuleDeclaration: (node) => reportType(context, node),
|
|
1164
|
+
TSTypeAliasDeclaration: (node) => reportType(context, node),
|
|
1165
|
+
}
|
|
1166
|
+
},
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
/** Bans a class outside the implementation file that names it. */
|
|
1170
|
+
export const CLASS_RULE: PolicyRuleInterface = {
|
|
1171
|
+
meta: {
|
|
1172
|
+
type: 'problem',
|
|
1173
|
+
docs: {
|
|
1174
|
+
description: 'Disallow a class outside errors.ts or the file named for it.',
|
|
1175
|
+
},
|
|
1176
|
+
messages: {
|
|
1177
|
+
class: 'Move this class to a PascalCase implementation file named for it, or to errors.ts.',
|
|
1178
|
+
},
|
|
1179
|
+
},
|
|
1180
|
+
create(context) {
|
|
1181
|
+
return {
|
|
1182
|
+
ClassDeclaration: (node) => reportClass(context, node),
|
|
1183
|
+
}
|
|
1184
|
+
},
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
/** Bans module data outside a data-kind file. */
|
|
1188
|
+
export const DATA_RULE: PolicyRuleInterface = {
|
|
1189
|
+
meta: {
|
|
1190
|
+
type: 'problem',
|
|
1191
|
+
docs: {
|
|
1192
|
+
description: 'Disallow a module-scope value declaration outside a data-kind file.',
|
|
1193
|
+
},
|
|
1194
|
+
messages: {
|
|
1195
|
+
data: 'Move this module data to constants.ts or another data-kind file.',
|
|
1196
|
+
},
|
|
1197
|
+
},
|
|
1198
|
+
create(context) {
|
|
1199
|
+
return {
|
|
1200
|
+
VariableDeclaration: (node) => reportData(context, node),
|
|
1201
|
+
}
|
|
1202
|
+
},
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
/** Bans module function syntax outside a function-kind file. */
|
|
1206
|
+
export const FUNCTION_RULE: PolicyRuleInterface = {
|
|
1207
|
+
meta: {
|
|
1208
|
+
type: 'problem',
|
|
1209
|
+
docs: {
|
|
1210
|
+
description:
|
|
1211
|
+
'Disallow module-scope function syntax outside a function-kind file or a registered function domain.',
|
|
1212
|
+
},
|
|
1213
|
+
messages: {
|
|
1214
|
+
function:
|
|
1215
|
+
'Move this function to a function-kind file or a registered function-domain module; only a directly passed callback and a directly returned function may sit in module data.',
|
|
1216
|
+
},
|
|
1217
|
+
},
|
|
1218
|
+
create(context) {
|
|
1219
|
+
return {
|
|
1220
|
+
ArrowFunctionExpression: (node) => reportFunction(context, node),
|
|
1221
|
+
FunctionDeclaration: (node) => reportFunction(context, node),
|
|
1222
|
+
FunctionExpression: (node) => reportFunction(context, node),
|
|
1223
|
+
TSDeclareFunction: (node) => reportFunction(context, node),
|
|
1224
|
+
}
|
|
1225
|
+
},
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
/** Bans a constants.ts declaration that is mutable, misnamed, or a bare collection. */
|
|
1229
|
+
export const CONSTANT_RULE: PolicyRuleInterface = {
|
|
1230
|
+
meta: {
|
|
1231
|
+
type: 'problem',
|
|
1232
|
+
docs: {
|
|
1233
|
+
description:
|
|
1234
|
+
'Disallow a non-const, non-UPPER_SNAKE_CASE, or bare-collection declaration in constants.ts.',
|
|
1235
|
+
},
|
|
1236
|
+
messages: {
|
|
1237
|
+
mutable: 'Declare every constants.ts binding with const.',
|
|
1238
|
+
naming: 'Name every constants.ts declaration in UPPER_SNAKE_CASE.',
|
|
1239
|
+
collection: 'Freeze this collection through a call; constants.ts holds no bare literal.',
|
|
1240
|
+
},
|
|
1241
|
+
},
|
|
1242
|
+
create(context) {
|
|
1243
|
+
return {
|
|
1244
|
+
VariableDeclaration: (node) => reportConstant(context, node),
|
|
1245
|
+
}
|
|
1246
|
+
},
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
/** Bans a parsers.ts function whose name lacks the parse prefix. */
|
|
1250
|
+
export const PARSER_RULE: PolicyRuleInterface = {
|
|
1251
|
+
meta: {
|
|
1252
|
+
type: 'problem',
|
|
1253
|
+
docs: {
|
|
1254
|
+
description: 'Disallow a parsers.ts function whose name does not start with parse.',
|
|
1255
|
+
},
|
|
1256
|
+
messages: {
|
|
1257
|
+
parser:
|
|
1258
|
+
'Name this parsers.ts function with the parse prefix, or move it to its own kind file.',
|
|
1259
|
+
},
|
|
1260
|
+
},
|
|
1261
|
+
create(context) {
|
|
1262
|
+
return {
|
|
1263
|
+
FunctionDeclaration: (node) => reportParser(context, node),
|
|
1264
|
+
TSDeclareFunction: (node) => reportParser(context, node),
|
|
1265
|
+
VariableDeclaration: (node) => reportParser(context, node),
|
|
1266
|
+
}
|
|
1267
|
+
},
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
/** Bans a factories.ts function whose name lacks the create prefix. */
|
|
1271
|
+
export const FACTORY_RULE: PolicyRuleInterface = {
|
|
1272
|
+
meta: {
|
|
1273
|
+
type: 'problem',
|
|
1274
|
+
docs: {
|
|
1275
|
+
description: 'Disallow a factories.ts function whose name does not start with create.',
|
|
1276
|
+
},
|
|
1277
|
+
messages: {
|
|
1278
|
+
factory:
|
|
1279
|
+
'Name this factories.ts function with the create prefix, or move it to its own kind file.',
|
|
1280
|
+
},
|
|
1281
|
+
},
|
|
1282
|
+
create(context) {
|
|
1283
|
+
return {
|
|
1284
|
+
FunctionDeclaration: (node) => reportFactory(context, node),
|
|
1285
|
+
TSDeclareFunction: (node) => reportFactory(context, node),
|
|
1286
|
+
VariableDeclaration: (node) => reportFactory(context, node),
|
|
1287
|
+
}
|
|
1288
|
+
},
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/** Bans a malformed module in a registered function domain, and a file named for one. */
|
|
1292
|
+
export const DOMAIN_RULE: PolicyRuleInterface = {
|
|
1293
|
+
meta: {
|
|
1294
|
+
type: 'problem',
|
|
1295
|
+
docs: {
|
|
1296
|
+
description:
|
|
1297
|
+
'Disallow a registered function domain taken as a source file, and a domain module that is not one exported function named for its file.',
|
|
1298
|
+
},
|
|
1299
|
+
messages: {
|
|
1300
|
+
file: 'Make this registered function domain a folder rather than a source file.',
|
|
1301
|
+
module:
|
|
1302
|
+
'Give this registered function module imports and one exported function named for its file.',
|
|
1303
|
+
},
|
|
1304
|
+
},
|
|
1305
|
+
create(context) {
|
|
1306
|
+
return {
|
|
1307
|
+
Program: (node) => reportDomain(context, node),
|
|
1308
|
+
}
|
|
1309
|
+
},
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
/** Bans host-specific line-ending handling. */
|
|
1313
|
+
export const ENDING_RULE: PolicyRuleInterface = {
|
|
1314
|
+
meta: {
|
|
1315
|
+
type: 'problem',
|
|
1316
|
+
docs: {
|
|
1317
|
+
description:
|
|
1318
|
+
'Disallow reading the host line ending and trimming an arrived payload before splitting it.',
|
|
1319
|
+
},
|
|
1320
|
+
messages: {
|
|
1321
|
+
split: 'Split arrived text on the line-ending pattern, then trim each line.',
|
|
1322
|
+
terminator: 'Emit a line feed rather than the host line ending.',
|
|
1323
|
+
},
|
|
1324
|
+
},
|
|
1325
|
+
create(context) {
|
|
1326
|
+
return {
|
|
1327
|
+
CallExpression: (node) => reportEnding(context, node),
|
|
1328
|
+
MemberExpression: (node) => reportEnding(context, node),
|
|
1329
|
+
ImportDeclaration: (node) => reportEndingImport(context, node),
|
|
1330
|
+
}
|
|
1331
|
+
},
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
/** Bans a doc block above an export whose first sentence is not a third-person summary. */
|
|
1335
|
+
export const VOICE_RULE: PolicyRuleInterface = {
|
|
1336
|
+
meta: {
|
|
1337
|
+
type: 'problem',
|
|
1338
|
+
docs: {
|
|
1339
|
+
description:
|
|
1340
|
+
'Disallow a description paragraph that opens on a word other than a third-person verb, and one that names the symbol it documents.',
|
|
1341
|
+
},
|
|
1342
|
+
messages: {
|
|
1343
|
+
voice:
|
|
1344
|
+
'Open this description with a third-person verb ending in s, such as Creates, Returns, or Checks whether.',
|
|
1345
|
+
name: 'State what the symbol does without naming {{name}} in the first sentence.',
|
|
1346
|
+
},
|
|
1347
|
+
},
|
|
1348
|
+
create(context) {
|
|
1349
|
+
return {
|
|
1350
|
+
Program: (node) => reportDocs(context, node),
|
|
1351
|
+
}
|
|
1352
|
+
},
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
/** Bans a comment carrying a term the substitution table bans unconditionally. */
|
|
1356
|
+
export const TERM_RULE: PolicyRuleInterface = {
|
|
1357
|
+
meta: {
|
|
1358
|
+
type: 'problem',
|
|
1359
|
+
docs: {
|
|
1360
|
+
description:
|
|
1361
|
+
'Disallow an unconditionally banned substitution-table term in comment prose, outside code spans, fenced blocks, link tags, and URLs.',
|
|
1362
|
+
},
|
|
1363
|
+
messages: {
|
|
1364
|
+
term: 'Replace {{term}} in this comment: {{replacement}}.',
|
|
1365
|
+
},
|
|
1366
|
+
},
|
|
1367
|
+
create(context) {
|
|
1368
|
+
return {
|
|
1369
|
+
Program: () => reportComments(context),
|
|
1370
|
+
}
|
|
1371
|
+
},
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
/** Declares the workspace Oxlint plugin. */
|
|
338
1375
|
export default {
|
|
339
1376
|
meta: { name: 'policy' },
|
|
340
1377
|
rules: {
|
|
341
1378
|
'no-mocking': MOCKING_RULE,
|
|
342
1379
|
'no-keyword-privacy': PRIVACY_RULE,
|
|
343
1380
|
'no-nested-functions': NESTED_RULE,
|
|
1381
|
+
'no-hidden-declaration': HIDDEN_RULE,
|
|
1382
|
+
'no-misplaced-type': TYPE_RULE,
|
|
1383
|
+
'no-misplaced-class': CLASS_RULE,
|
|
1384
|
+
'no-misplaced-data': DATA_RULE,
|
|
1385
|
+
'no-misplaced-function': FUNCTION_RULE,
|
|
1386
|
+
'no-malformed-constant': CONSTANT_RULE,
|
|
1387
|
+
'no-misnamed-parser': PARSER_RULE,
|
|
1388
|
+
'no-misnamed-factory': FACTORY_RULE,
|
|
1389
|
+
'no-malformed-domain': DOMAIN_RULE,
|
|
1390
|
+
'no-host-line-endings': ENDING_RULE,
|
|
1391
|
+
'no-malformed-summary': VOICE_RULE,
|
|
1392
|
+
'no-banned-term': TERM_RULE,
|
|
344
1393
|
},
|
|
345
1394
|
}
|