| author | |
| committer | |
| log | 31d0c8a4f47ead36c9a63ac4d3bf27b55c05095a |
| tree | c9367413d6eb2d0c9d6da5e143a5504292459b51 |
| parent | 6262e5f50081845bb247e5af2fc81b068c8ae893 |
| signature | Signed by PGP key B5690EEEBB952194 |
7 files changed, 194 insertions(+), 4 deletions(-)
apps/server/src/project/ProjectSetupScriptRunner.ts+2-2| ... | ... | @@ -144,11 +144,11 @@ function stripTerminalControl(text: string): string { |
| 144 | 144 | return ( |
| 145 | 145 | text |
| 146 | 146 | .replace( |
| 147 | // eslint-disable-next-line no-control-regex | |
| 147 | // eslint-disable-next-line no-control-regex -- ANSI escape sequences start with ESC. | |
| 148 | 148 | /\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[()][A-Za-z0-9]|\x1b[=>]/g, |
| 149 | 149 | "", |
| 150 | 150 | ) |
| 151 | // eslint-disable-next-line no-control-regex | |
| 151 | // eslint-disable-next-line no-control-regex -- removing control characters is the point. | |
| 152 | 152 | .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "") |
| 153 | 153 | ); |
| 154 | 154 | } |
apps/web/src/components/files/BrowserDocumentFrame.tsx+1-1| ... | ... | @@ -35,7 +35,7 @@ export function BrowserDocumentFrame(props: { |
| 35 | 35 | }) { |
| 36 | 36 | const className = "min-h-0 flex-1 border-0 bg-white"; |
| 37 | 37 | return props.pdf ? ( |
| 38 | // oxlint-disable-next-line react/iframe-missing-sandbox | |
| 38 | // oxlint-disable-next-line react/iframe-missing-sandbox -- the built-in PDF viewer needs an unsandboxed frame. | |
| 39 | 39 | <iframe |
| 40 | 40 | key={props.src} |
| 41 | 41 | src={`${props.src}${PDF_VIEWER_FRAGMENT}`} |
oxlint-plugin-t3code/index.ts+2| ... | ... | @@ -9,6 +9,7 @@ import noMobileUniwindThemeEscapeHatches from "./rules/no-mobile-uniwind-theme-e |
| 9 | 9 | import noNativeTitleTooltip from "./rules/no-native-title-tooltip.ts"; |
| 10 | 10 | import noTestInLoop from "./rules/no-test-in-loop.ts"; |
| 11 | 11 | import noUnscopedHas from "./rules/no-unscoped-has.ts"; |
| 12 | import requireSuppressionReason from "./rules/require-suppression-reason.ts"; | |
| 12 | 13 | |
| 13 | 14 | export default definePlugin({ |
| 14 | 15 | meta: { |
| ... | ... | @@ -24,5 +25,6 @@ export default definePlugin({ |
| 24 | 25 | "no-native-title-tooltip": noNativeTitleTooltip, |
| 25 | 26 | "no-test-in-loop": noTestInLoop, |
| 26 | 27 | "no-unscoped-has": noUnscopedHas, |
| 28 | "require-suppression-reason": requireSuppressionReason, | |
| 27 | 29 | }, |
| 28 | 30 | }); |
oxlint-plugin-t3code/rules/require-suppression-reason.test.ts created+117| ... | ... | @@ -0,0 +1,117 @@ |
| 1 | import { assert, describe } from "@effect/vitest"; | |
| 2 | ||
| 3 | import { createOxlintRuleHarness } from "../test/utils.ts"; | |
| 4 | ||
| 5 | const rule = createOxlintRuleHarness("t3code/require-suppression-reason", { | |
| 6 | filename: "fixture.tsx", | |
| 7 | }); | |
| 8 | ||
| 9 | describe("t3code/require-suppression-reason", () => { | |
| 10 | rule.valid( | |
| 11 | "allows a directive with a -- reason", | |
| 12 | ` | |
| 13 | // oxlint-disable-next-line no-control-regex -- ANSI sequences start with ESC. | |
| 14 | const ansi = /\\x1b\\[/; | |
| 15 | /* eslint-disable no-console -- this script reports to the terminal */ | |
| 16 | `, | |
| 17 | ); | |
| 18 | ||
| 19 | rule.valid( | |
| 20 | "allows a directive explained by the comment above", | |
| 21 | ` | |
| 22 | // Positions are stable for a given string. | |
| 23 | // oxlint-disable-next-line react/no-array-index-key | |
| 24 | const items = ["a"].map((item, index) => <li key={index}>{item}</li>); | |
| 25 | `, | |
| 26 | ); | |
| 27 | ||
| 28 | rule.valid( | |
| 29 | "allows stacked directives under one explanation", | |
| 30 | ` | |
| 31 | // The fixture is invalid on purpose. | |
| 32 | // @ts-expect-error | |
| 33 | // oxlint-disable-next-line no-unused-vars | |
| 34 | const value: number = "text"; | |
| 35 | `, | |
| 36 | ); | |
| 37 | ||
| 38 | rule.valid( | |
| 39 | "allows a TypeScript directive followed by its reason", | |
| 40 | ` | |
| 41 | // @ts-expect-error the fixture passes a string on purpose | |
| 42 | const count: number = "text"; | |
| 43 | `, | |
| 44 | ); | |
| 45 | ||
| 46 | rule.valid( | |
| 47 | "allows a directive in a JSX comment with a reason", | |
| 48 | `const el = <div>{/* oxlint-disable-next-line react/no-danger -- sanitized above */}</div>;`, | |
| 49 | ); | |
| 50 | ||
| 51 | rule.valid("ignores enable directives", `/* oxlint-enable no-console */`); | |
| 52 | ||
| 53 | rule.invalid( | |
| 54 | "reports a lint directive without a reason", | |
| 55 | ` | |
| 56 | // oxlint-disable-next-line no-control-regex | |
| 57 | const ansi = /\\x1b\\[/; | |
| 58 | `, | |
| 59 | (output) => { | |
| 60 | assert.match(output, /end the directive with `-- reason`/); | |
| 61 | }, | |
| 62 | ); | |
| 63 | ||
| 64 | rule.invalid( | |
| 65 | "reports a file-level directive without a reason", | |
| 66 | `/* oxlint-disable eslint/no-useless-escape */`, | |
| 67 | ); | |
| 68 | ||
| 69 | rule.invalid( | |
| 70 | "reports a TypeScript directive without a reason", | |
| 71 | ` | |
| 72 | // @ts-expect-error | |
| 73 | const count: number = "text"; | |
| 74 | `, | |
| 75 | (output) => { | |
| 76 | assert.match(output, /write the reason after the directive/); | |
| 77 | }, | |
| 78 | ); | |
| 79 | ||
| 80 | rule.invalid( | |
| 81 | "does not count a comment separated by a blank line", | |
| 82 | ` | |
| 83 | // Positions are stable for a given string. | |
| 84 | ||
| 85 | // oxlint-disable-next-line react/no-array-index-key | |
| 86 | const items = ["a"].map((item, index) => <li key={index}>{item}</li>); | |
| 87 | `, | |
| 88 | ); | |
| 89 | ||
| 90 | rule.valid( | |
| 91 | "allows an empty comment line between the reason and the directive", | |
| 92 | ` | |
| 93 | // Positions are stable for a given string. | |
| 94 | // | |
| 95 | // oxlint-disable-next-line react/no-array-index-key | |
| 96 | const items = ["a"].map((item, index) => <li key={index}>{item}</li>); | |
| 97 | `, | |
| 98 | ); | |
| 99 | ||
| 100 | rule.invalid( | |
| 101 | "does not count an empty comment above", | |
| 102 | ` | |
| 103 | // | |
| 104 | /* */ | |
| 105 | // oxlint-disable-next-line react/no-array-index-key | |
| 106 | const items = ["a"].map((item, index) => <li key={index}>{item}</li>); | |
| 107 | `, | |
| 108 | ); | |
| 109 | ||
| 110 | rule.invalid( | |
| 111 | "does not count a trailing comment on the code line above", | |
| 112 | ` | |
| 113 | const first = 1; // the first value | |
| 114 | const second = 2; // oxlint-disable-line no-unused-vars | |
| 115 | `, | |
| 116 | ); | |
| 117 | }); |
oxlint-plugin-t3code/rules/require-suppression-reason.ts created+70| ... | ... | @@ -0,0 +1,70 @@ |
| 1 | import { defineRule, type ESTree } from "@oxlint/plugins"; | |
| 2 | ||
| 3 | // Comments that switch a diagnostic off or on. None of them explains another. | |
| 4 | const DIRECTIVE_PATTERN = | |
| 5 | /^\s*\*?\s*(?:(?:oxlint|eslint)-(?:disable|enable)|@ts-(?:expect-error|ignore|nocheck)|@effect-diagnostics)/u; | |
| 6 | // TypeScript's own directives; whatever follows one is its reason. | |
| 7 | const TS_DIRECTIVE_PATTERN = /^\s*\*?\s*@ts-(?:expect-error|ignore|nocheck)(?![\w-])(.*)$/su; | |
| 8 | const EMPTY_COMMENT_PATTERN = /^[\s*]*$/u; | |
| 9 | ||
| 10 | /** | |
| 11 | * Reports a directive that disables a lint rule or the type checker without saying why. The reason | |
| 12 | * is a `-- reason` suffix (text after the directive, for `@ts-*`) or a comment on the line above. | |
| 13 | */ | |
| 14 | export default defineRule({ | |
| 15 | meta: { | |
| 16 | type: "suggestion", | |
| 17 | docs: { | |
| 18 | description: "Require every lint or type-checker suppression to say why.", | |
| 19 | }, | |
| 20 | }, | |
| 21 | create(context) { | |
| 22 | const { sourceCode } = context; | |
| 23 | return { | |
| 24 | Program() { | |
| 25 | const comments = sourceCode.getAllComments(); | |
| 26 | const ownLineCommentsByEndLine = new Map<number, ESTree.Comment>(); | |
| 27 | for (const comment of comments) { | |
| 28 | const { line, column } = comment.loc.start; | |
| 29 | if ((sourceCode.lines[line - 1] ?? "").slice(0, column).trim() === "") { | |
| 30 | ownLineCommentsByEndLine.set(comment.loc.end.line, comment); | |
| 31 | } | |
| 32 | } | |
| 33 | ||
| 34 | // A comment directly above explains a directive, even across stacked directives and empty | |
| 35 | // comment lines. | |
| 36 | const explainedAbove = (directive: ESTree.Comment) => { | |
| 37 | let above = ownLineCommentsByEndLine.get(directive.loc.start.line - 1); | |
| 38 | while ( | |
| 39 | above && | |
| 40 | (DIRECTIVE_PATTERN.test(above.value) || EMPTY_COMMENT_PATTERN.test(above.value)) | |
| 41 | ) { | |
| 42 | above = ownLineCommentsByEndLine.get(above.loc.start.line - 1); | |
| 43 | } | |
| 44 | return above !== undefined; | |
| 45 | }; | |
| 46 | ||
| 47 | for (const directive of sourceCode.getDisableDirectives().directives) { | |
| 48 | if (directive.type === "enable" || directive.justification?.trim()) continue; | |
| 49 | if (explainedAbove(directive.node)) continue; | |
| 50 | context.report({ | |
| 51 | loc: directive.node.loc, | |
| 52 | message: | |
| 53 | "Say why: end the directive with `-- reason` or put a comment on the line above.", | |
| 54 | }); | |
| 55 | } | |
| 56 | ||
| 57 | for (const comment of comments) { | |
| 58 | const after = TS_DIRECTIVE_PATTERN.exec(comment.value)?.[1]; | |
| 59 | if (after === undefined || after.replace(/^[\s:-]+/u, "").trim()) continue; | |
| 60 | if (explainedAbove(comment)) continue; | |
| 61 | context.report({ | |
| 62 | loc: comment.loc, | |
| 63 | message: | |
| 64 | "Say why: write the reason after the directive or in a comment on the line above.", | |
| 65 | }); | |
| 66 | } | |
| 67 | }, | |
| 68 | }; | |
| 69 | }, | |
| 70 | }); |
packages/shared/src/qrCode.ts+1-1| ... | ... | @@ -1,4 +1,4 @@ |
| 1 | /* oxlint-disable eslint/no-useless-escape */ | |
| 1 | /* oxlint-disable eslint/no-useless-escape -- vendored Project Nayuki code, kept as upstream wrote it. */ | |
| 2 | 2 | /* |
| 3 | 3 | * QR Code generator library (TypeScript) |
| 4 | 4 | * |
vite.config.ts+1| ... | ... | @@ -170,6 +170,7 @@ export default defineConfig({ |
| 170 | 170 | "t3code/no-test-in-loop": "error", |
| 171 | 171 | "t3code/no-unscoped-has": "error", |
| 172 | 172 | "t3code/namespace-node-imports": "error", |
| 173 | "t3code/require-suppression-reason": "error", | |
| 173 | 174 | }, |
| 174 | 175 | overrides: [ |
| 175 | 176 | { |