@sinemacula/coding-standards 1.20.2 → 1.22.0
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 +207 -26
- package/js/eslint/index.js +39 -8
- package/js/eslint/rules/boolean-method-name.js +24 -10
- package/js/eslint/rules/comment-classifier.js +5 -2
- package/js/eslint/rules/comment-line-wrap.js +69 -28
- package/package.json +14 -4
- package/swift/.swiftformat +48 -0
- package/swift/.swiftlint.yml +211 -0
- package/swift/README.md +35 -0
package/README.md
CHANGED
|
@@ -26,8 +26,13 @@ composer require --dev sinemacula/coding-standards
|
|
|
26
26
|
npm install --save-dev @sinemacula/coding-standards
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
The npm package ships only the static configs (`js/`, `markdown/`, `yaml/`, `shell/`, `security/`). The PHP
|
|
30
|
-
code lives in the Composer package.
|
|
29
|
+
The npm package ships only the static configs (`js/`, `markdown/`, `yaml/`, `shell/`, `security/`, `swift/`). The PHP
|
|
30
|
+
autoloaded code lives in the Composer package.
|
|
31
|
+
|
|
32
|
+
### Qlty (Swift-side: SwiftLint, SwiftFormat)
|
|
33
|
+
|
|
34
|
+
Swift consumers do not need the Composer or npm package. Qlty fetches the shared Swift configs from this repository as
|
|
35
|
+
a pinned source and installs the native tools on macOS.
|
|
31
36
|
|
|
32
37
|
## Usage
|
|
33
38
|
|
|
@@ -74,16 +79,19 @@ The `SineMacula` coding standard is auto-discovered via the `phpcodesniffer-stan
|
|
|
74
79
|
### PHPStan
|
|
75
80
|
|
|
76
81
|
The shared PHPStan configs are auto-included via the `extra.phpstan.includes` section in `composer.json`. Your project's
|
|
77
|
-
`phpstan.neon` only needs
|
|
82
|
+
`phpstan.neon` only needs its paths:
|
|
78
83
|
|
|
79
84
|
```neon
|
|
80
85
|
parameters:
|
|
81
|
-
level: 8
|
|
82
86
|
paths:
|
|
83
87
|
- src
|
|
84
88
|
- tests
|
|
85
89
|
```
|
|
86
90
|
|
|
91
|
+
Do not set `level`. Analysis runs through qlty, whose phpstan driver passes `--level=9` on the command line, and a
|
|
92
|
+
command-line level overrides the config file outright - so a level set here does nothing except mislead whoever reads
|
|
93
|
+
it next.
|
|
94
|
+
|
|
87
95
|
The base config enables PHPStan's checked-exception analysis: every exception a method can throw must appear in its
|
|
88
96
|
`@throws` tag, except a configured set of programming-error and infrastructure exceptions that stay unchecked - the
|
|
89
97
|
`LogicException`, `RuntimeException` and `Error` families among them (see `php/phpstan-base.neon` for the full list).
|
|
@@ -130,13 +138,17 @@ type-aware rules (the curated typescript-eslint set plus the type-driven custom
|
|
|
130
138
|
typescript-eslint tooling, and this package to your dev dependencies:
|
|
131
139
|
|
|
132
140
|
```bash
|
|
133
|
-
npm install --save-dev eslint typescript typescript-eslint eslint-plugin-jsdoc
|
|
141
|
+
npm install --save-dev eslint typescript typescript-eslint eslint-plugin-jsdoc yaml-eslint-parser \
|
|
142
|
+
@sinemacula/coding-standards
|
|
134
143
|
```
|
|
135
144
|
|
|
145
|
+
`yaml-eslint-parser` is imported by the base layer for the YAML comment-width block, so it has to resolve even in a
|
|
146
|
+
repository with no YAML worth linting; without it the flat config fails to load at all.
|
|
147
|
+
|
|
136
148
|
The package exposes three flat-config entry points:
|
|
137
149
|
|
|
138
150
|
- `@sinemacula/coding-standards/js/eslint` - the base layer of syntax-only custom rules; needs no `tsconfig`, so it
|
|
139
|
-
stays cheap and runs anywhere Biome runs.
|
|
151
|
+
stays cheap and runs anywhere Biome runs. Covers `.ts`/`.js` and, for the comment-width rule alone, `.yml`/`.yaml`.
|
|
140
152
|
- `@sinemacula/coding-standards/js/eslint/type-checked` - the opt-in type-aware layer. It includes the base layer and
|
|
141
153
|
adds the cross-file / type-driven rules, so it needs a consumer `tsconfig`; use it in place of the base layer where
|
|
142
154
|
one exists.
|
|
@@ -187,7 +199,10 @@ source-exported plugin definitions do not reliably propagate, so mirror it consu
|
|
|
187
199
|
|
|
188
200
|
```toml
|
|
189
201
|
[plugins.definitions.eslint]
|
|
190
|
-
package_filters = [
|
|
202
|
+
package_filters = [
|
|
203
|
+
"@sinemacula/coding-standards", "typescript-eslint", "@typescript-eslint", "eslint-plugin-jsdoc",
|
|
204
|
+
"yaml-eslint-parser",
|
|
205
|
+
]
|
|
191
206
|
```
|
|
192
207
|
|
|
193
208
|
Repositories enabling the Vue layer widen the same filter further, since its plugins have to resolve inside that sandbox
|
|
@@ -197,7 +212,7 @@ too:
|
|
|
197
212
|
[plugins.definitions.eslint]
|
|
198
213
|
package_filters = [
|
|
199
214
|
"@sinemacula/coding-standards", "typescript-eslint", "@typescript-eslint", "eslint-plugin-jsdoc",
|
|
200
|
-
"eslint-plugin-vue", "vue-eslint-parser", "eslint-plugin-check-file",
|
|
215
|
+
"yaml-eslint-parser", "eslint-plugin-vue", "vue-eslint-parser", "eslint-plugin-check-file",
|
|
201
216
|
]
|
|
202
217
|
```
|
|
203
218
|
|
|
@@ -258,6 +273,70 @@ repository = "https://github.com/sinemacula/coding-standards"
|
|
|
258
273
|
tag = "<version>"
|
|
259
274
|
```
|
|
260
275
|
|
|
276
|
+
### Swift (SwiftLint and SwiftFormat)
|
|
277
|
+
|
|
278
|
+
Swift repositories consume the shared policy through Qlty. Enable the default source for the tools, this repository
|
|
279
|
+
for the exported configs, and both native plugins:
|
|
280
|
+
|
|
281
|
+
```toml
|
|
282
|
+
config_version = "0"
|
|
283
|
+
|
|
284
|
+
# SwiftLint's own `excluded:` list only applies when it walks a directory. Qlty
|
|
285
|
+
# passes explicit file paths, so generated and third-party sources have to be
|
|
286
|
+
# excluded here or the shared policy is reported against machine-written code.
|
|
287
|
+
exclude_patterns = [
|
|
288
|
+
".build/**",
|
|
289
|
+
"build/**",
|
|
290
|
+
"DerivedData/**",
|
|
291
|
+
"Carthage/**",
|
|
292
|
+
"Pods/**",
|
|
293
|
+
"vendor/**",
|
|
294
|
+
"**/Generated/**",
|
|
295
|
+
]
|
|
296
|
+
|
|
297
|
+
test_patterns = [
|
|
298
|
+
"**/*Tests.swift",
|
|
299
|
+
"**/Tests/**",
|
|
300
|
+
]
|
|
301
|
+
|
|
302
|
+
[[source]]
|
|
303
|
+
name = "default"
|
|
304
|
+
default = true
|
|
305
|
+
|
|
306
|
+
[[source]]
|
|
307
|
+
name = "sinemacula"
|
|
308
|
+
repository = "https://github.com/sinemacula/coding-standards"
|
|
309
|
+
tag = "<version>"
|
|
310
|
+
|
|
311
|
+
[[plugin]]
|
|
312
|
+
name = "swiftlint"
|
|
313
|
+
|
|
314
|
+
[[plugin]]
|
|
315
|
+
name = "swiftformat"
|
|
316
|
+
mode = "comment"
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
This is the same configuration the package's own integration test runs, so what is documented here is what CI exercises.
|
|
320
|
+
|
|
321
|
+
Run formatting and linting locally:
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
qlty fmt --all
|
|
325
|
+
qlty check --all
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
SwiftLint and SwiftFormat are native macOS plugins. Qlty Cloud's Linux workers cannot execute them, so every Swift
|
|
329
|
+
consumer needs a macOS CI job that verifies formatting and runs `qlty check`. Qlty Cloud still provides its built-in
|
|
330
|
+
Swift maintainability, duplication, complexity, security, and coverage capabilities.
|
|
331
|
+
|
|
332
|
+
The shared policy targets Swift 6 and deliberately contains no application-specific include paths or architecture
|
|
333
|
+
rules. Xcode compiler checks such as strict concurrency, warnings-as-errors, platform availability, and test builds
|
|
334
|
+
remain the consuming project's responsibility.
|
|
335
|
+
|
|
336
|
+
SwiftLint has no configuration inheritance. A `.swiftlint.yml` committed to a consuming repository *replaces* the
|
|
337
|
+
shared policy rather than extending it, so adding one to relax a single rule silently discards the whole standard.
|
|
338
|
+
Raise a pull request here instead, or copy the shared file wholesale and edit the copy.
|
|
339
|
+
|
|
261
340
|
## What's Included
|
|
262
341
|
|
|
263
342
|
| Path | Tool | Description |
|
|
@@ -269,11 +348,14 @@ tag = "<version>"
|
|
|
269
348
|
| `js/biome.json` | Biome | JavaScript / TypeScript formatter + linter rules |
|
|
270
349
|
| `js/knip.json` | Knip | Unused-export detection rules |
|
|
271
350
|
| `js/eslint/` | ESLint | Structural, type-aware + Vue rules; runs with Biome |
|
|
351
|
+
| `js/eslint/` (YAML block) | ESLint | Comment width in `.yml` / `.yaml`; yamllint owns rest |
|
|
272
352
|
| `markdown/.markdownlint.json` | markdownlint | Markdown linting rules |
|
|
273
353
|
| `yaml/.yamllint.yaml` | yamllint | YAML linting rules |
|
|
274
354
|
| `shell/.shellcheckrc` | ShellCheck | Shell script linting rules |
|
|
275
355
|
| `security/.gitleaks.toml` | Gitleaks | Secret-detection ruleset |
|
|
276
356
|
| `editorconfig/.editorconfig-checker.json` | editorconfig-checker | Disables only the max-line-length check |
|
|
357
|
+
| `swift/.swiftlint.yml` | SwiftLint | Shared Swift 6 lint, safety, concurrency, and metrics |
|
|
358
|
+
| `swift/.swiftformat` | SwiftFormat | Shared deterministic Swift 6 formatting policy |
|
|
277
359
|
|
|
278
360
|
## Rules
|
|
279
361
|
|
|
@@ -298,7 +380,7 @@ native directive - `// phpcs:ignore <code>` for a sniff, `@phpstan-ignore <ident
|
|
|
298
380
|
| `SineMacula.Commenting.SingleLineMemberComment` | A property, constant or enum-case doc comment sits on one line. |
|
|
299
381
|
| `SineMacula.Exceptions.DisallowBaseException` | No throwing the base `\Exception`; throw a domain exception. |
|
|
300
382
|
| `SineMacula.Exceptions.RequireEmptyCatchComment` | An empty catch block must comment its intentional swallow. |
|
|
301
|
-
| `SineMacula.Functions.RequireSensitiveParameter` | Secret-named params need `#[\SensitiveParameter]
|
|
383
|
+
| `SineMacula.Functions.RequireSensitiveParameter` | Secret-named params need `#[\SensitiveParameter]`; object types exempt. |
|
|
302
384
|
| `SineMacula.Metrics.MaxMethodCount` | A class/interface/trait/enum may declare at most 20 methods (tests exempt). |
|
|
303
385
|
| `SineMacula.Metrics.MethodLength` | A method body may have at most 50 significant lines (tests exempt). |
|
|
304
386
|
| `SineMacula.Namespaces.RequireConcernsNamespace` | Traits must live under a `Concerns` namespace segment. |
|
|
@@ -313,15 +395,17 @@ native directive - `// phpcs:ignore <code>` for a sniff, `@phpstan-ignore <ident
|
|
|
313
395
|
|
|
314
396
|
### PHPStan rules
|
|
315
397
|
|
|
316
|
-
| Identifier
|
|
317
|
-
|
|
318
|
-
| `sineMacula.mutableStaticProperty`
|
|
319
|
-
| `sineMacula.readonlyClass`
|
|
398
|
+
| Identifier | Enforces |
|
|
399
|
+
|----------------------------------------|--------------------------------------------------------------------------------|
|
|
400
|
+
| `sineMacula.mutableStaticProperty` | Static properties written at runtime; `@managed-static` opts out. |
|
|
401
|
+
| `sineMacula.readonlyClass` | A final class with only readonly properties must be `readonly`. |
|
|
402
|
+
| `sineMacula.redundantStaticReference` | In a final class, `new static`, `static::` and `instanceof static` are `self`. |
|
|
403
|
+
| `sineMacula.redundantStaticReturnType` | In a final class, a `static` return type or `@return` must be `self`. |
|
|
320
404
|
|
|
321
405
|
### ESLint rules
|
|
322
406
|
|
|
323
407
|
All rules run in the base layer except `boolean-method-name`, which resolves return types and so requires the opt-in
|
|
324
|
-
type-checked layer.
|
|
408
|
+
type-checked layer. Every rule is scoped to `.ts`/`.js`; `comment-line-wrap` alone also runs over `.yml`/`.yaml`.
|
|
325
409
|
|
|
326
410
|
| Rule | Enforces |
|
|
327
411
|
|------------------------------------------------|-------------------------------------------------------------------------------------|
|
|
@@ -336,7 +420,7 @@ type-checked layer.
|
|
|
336
420
|
| `@sinemacula/align-doc-tags` | `@author` and `@copyright` values line up at a single column; autofixable. |
|
|
337
421
|
| `@sinemacula/single-line-property-doc` | A data member's documentation comment sits on one line; autofixable. |
|
|
338
422
|
| `@sinemacula/multiline-function-doc` | A method's documentation comment spans multiple lines; autofixable. |
|
|
339
|
-
| `@sinemacula/comment-line-wrap` | Standalone comment prose wrapped to 80 chars; premature wraps
|
|
423
|
+
| `@sinemacula/comment-line-wrap` | Standalone comment prose wrapped to 80 chars, YAML included; premature wraps too. |
|
|
340
424
|
|
|
341
425
|
`boolean-method-name` takes `additionalPrefixes`, `additionalPredicates` and `additionalCommandVerbs` (string arrays)
|
|
342
426
|
to widen the accepted vocabulary from a consumer config. `max-methods-per-class` takes `max`, `no-base-error` takes
|
|
@@ -348,26 +432,122 @@ signatures and class fields holding a function take several. A data comment is n
|
|
|
348
432
|
where present; a free function keeps the freedom of either shape.
|
|
349
433
|
|
|
350
434
|
`comment-line-wrap` takes `maxLength` (default 80) and is the syntax-only counterpart of the PHP
|
|
351
|
-
`SineMacula.Commenting.CommentLineLength` sniff. It fills standalone `//` runs and multi-line docblock prose
|
|
352
|
-
reporting an overflowing line and a prematurely wrapped line on their own footings and autofixing both.
|
|
353
|
-
headings, docblock tag lines, machine-parsed tool directives (`eslint`, `biome-ignore`, `@ts-`, `Stryker`,
|
|
354
|
-
`istanbul ignore`, `@vite-ignore` and the like), fenced code,
|
|
355
|
-
opens a multi-line bracketed type (an `array{...}` shape, a
|
|
356
|
-
separators, a line whose overflow is a single unbreakable token
|
|
357
|
-
and compact single-line docblocks are left untouched.
|
|
435
|
+
`SineMacula.Commenting.CommentLineLength` sniff. It fills standalone `//` and `#` runs and multi-line docblock prose
|
|
436
|
+
greedily, reporting an overflowing line and a prematurely wrapped line on their own footings and autofixing both.
|
|
437
|
+
Markdown headings, docblock tag lines, machine-parsed tool directives (`eslint`, `biome-ignore`, `@ts-`, `Stryker`,
|
|
438
|
+
`c8`/`v8`/`istanbul ignore`, `@vite-ignore`, `yamllint`, `yaml-language-server`, `renovate:` and the like), fenced code,
|
|
439
|
+
an indented code or command block, a doc-tag whose value opens a multi-line bracketed type (an `array{...}` shape, a
|
|
440
|
+
`<...>` generic or a `\Closure(...)` signature), tables, separators, a line whose overflow is a single unbreakable token
|
|
441
|
+
such as a long name or URL, trailing comments after code and compact single-line docblocks are left untouched. Each
|
|
442
|
+
comment token reclaims its own width from the line, so a `#` comment fills one column further than a `//` one.
|
|
443
|
+
|
|
444
|
+
This is the one rule the base layer also carries over `.yml` and `.yaml`, which nothing else in the standards bounds for
|
|
445
|
+
comment width: yamllint's `line-length` cannot tell a comment from a value, so it would fault `run:` commands and action
|
|
446
|
+
refs nobody can shorten, and it has no autofix. The YAML block registers `yaml-eslint-parser` for its `#` comments and
|
|
447
|
+
enables this rule alone - none of `eslint-plugin-yml`'s own rules are switched on, so YAML quoting, key order and
|
|
448
|
+
indentation stay yamllint's business. Only standalone comments are reached: a block scalar's body is content rather
|
|
449
|
+
than comment, so a shell comment inside a `run: |` step is never seen, and a comment trailing a value is not standalone.
|
|
358
450
|
|
|
359
451
|
The base layer also switches on a set of built-in rules: `@typescript-eslint/no-explicit-any`, `curly` (a brace on every
|
|
360
452
|
control statement, as PSR-12 already requires on the PHP side), `max-lines-per-function` (50 lines, test code exempt)
|
|
361
453
|
and `max-depth` (4), plus `eslint-plugin-jsdoc` rules that require a documentation comment on every declared function,
|
|
362
|
-
method, class, interface member and class field,
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
454
|
+
method, class, interface member and class field, require a description on every `@param` and `@returns`, and keep a
|
|
455
|
+
blank line above every documentation block, single-line blocks included. The type-checked layer adds
|
|
456
|
+
`@typescript-eslint/explicit-module-boundary-types` and `@typescript-eslint/only-throw-error`.
|
|
457
|
+
|
|
458
|
+
`jsdoc/no-types`, which forbids a type in `@param`/`@returns`, runs over `.ts`/`.tsx`/`.mts`/`.cts` alone, alongside
|
|
459
|
+
`@typescript-eslint/no-explicit-any`. A TypeScript signature already records the type, so the tag would only repeat it
|
|
460
|
+
and is free to drift; plain JavaScript has no signature to hold one, which makes the tag the only place a type is
|
|
461
|
+
written down, and clearing it there would delete the type rather than move it. The description rules are not scoped
|
|
462
|
+
that way: a tag says what a value means whether or not it also says what the value is, so `@param` and `@returns` need
|
|
463
|
+
a description in both languages.
|
|
464
|
+
|
|
465
|
+
### Swift policy
|
|
466
|
+
|
|
467
|
+
SwiftLint keeps its default rule set and adds curated opt-in rules with a strong correctness, concurrency, safety,
|
|
468
|
+
performance, or readability signal. The policy intentionally avoids analyzer-only rules, which need an Xcode compiler
|
|
469
|
+
log and cannot run through Qlty's normal lint driver.
|
|
470
|
+
|
|
471
|
+
Notable additions include checks for force-unwrapping, silently discarded throwing tasks, invalid concurrency
|
|
472
|
+
annotations, unsafe optional modelling, empty XCTest methods, unbalanced access control, inefficient collection
|
|
473
|
+
operations, oversized closures, and non-private SwiftUI state. A discarded throwing task is an error because it can
|
|
474
|
+
silently lose an operational failure; most style and maintainability findings retain warning severity.
|
|
475
|
+
|
|
476
|
+
The main review and hard ceilings are:
|
|
477
|
+
|
|
478
|
+
| Metric | Warning | Error |
|
|
479
|
+
|-----------------------|--------:|------:|
|
|
480
|
+
| Line length | 120 | 160 |
|
|
481
|
+
| File length | 500 | 800 |
|
|
482
|
+
| Type body length | 300 | 500 |
|
|
483
|
+
| Function body length | 50 | 80 |
|
|
484
|
+
| Closure body length | 50 | 80 |
|
|
485
|
+
| Cyclomatic complexity | 10 | 20 |
|
|
486
|
+
| Function parameters | 6 | 8 |
|
|
487
|
+
|
|
488
|
+
SwiftFormat owns whitespace, wrapping, imports, and other mechanically correctable layout. Its configuration matches
|
|
489
|
+
SwiftLint on 120-column wrapping, import ordering, and no trailing commas, and it is the only tool of the two that
|
|
490
|
+
enforces four-space indentation.
|
|
491
|
+
|
|
492
|
+
The policy also carries over the documentation opinions the PHP and TypeScript standards enforce:
|
|
493
|
+
|
|
494
|
+
- `file_header` requires a copyright header, as `RequireCopyrightTagSniff` does for PHP classes and `require-copyright`
|
|
495
|
+
does for TypeScript declarations. The pattern asserts only that the tag is present, so the holder, year and format
|
|
496
|
+
stay the consuming project's choice, and SwiftFormat's `--header ignore` leaves an existing header untouched.
|
|
497
|
+
- `missing_docs` requires documentation on `open` and `public` declarations, the Swift equivalent of the PHP docblock
|
|
498
|
+
sniffs and `jsdoc/require-jsdoc`. It is scoped to the public surface deliberately: demanding a comment on every
|
|
499
|
+
internal member would generate noise the other two standards do not.
|
|
500
|
+
- `line_length` holds comments to the limit rather than exempting them. SwiftFormat wraps `//` comments to 120 but
|
|
501
|
+
leaves `///` doc comments alone, so without this an over-long documentation line passes both tools - where PHP and
|
|
502
|
+
TypeScript both wrap comment prose. Rules that can alter ownership,
|
|
503
|
+
control flow, explicit `Sendable` conformance, or public API shape are disabled; those changes require human review.
|
|
504
|
+
|
|
505
|
+
### Methods that only throw
|
|
506
|
+
|
|
507
|
+
A method that exists solely to refuse - a `__serialize()` that throws so a value holding a secret cannot reach a queue
|
|
508
|
+
payload or a cache entry - returns nothing on any path, and `never` is how to say so:
|
|
509
|
+
|
|
510
|
+
```php
|
|
511
|
+
/**
|
|
512
|
+
* @throws \LogicException
|
|
513
|
+
*
|
|
514
|
+
* @return never
|
|
515
|
+
*/
|
|
516
|
+
public function __serialize(): never
|
|
517
|
+
{
|
|
518
|
+
throw new LogicException('A token must not be serialised.');
|
|
519
|
+
}
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
`never` is a subtype of every return type, so narrowing to it always satisfies an inherited signature, a magic method's
|
|
523
|
+
expected return included. The one place it does not fit is a method a subclass is meant to return from, because a child
|
|
524
|
+
cannot widen `never` back. Such a method keeps the type it declares and throws anyway, which needs no directive:
|
|
525
|
+
|
|
526
|
+
```php
|
|
527
|
+
/**
|
|
528
|
+
* @throws \LogicException
|
|
529
|
+
*
|
|
530
|
+
* @return array<int, string>
|
|
531
|
+
*/
|
|
532
|
+
public function build(): array
|
|
533
|
+
{
|
|
534
|
+
throw new LogicException('Not implemented.');
|
|
535
|
+
}
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
Whether a documented return is ever produced is a question of control flow, not of tokens, so no sniff here asks it -
|
|
539
|
+
`Squiz.Commenting.FunctionComment.InvalidNoReturn` decides by looking for a `return` token and so faults exactly the
|
|
540
|
+
guard above. PHPStan's `return.missing` answers it properly: it reports a method that can reach its end without
|
|
541
|
+
returning the type it documents, and stays quiet where every path throws.
|
|
542
|
+
|
|
543
|
+
The one thing still worth knowing is that spelling out the contained type of a documented traversable can drag in
|
|
544
|
+
`mixed`, which the mixed ban faults on its own footing and which has its own directive.
|
|
366
545
|
|
|
367
546
|
## Requirements
|
|
368
547
|
|
|
369
548
|
- PHP ^8.3 (Composer package)
|
|
370
549
|
- Node.js (npm package)
|
|
550
|
+
- macOS and Qlty CLI (SwiftLint and SwiftFormat policy)
|
|
371
551
|
|
|
372
552
|
## Testing
|
|
373
553
|
|
|
@@ -380,6 +560,7 @@ composer analyse # PHPStan static analysis
|
|
|
380
560
|
composer check # static analysis and lint via qlty
|
|
381
561
|
composer format # format via qlty
|
|
382
562
|
composer smells # duplication / complexity smells via qlty
|
|
563
|
+
bash scripts/test-swift-policy.sh # exported Swift policy integration test (macOS)
|
|
383
564
|
```
|
|
384
565
|
|
|
385
566
|
## Changelog
|
package/js/eslint/index.js
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
import jsdoc from 'eslint-plugin-jsdoc';
|
|
2
2
|
import tseslint from 'typescript-eslint';
|
|
3
|
+
import * as yamlParser from 'yaml-eslint-parser';
|
|
3
4
|
import plugin from './plugin.js';
|
|
4
5
|
|
|
5
6
|
const TS_FILES = ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts'];
|
|
6
7
|
const TS_AND_JS_FILES = [...TS_FILES, '**/*.js', '**/*.jsx', '**/*.mjs', '**/*.cjs'];
|
|
8
|
+
const YAML_FILES = ['**/*.yml', '**/*.yaml'];
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* Base flat config: the AST-only custom rules that need no type information.
|
|
10
12
|
*
|
|
11
13
|
* Requires no tsconfig, so it stays cheap. The typescript-eslint parser
|
|
12
14
|
* resolves TypeScript syntax. The interface, readonly-property and enum rules
|
|
13
|
-
* target TypeScript-only constructs
|
|
14
|
-
*
|
|
15
|
-
*
|
|
15
|
+
* target TypeScript-only constructs, as does jsdoc/no-types, which presumes a
|
|
16
|
+
* signature to hold the type it strips from the tag; no-mutable-static also
|
|
17
|
+
* applies to plain JavaScript (exported let/var, mutable static fields), so it
|
|
18
|
+
* runs across both. A final block carries the comment-wrap rule alone over
|
|
19
|
+
* YAML, which otherwise bounds nothing about a comment's width. The opt-in
|
|
20
|
+
* type-aware layer lives in ./type-checked.js.
|
|
16
21
|
*
|
|
17
22
|
* @author Ben Carey <bdmc@sinemacula.co.uk>
|
|
18
23
|
* @copyright 2026 Sine Macula Limited
|
|
@@ -23,6 +28,7 @@ export default [
|
|
|
23
28
|
plugins: {
|
|
24
29
|
'@sinemacula': plugin,
|
|
25
30
|
'@typescript-eslint': tseslint.plugin,
|
|
31
|
+
jsdoc,
|
|
26
32
|
},
|
|
27
33
|
languageOptions: {
|
|
28
34
|
parser: tseslint.parser,
|
|
@@ -33,6 +39,14 @@ export default [
|
|
|
33
39
|
'@sinemacula/valid-enum-member-name': 'error',
|
|
34
40
|
|
|
35
41
|
'@typescript-eslint/no-explicit-any': 'error',
|
|
42
|
+
|
|
43
|
+
// A TypeScript signature already records the type, so a tag that
|
|
44
|
+
// repeats it is a second copy free to drift from the first. Plain
|
|
45
|
+
// JavaScript has no signature to hold one, which is why this rule
|
|
46
|
+
// stops at TypeScript: there the tag is the only place a type is
|
|
47
|
+
// written down, and clearing it would delete the type rather than
|
|
48
|
+
// move it to where the reader already looks.
|
|
49
|
+
'jsdoc/no-types': 'error',
|
|
36
50
|
},
|
|
37
51
|
},
|
|
38
52
|
{
|
|
@@ -62,10 +76,11 @@ export default [
|
|
|
62
76
|
// property carries a documentation comment describing intent, so a
|
|
63
77
|
// reader meets each member's purpose before its type. Interface
|
|
64
78
|
// members and class fields are held to the same bar as methods.
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
79
|
+
// Every @param and @returns says what the value means, whether or
|
|
80
|
+
// not the tag also carries a type; the type itself is governed by
|
|
81
|
+
// no-types in the TypeScript block above, which is the only place a
|
|
82
|
+
// signature holds one. Each block stands off from the code above
|
|
83
|
+
// it, single-line blocks included.
|
|
69
84
|
'jsdoc/require-jsdoc': ['error', {
|
|
70
85
|
require: {
|
|
71
86
|
ClassDeclaration: true,
|
|
@@ -87,12 +102,28 @@ export default [
|
|
|
87
102
|
enableFixer: false,
|
|
88
103
|
}],
|
|
89
104
|
'jsdoc/require-description': 'error',
|
|
90
|
-
'jsdoc/no-types': 'error',
|
|
91
105
|
'jsdoc/require-param-description': 'error',
|
|
92
106
|
'jsdoc/require-returns-description': 'error',
|
|
93
107
|
'jsdoc/lines-before-block': ['error', { lines: 1, ignoreSingleLines: false }],
|
|
94
108
|
},
|
|
95
109
|
},
|
|
110
|
+
{
|
|
111
|
+
// YAML carries the comment-wrap rule and nothing else. The parser is
|
|
112
|
+
// registered for its `#` comments alone, so no eslint-plugin-yml rule
|
|
113
|
+
// is enabled: pulling that plugin's own set in would fault every
|
|
114
|
+
// consumer's YAML on quoting, key order and indentation at once, none
|
|
115
|
+
// of which this package governs. yamllint keeps the rest of YAML.
|
|
116
|
+
files: YAML_FILES,
|
|
117
|
+
plugins: {
|
|
118
|
+
'@sinemacula': plugin,
|
|
119
|
+
},
|
|
120
|
+
languageOptions: {
|
|
121
|
+
parser: yamlParser,
|
|
122
|
+
},
|
|
123
|
+
rules: {
|
|
124
|
+
'@sinemacula/comment-line-wrap': 'error',
|
|
125
|
+
},
|
|
126
|
+
},
|
|
96
127
|
{
|
|
97
128
|
files: ['**/*.{test,spec}.{ts,tsx,mts,cts,js,jsx,mjs,cjs}', '**/__tests__/**', '**/tests/**', '**/test-support/**'],
|
|
98
129
|
rules: {
|
|
@@ -140,22 +140,36 @@ function hasImperativeTag(sourceCode, docHost, nameNode) {
|
|
|
140
140
|
return false;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Whether the name is outside the rule's reach: a magic member, a name that
|
|
145
|
+
* already reads as a predicate, a command or event handler, a well-known type
|
|
146
|
+
* accessor, or a member carrying the @imperative opt-out tag.
|
|
147
|
+
*/
|
|
148
|
+
function isExempt(state, nameNode, name, docHost) {
|
|
149
|
+
if (name.startsWith('__') || TYPE_ACCESSOR_NAMES.has(name)) {
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (isPredicate(name, state.prefixes, state.predicates)) {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (isCommandVerb(name, state.commandVerbs) || isEventHandler(name)) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return hasImperativeTag(state.sourceCode, docHost, nameNode);
|
|
162
|
+
}
|
|
163
|
+
|
|
143
164
|
/**
|
|
144
165
|
* Report the name when it neither reads as a predicate nor is exempt and the
|
|
145
166
|
* resolved (awaited) return type is boolean. Type-predicate guards are
|
|
146
167
|
* predicates by structure and left alone.
|
|
147
168
|
*/
|
|
148
169
|
function inspect(state, nameNode, name, fnNode, docHost) {
|
|
149
|
-
const { checker, services, context
|
|
150
|
-
|
|
151
|
-
if (
|
|
152
|
-
name.startsWith('__')
|
|
153
|
-
|| isPredicate(name, state.prefixes, state.predicates)
|
|
154
|
-
|| isCommandVerb(name, state.commandVerbs)
|
|
155
|
-
|| isEventHandler(name)
|
|
156
|
-
|| TYPE_ACCESSOR_NAMES.has(name)
|
|
157
|
-
|| hasImperativeTag(sourceCode, docHost, nameNode)
|
|
158
|
-
) {
|
|
170
|
+
const { checker, services, context } = state;
|
|
171
|
+
|
|
172
|
+
if (isExempt(state, nameNode, name, docHost)) {
|
|
159
173
|
return;
|
|
160
174
|
}
|
|
161
175
|
|
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
* verbatim and acts as a paragraph boundary. Directives, docblock tags, fenced
|
|
6
6
|
* or indented code, tables and rule separators are left exactly as written, so
|
|
7
7
|
* a reflow never disturbs a construct whose position or spacing carries
|
|
8
|
-
* meaning.
|
|
8
|
+
* meaning. The directive set spans the tools that read instructions out of a
|
|
9
|
+
* comment in any of the governed languages, YAML's included: a schema
|
|
10
|
+
* association or a Renovate manager hint is machine-read, so wrapping it would
|
|
11
|
+
* silently sever it from the key it annotates.
|
|
9
12
|
*
|
|
10
13
|
* @author Ben Carey <bdmc@sinemacula.co.uk>
|
|
11
14
|
* @copyright 2026 Sine Macula Limited
|
|
@@ -18,7 +21,7 @@ const FENCE = /^(```|~~~)/;
|
|
|
18
21
|
const TAG = new RegExp(`^@[A-Za-z][A-Za-z0-9-]*(?=${WS}|$)`);
|
|
19
22
|
const LIST = new RegExp(`^${WS}*([-*+]|\\d+[.)])${WS}+`);
|
|
20
23
|
const HEADING = new RegExp(`^#{1,6}${WS}`);
|
|
21
|
-
const DIRECTIVE = /^(phpcs:|phpstan-ignore|@phpstan-|@psalm-|@phan-|eslint\b|globals?\b|exported\b|biome-ignore\b|@ts-|prettier-ignore|stylelint-|Stryker (?:disable|restore)\b|(?:c8|v8|istanbul) ignore\b|@vite-ignore\b|webpackChunkName\b|@preserve\b|@license\b|@codingStandards|@SuppressWarnings|NOSONAR|qlty-ignore)/;
|
|
24
|
+
const DIRECTIVE = /^(phpcs:|phpstan-ignore|@phpstan-|@psalm-|@phan-|eslint\b|globals?\b|exported\b|biome-ignore\b|@ts-|prettier-ignore|stylelint-|Stryker (?:disable|restore)\b|(?:c8|v8|istanbul) ignore\b|@vite-ignore\b|webpackChunkName\b|@preserve\b|@license\b|@codingStandards|@SuppressWarnings|NOSONAR|qlty-ignore|yamllint\b|yaml-language-server\b|renovate:)/;
|
|
22
25
|
const SEPARATOR = /^[=\-~*_#.+ ]{3,}$/;
|
|
23
26
|
const CODE = new RegExp(`=>|->|::|;${WS}*$|\\{${WS}*$|^\\}|^(?:if|elseif|for|foreach|while|switch|catch)${WS}*\\(|^[\\w$>[\\]'.-]+${WS}*=[^=>]|^\\$`);
|
|
24
27
|
const SPAN = /`[^`]*`|\{@[^}]*\}|\[[^\]]*\]\([^)]*\)/g;
|
|
@@ -6,6 +6,12 @@ const DEFAULT_MAX_LENGTH = 80;
|
|
|
6
6
|
/** A docblock interior line: its indent, the star, and the prose after it. */
|
|
7
7
|
const DOC_LINE = /^([ \t\n\r\f\v]*)\*( ?)(.*)$/;
|
|
8
8
|
|
|
9
|
+
/** The comment types a parser gives a line or block comment it recognises. */
|
|
10
|
+
const COMMENT_TYPES = ['Line', 'Block'];
|
|
11
|
+
|
|
12
|
+
/** The line-comment openers governed, matched longest first. */
|
|
13
|
+
const LINE_TOKENS = ['//', '#'];
|
|
14
|
+
|
|
9
15
|
/** Whether only whitespace precedes the comment on its own line. */
|
|
10
16
|
function isStandalone(comment, sourceCode) {
|
|
11
17
|
const line = sourceCode.lines[comment.loc.start.line - 1];
|
|
@@ -14,25 +20,44 @@ function isStandalone(comment, sourceCode) {
|
|
|
14
20
|
}
|
|
15
21
|
|
|
16
22
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
23
|
+
* The line-comment token a comment opens with, or null when it opens none.
|
|
24
|
+
*
|
|
25
|
+
* Read from the source text rather than inferred from the comment's type,
|
|
26
|
+
* because parsers label the same shape differently: a `//` comment arrives as
|
|
27
|
+
* `Line`, while yaml-eslint-parser tags a `#` comment `Block`. Requiring one of
|
|
28
|
+
* the two recognised types first leaves out a hashbang, which is neither and
|
|
29
|
+
* whose leading `#` must never be read as prose.
|
|
19
30
|
*/
|
|
20
|
-
function
|
|
31
|
+
function lineToken(comment, sourceCode) {
|
|
32
|
+
if (!COMMENT_TYPES.includes(comment.type)) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const opener = sourceCode.text.slice(comment.range[0], comment.range[0] + 2);
|
|
37
|
+
|
|
38
|
+
return LINE_TOKENS.find(token => opener.startsWith(token)) ?? null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Group standalone line comments into runs of adjacent lines sharing a token
|
|
43
|
+
* and an indent, so a wrapped paragraph is reflowed as one unit.
|
|
44
|
+
*/
|
|
45
|
+
function lineRuns(comments, sourceCode) {
|
|
21
46
|
const runs = [];
|
|
22
47
|
let current = null;
|
|
23
48
|
|
|
24
49
|
for (const comment of comments) {
|
|
25
|
-
|
|
50
|
+
const token = lineToken(comment, sourceCode);
|
|
51
|
+
|
|
52
|
+
if (token === null || !isStandalone(comment, sourceCode)) {
|
|
26
53
|
current = null;
|
|
27
54
|
continue;
|
|
28
55
|
}
|
|
29
56
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (current && comment.loc.start.line === last.loc.start.line + 1 && comment.loc.start.column === current[0].loc.start.column) {
|
|
33
|
-
current.push(comment);
|
|
57
|
+
if (current !== null && continues(current, comment, token)) {
|
|
58
|
+
current.comments.push(comment);
|
|
34
59
|
} else {
|
|
35
|
-
current = [comment];
|
|
60
|
+
current = { token, comments: [comment] };
|
|
36
61
|
runs.push(current);
|
|
37
62
|
}
|
|
38
63
|
}
|
|
@@ -40,22 +65,32 @@ function slashRuns(comments, sourceCode) {
|
|
|
40
65
|
return runs;
|
|
41
66
|
}
|
|
42
67
|
|
|
43
|
-
/**
|
|
44
|
-
function
|
|
68
|
+
/** Whether a comment extends the open run: same token, next line, same column. */
|
|
69
|
+
function continues(run, comment, token) {
|
|
70
|
+
const last = run.comments[run.comments.length - 1];
|
|
71
|
+
|
|
72
|
+
return run.token === token
|
|
73
|
+
&& comment.loc.start.line === last.loc.start.line + 1
|
|
74
|
+
&& comment.loc.start.column === run.comments[0].loc.start.column;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Strip the single optional space that follows the token from a value. */
|
|
78
|
+
function lineContent(value) {
|
|
45
79
|
return value.startsWith(' ') ? value.slice(1) : value;
|
|
46
80
|
}
|
|
47
81
|
|
|
48
|
-
/** Describe a
|
|
49
|
-
function
|
|
50
|
-
const
|
|
82
|
+
/** Describe a line-comment run: content, margin, report locations and rebuild. */
|
|
83
|
+
function lineDescriptor(run, sourceCode, eol) {
|
|
84
|
+
const { token, comments } = run;
|
|
85
|
+
const first = comments[0].loc.start;
|
|
51
86
|
const indent = sourceCode.lines[first.line - 1].slice(0, first.column);
|
|
52
87
|
|
|
53
88
|
return {
|
|
54
|
-
content:
|
|
55
|
-
marginWidth: indent.length +
|
|
56
|
-
locs:
|
|
57
|
-
range: [
|
|
58
|
-
rebuild: lines => lines.map((line, offset) => `${offset === 0 ? '' : indent}
|
|
89
|
+
content: comments.map(comment => lineContent(comment.value)),
|
|
90
|
+
marginWidth: indent.length + token.length + 1,
|
|
91
|
+
locs: comments.map(comment => comment.loc),
|
|
92
|
+
range: [comments[0].range[0], comments[comments.length - 1].range[1]],
|
|
93
|
+
rebuild: lines => lines.map((line, offset) => `${offset === 0 ? '' : indent}${token}${line === '' ? '' : ` ${line}`}`).join(eol),
|
|
59
94
|
};
|
|
60
95
|
}
|
|
61
96
|
|
|
@@ -135,13 +170,19 @@ function enforce(context, descriptor, maxLength) {
|
|
|
135
170
|
* The syntax-only counterpart of the PHP comment line length sniff. It fills
|
|
136
171
|
* each line greedily with as many whole words as fit and reports two faults on
|
|
137
172
|
* their own footings: a line that overflows the width, and a line that wraps
|
|
138
|
-
* earlier than it needs to. Standalone
|
|
139
|
-
* governed; tag lines, suppression directives,
|
|
140
|
-
* separators and trailing comments after code
|
|
141
|
-
*
|
|
142
|
-
* a compact single-line docblock, which the
|
|
143
|
-
* The fix reflows each faulted paragraph to
|
|
144
|
-
* idempotent.
|
|
173
|
+
* earlier than it needs to. Standalone runs of the line-comment tokens `//` and
|
|
174
|
+
* `#` and multi-line docblocks are governed; tag lines, suppression directives,
|
|
175
|
+
* fenced or indented code, tables, separators and trailing comments after code
|
|
176
|
+
* are left untouched, as is a line whose overflow is a single unbreakable token
|
|
177
|
+
* such as a long name or URL, and a compact single-line docblock, which the
|
|
178
|
+
* single-line property rule governs. The fix reflows each faulted paragraph to
|
|
179
|
+
* its greedy canonical form and is idempotent.
|
|
180
|
+
*
|
|
181
|
+
* The `#` token carries the rule into YAML through yaml-eslint-parser, where it
|
|
182
|
+
* reaches only standalone comments: block-scalar bodies are content rather than
|
|
183
|
+
* comments, so a shell comment inside a `run:` step is never seen, and a
|
|
184
|
+
* comment trailing a value is not standalone. Each token reclaims its own
|
|
185
|
+
* width, so a `#` comment fills one column further than a `//` one.
|
|
145
186
|
*
|
|
146
187
|
* @author Ben Carey <bdmc@sinemacula.co.uk>
|
|
147
188
|
* @copyright 2026 Sine Macula Limited
|
|
@@ -181,8 +222,8 @@ export default createRule({
|
|
|
181
222
|
Program() {
|
|
182
223
|
const comments = sourceCode.getAllComments();
|
|
183
224
|
|
|
184
|
-
for (const run of
|
|
185
|
-
enforce(context,
|
|
225
|
+
for (const run of lineRuns(comments, sourceCode)) {
|
|
226
|
+
enforce(context, lineDescriptor(run, sourceCode, eol), maxLength);
|
|
186
227
|
}
|
|
187
228
|
|
|
188
229
|
for (const comment of comments) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sinemacula/coding-standards",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.22.0",
|
|
4
4
|
"description": "Centralized coding standards, static analysis configurations, and code quality tooling for all Sine Macula repositories.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Ben Carey <bdmc@sinemacula.co.uk>",
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
"biome",
|
|
17
17
|
"knip",
|
|
18
18
|
"eslint",
|
|
19
|
+
"swift",
|
|
20
|
+
"swiftformat",
|
|
21
|
+
"swiftlint",
|
|
19
22
|
"typescript",
|
|
20
23
|
"coding-standards",
|
|
21
24
|
"linting",
|
|
@@ -28,9 +31,11 @@
|
|
|
28
31
|
"yaml/",
|
|
29
32
|
"shell/",
|
|
30
33
|
"security/",
|
|
34
|
+
"swift/",
|
|
31
35
|
"README.md",
|
|
32
36
|
"LICENSE",
|
|
33
37
|
"NOTICE",
|
|
38
|
+
"!js/eslint/__tests__",
|
|
34
39
|
"!js/eslint/rules/__tests__"
|
|
35
40
|
],
|
|
36
41
|
"exports": {
|
|
@@ -42,7 +47,7 @@
|
|
|
42
47
|
},
|
|
43
48
|
"scripts": {
|
|
44
49
|
"test:js": "vitest run",
|
|
45
|
-
"lint:js": "eslint
|
|
50
|
+
"lint:js": "eslint ."
|
|
46
51
|
},
|
|
47
52
|
"devDependencies": {
|
|
48
53
|
"@typescript-eslint/rule-tester": "^8.0.0",
|
|
@@ -54,7 +59,8 @@
|
|
|
54
59
|
"typescript": "^5.0.0",
|
|
55
60
|
"typescript-eslint": "^8.0.0",
|
|
56
61
|
"vitest": "^3.0.0",
|
|
57
|
-
"vue-eslint-parser": "^10.0.0"
|
|
62
|
+
"vue-eslint-parser": "^10.0.0",
|
|
63
|
+
"yaml-eslint-parser": "^2.1.0"
|
|
58
64
|
},
|
|
59
65
|
"peerDependencies": {
|
|
60
66
|
"eslint": ">=9",
|
|
@@ -63,7 +69,8 @@
|
|
|
63
69
|
"eslint-plugin-vue": ">=10",
|
|
64
70
|
"typescript": ">=4.8.4",
|
|
65
71
|
"typescript-eslint": "^8",
|
|
66
|
-
"vue-eslint-parser": ">=10"
|
|
72
|
+
"vue-eslint-parser": ">=10",
|
|
73
|
+
"yaml-eslint-parser": ">=2"
|
|
67
74
|
},
|
|
68
75
|
"peerDependenciesMeta": {
|
|
69
76
|
"eslint": {
|
|
@@ -86,6 +93,9 @@
|
|
|
86
93
|
},
|
|
87
94
|
"vue-eslint-parser": {
|
|
88
95
|
"optional": true
|
|
96
|
+
},
|
|
97
|
+
"yaml-eslint-parser": {
|
|
98
|
+
"optional": true
|
|
89
99
|
}
|
|
90
100
|
},
|
|
91
101
|
"publishConfig": {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Shared Swift 6 formatting policy for Sine Macula applications and packages.
|
|
2
|
+
|
|
3
|
+
--swiftversion 6.0
|
|
4
|
+
--indent 4
|
|
5
|
+
--linebreaks lf
|
|
6
|
+
--maxwidth 120
|
|
7
|
+
--trim-whitespace always
|
|
8
|
+
--xcode-indentation enabled
|
|
9
|
+
|
|
10
|
+
# Keep multiline declarations deterministic and easy to diff.
|
|
11
|
+
--wrap-arguments before-first
|
|
12
|
+
--wrap-parameters before-first
|
|
13
|
+
--wrap-collections before-first
|
|
14
|
+
--wrap-conditions before-first
|
|
15
|
+
--wrap-effects if-multiline
|
|
16
|
+
--wrap-return-type if-multiline
|
|
17
|
+
--closing-paren balanced
|
|
18
|
+
--call-site-paren balanced
|
|
19
|
+
|
|
20
|
+
# Match SwiftLint and avoid formatter/linter churn. SwiftLint's sorted_imports
|
|
21
|
+
# orders imports by module name alone, while SwiftFormat defaults to grouping by
|
|
22
|
+
# access level first (access-control,alpha). On a file using Swift 6 access-level
|
|
23
|
+
# imports that disagreement makes `qlty fmt` emit an order `qlty check` rejects,
|
|
24
|
+
# and re-running the formatter never settles it.
|
|
25
|
+
--import-grouping alpha
|
|
26
|
+
--trailing-commas never
|
|
27
|
+
--semicolons never
|
|
28
|
+
--self remove
|
|
29
|
+
--ifdef no-indent
|
|
30
|
+
|
|
31
|
+
# SwiftLint's file_header rule owns the copyright header. `ignore` leaves an
|
|
32
|
+
# existing header alone rather than rewriting every file to one template, so the
|
|
33
|
+
# holder and format stay the consuming project's choice.
|
|
34
|
+
--header ignore
|
|
35
|
+
|
|
36
|
+
# Do not rewrite public function signatures just because an implementation
|
|
37
|
+
# does not currently use an argument. Removing unused closure arguments is
|
|
38
|
+
# local and cannot alter a declared API.
|
|
39
|
+
--strip-unused-args closure-only
|
|
40
|
+
|
|
41
|
+
# Formatting must not change ownership, control flow, or API shape.
|
|
42
|
+
--anonymous-for-each ignore
|
|
43
|
+
--disable enumNamespaces,initCoderUnavailable,preferForLoop,redundantSendable,strongOutlets
|
|
44
|
+
|
|
45
|
+
# Mirror the exclusions in .swiftlint.yml. SwiftFormat rewrites files in place,
|
|
46
|
+
# and unlike SwiftLint's `excluded:` it honours these even when a path is passed
|
|
47
|
+
# explicitly - so generated and third-party code is protected under Qlty too.
|
|
48
|
+
--exclude .build,build,DerivedData,Carthage,Pods,vendor,**/Generated
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# Shared Swift 6 lint policy for Sine Macula applications and packages.
|
|
2
|
+
#
|
|
3
|
+
# Consumer-specific include paths do not belong here. Qlty determines the files
|
|
4
|
+
# passed to SwiftLint, while the exclusions below protect direct CLI and Xcode
|
|
5
|
+
# build-phase usage from generated and third-party code.
|
|
6
|
+
#
|
|
7
|
+
# Note that SwiftLint applies `excluded:` only when it walks a directory itself.
|
|
8
|
+
# Qlty passes explicit file paths, so a consumer must also exclude generated and
|
|
9
|
+
# third-party sources via `exclude_patterns` in its own .qlty/qlty.toml.
|
|
10
|
+
|
|
11
|
+
excluded:
|
|
12
|
+
- .build
|
|
13
|
+
- build
|
|
14
|
+
- DerivedData
|
|
15
|
+
- Carthage
|
|
16
|
+
- Pods
|
|
17
|
+
- vendor
|
|
18
|
+
- "**/Generated"
|
|
19
|
+
- "**/Generated/**"
|
|
20
|
+
|
|
21
|
+
reporter: xcode
|
|
22
|
+
|
|
23
|
+
# SwiftLint's default rules remain enabled. These opt-in rules add checks with a
|
|
24
|
+
# strong correctness, concurrency, safety, performance, or readability signal
|
|
25
|
+
# without requiring a particular application architecture.
|
|
26
|
+
opt_in_rules:
|
|
27
|
+
- accessibility_label_for_image
|
|
28
|
+
- accessibility_trait_for_button
|
|
29
|
+
- anonymous_argument_in_multiline_closure
|
|
30
|
+
- array_init
|
|
31
|
+
- closure_body_length
|
|
32
|
+
- closure_spacing
|
|
33
|
+
- collection_alignment
|
|
34
|
+
- contains_over_filter_count
|
|
35
|
+
- contains_over_filter_is_empty
|
|
36
|
+
- contains_over_first_not_nil
|
|
37
|
+
- contains_over_range_nil_comparison
|
|
38
|
+
- convenience_type
|
|
39
|
+
- direct_return
|
|
40
|
+
- discarded_notification_center_observer
|
|
41
|
+
- discouraged_assert
|
|
42
|
+
- discouraged_none_name
|
|
43
|
+
- discouraged_optional_boolean
|
|
44
|
+
- discouraged_optional_collection
|
|
45
|
+
- empty_collection_literal
|
|
46
|
+
- empty_count
|
|
47
|
+
- empty_string
|
|
48
|
+
- empty_xctest_method
|
|
49
|
+
- enum_case_associated_values_count
|
|
50
|
+
- expiring_todo
|
|
51
|
+
- explicit_init
|
|
52
|
+
- fallthrough
|
|
53
|
+
- fatal_error_message
|
|
54
|
+
- final_test_case
|
|
55
|
+
- first_where
|
|
56
|
+
- flatmap_over_map_reduce
|
|
57
|
+
- force_unwrapping
|
|
58
|
+
- function_default_parameter_at_end
|
|
59
|
+
- identical_operands
|
|
60
|
+
- implicitly_unwrapped_optional
|
|
61
|
+
- incompatible_concurrency_annotation
|
|
62
|
+
- joined_default_parameter
|
|
63
|
+
- last_where
|
|
64
|
+
- literal_expression_end_indentation
|
|
65
|
+
- local_doc_comment
|
|
66
|
+
- lower_acl_than_parent
|
|
67
|
+
- file_header
|
|
68
|
+
- missing_docs
|
|
69
|
+
- modifier_order
|
|
70
|
+
- no_empty_block
|
|
71
|
+
- non_overridable_class_declaration
|
|
72
|
+
- optional_enum_case_matching
|
|
73
|
+
- override_in_extension
|
|
74
|
+
- pattern_matching_keywords
|
|
75
|
+
- prefer_condition_list
|
|
76
|
+
- prefer_key_path
|
|
77
|
+
- prefer_self_in_static_references
|
|
78
|
+
- prefer_self_type_over_type_of_self
|
|
79
|
+
- prefer_zero_over_explicit_init
|
|
80
|
+
- private_subject
|
|
81
|
+
- private_swiftui_state
|
|
82
|
+
- reduce_into
|
|
83
|
+
- redundant_nil_coalescing
|
|
84
|
+
- redundant_type_annotation
|
|
85
|
+
- return_value_from_void_function
|
|
86
|
+
- shorthand_argument
|
|
87
|
+
- shorthand_optional_binding
|
|
88
|
+
- sorted_first_last
|
|
89
|
+
- sorted_imports
|
|
90
|
+
- static_operator
|
|
91
|
+
- strict_fileprivate
|
|
92
|
+
- superfluous_else
|
|
93
|
+
- test_case_accessibility
|
|
94
|
+
- toggle_bool
|
|
95
|
+
- unavailable_function
|
|
96
|
+
- unhandled_throwing_task
|
|
97
|
+
- unneeded_parentheses_in_closure_argument
|
|
98
|
+
- unowned_variable_capture
|
|
99
|
+
- untyped_error_in_catch
|
|
100
|
+
- unused_parameter
|
|
101
|
+
- variable_shadowing
|
|
102
|
+
- weak_delegate
|
|
103
|
+
- xct_specific_matcher
|
|
104
|
+
- yoda_condition
|
|
105
|
+
|
|
106
|
+
# Thresholds intentionally distinguish a review signal from a hard ceiling. Qlty
|
|
107
|
+
# preserves the warning/error severity in its findings.
|
|
108
|
+
closure_body_length:
|
|
109
|
+
warning: 50
|
|
110
|
+
error: 80
|
|
111
|
+
|
|
112
|
+
cyclomatic_complexity:
|
|
113
|
+
warning: 10
|
|
114
|
+
error: 20
|
|
115
|
+
ignores_case_statements: false
|
|
116
|
+
|
|
117
|
+
enum_case_associated_values_count:
|
|
118
|
+
warning: 5
|
|
119
|
+
error: 6
|
|
120
|
+
|
|
121
|
+
file_length:
|
|
122
|
+
warning: 500
|
|
123
|
+
error: 800
|
|
124
|
+
ignore_comment_only_lines: true
|
|
125
|
+
|
|
126
|
+
function_body_length:
|
|
127
|
+
warning: 50
|
|
128
|
+
error: 80
|
|
129
|
+
|
|
130
|
+
function_parameter_count:
|
|
131
|
+
warning: 6
|
|
132
|
+
error: 8
|
|
133
|
+
ignores_default_parameters: true
|
|
134
|
+
|
|
135
|
+
identifier_name:
|
|
136
|
+
min_length:
|
|
137
|
+
warning: 3
|
|
138
|
+
error: 2
|
|
139
|
+
max_length:
|
|
140
|
+
warning: 50
|
|
141
|
+
error: 60
|
|
142
|
+
excluded:
|
|
143
|
+
- id
|
|
144
|
+
- x
|
|
145
|
+
- y
|
|
146
|
+
|
|
147
|
+
line_length:
|
|
148
|
+
warning: 120
|
|
149
|
+
error: 160
|
|
150
|
+
ignores_urls: true
|
|
151
|
+
ignores_function_declarations: false
|
|
152
|
+
# SwiftFormat wraps `//` comments to this width but leaves `///` doc comments
|
|
153
|
+
# alone, so exempting comments here would let an over-long documentation line
|
|
154
|
+
# pass both tools. PHP and TypeScript hold comments to the limit too.
|
|
155
|
+
ignores_comments: false
|
|
156
|
+
ignores_interpolated_strings: true
|
|
157
|
+
ignores_multiline_strings: true
|
|
158
|
+
ignores_regex_literals: true
|
|
159
|
+
|
|
160
|
+
nesting:
|
|
161
|
+
type_level:
|
|
162
|
+
warning: 2
|
|
163
|
+
function_level:
|
|
164
|
+
warning: 3
|
|
165
|
+
check_nesting_in_closures_and_statements: true
|
|
166
|
+
always_allow_one_type_in_functions: false
|
|
167
|
+
ignore_typealiases_and_associatedtypes: true
|
|
168
|
+
ignore_coding_keys: true
|
|
169
|
+
|
|
170
|
+
type_body_length:
|
|
171
|
+
warning: 300
|
|
172
|
+
error: 500
|
|
173
|
+
excluded_types:
|
|
174
|
+
- extension
|
|
175
|
+
- protocol
|
|
176
|
+
|
|
177
|
+
type_name:
|
|
178
|
+
min_length:
|
|
179
|
+
warning: 3
|
|
180
|
+
error: 2
|
|
181
|
+
max_length:
|
|
182
|
+
warning: 50
|
|
183
|
+
error: 60
|
|
184
|
+
|
|
185
|
+
# Match SwiftFormat's explicit no-trailing-comma policy.
|
|
186
|
+
trailing_comma:
|
|
187
|
+
mandatory_comma: false
|
|
188
|
+
|
|
189
|
+
# The PHP standard requires an @copyright tag in every class docblock and the
|
|
190
|
+
# TypeScript standard requires @copyright and @author, so a Swift file carries a
|
|
191
|
+
# copyright header too. The pattern deliberately asserts only that the tag is
|
|
192
|
+
# present: the holder, year and format stay the consuming project's to choose,
|
|
193
|
+
# and SwiftFormat's `--header ignore` leaves an existing header untouched rather
|
|
194
|
+
# than rewriting it to a template.
|
|
195
|
+
file_header:
|
|
196
|
+
required_pattern: '(?:\/\/|\/\*)(?s:.)*[Cc]opyright(?s:.)*'
|
|
197
|
+
|
|
198
|
+
# PHP requires a docblock on every class and method, TypeScript requires a JSDoc
|
|
199
|
+
# block with a description. Swift's equivalent is held to the public surface:
|
|
200
|
+
# `open` and `public` declarations are the API a consumer reads, and demanding a
|
|
201
|
+
# comment on every internal member would be noise the other two standards do not
|
|
202
|
+
# generate either.
|
|
203
|
+
missing_docs:
|
|
204
|
+
warning: [open, public]
|
|
205
|
+
excludes_extensions: true
|
|
206
|
+
excludes_inherited_types: true
|
|
207
|
+
excludes_trivial_init: true
|
|
208
|
+
|
|
209
|
+
# A discarded throwing task can silently lose an operational failure.
|
|
210
|
+
unhandled_throwing_task:
|
|
211
|
+
severity: error
|
package/swift/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Swift standards
|
|
2
|
+
|
|
3
|
+
The shared Swift 6 policy consists of:
|
|
4
|
+
|
|
5
|
+
- `.swiftlint.yml` for correctness, concurrency, safety, performance, metrics, and style findings.
|
|
6
|
+
- `.swiftformat` for deterministic source formatting.
|
|
7
|
+
|
|
8
|
+
Both tools are provided by Qlty's default source. Consumers enable the `swiftlint` and `swiftformat` plugins and receive
|
|
9
|
+
these configurations from the Sine Macula source declared in `.qlty/qlty.toml`.
|
|
10
|
+
|
|
11
|
+
SwiftLint and SwiftFormat run through Qlty CLI only on macOS. Qlty Cloud still provides Swift maintainability analysis,
|
|
12
|
+
but its Linux workers cannot execute these two native plugins. A Swift repository therefore needs a macOS quality job
|
|
13
|
+
that runs both `qlty fmt --all` and `qlty check --all`.
|
|
14
|
+
|
|
15
|
+
The policy is not purely stock SwiftLint. Where the PHP and TypeScript standards in this repository express a house
|
|
16
|
+
opinion that SwiftLint can express too, it is carried over: a required copyright header (`file_header`), documentation
|
|
17
|
+
on the public surface (`missing_docs`), and comment prose held to the line limit. Opinions with no SwiftLint
|
|
18
|
+
equivalent - a maximum method count per type, protocol and boolean-method naming, and a required-readonly property
|
|
19
|
+
rule - are not enforced for Swift, and closing those would mean writing custom regex rules.
|
|
20
|
+
|
|
21
|
+
Application-specific paths, generated-source conventions, and architectural restrictions stay in the consuming
|
|
22
|
+
repository. The shared policy must remain usable by macOS apps, iOS apps, command-line tools, and Swift packages.
|
|
23
|
+
|
|
24
|
+
The standard deliberately avoids formatter rules that can change ownership, control flow, or an API declaration. A
|
|
25
|
+
formatter should make an equivalent program consistent; correctness and design changes belong in reviewed source
|
|
26
|
+
edits.
|
|
27
|
+
|
|
28
|
+
## Deliberately not included
|
|
29
|
+
|
|
30
|
+
- StringsLint is deferred because its current Qlty integration does not support modern `.xcstrings` String Catalogs.
|
|
31
|
+
Reconsider it for a consumer that deliberately uses legacy `.strings` or `.stringsdict` resources.
|
|
32
|
+
- Semgrep rules belong here only after an organization-wide Swift security or architecture rule is defined. Product-
|
|
33
|
+
specific boundaries should stay in the product repository.
|
|
34
|
+
- Compiler-enforced policy such as strict concurrency, warnings-as-errors, deployment targets, and platform
|
|
35
|
+
availability remains in each Xcode project or shared project template; Qlty is not a substitute for `xcodebuild`.
|