| 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,11 +144,11 @@ function stripTerminalControl(text: string): string { |
| 144 | return ( | 144 | return ( |
| 145 | text | 145 | text |
| 146 | .replace( | 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 | /\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[()][A-Za-z0-9]|\x1b[=>]/g, | 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 | .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "") | 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,7 +35,7 @@ export function BrowserDocumentFrame(props: { |
| 35 | }) { | 35 | }) { |
| 36 | const className = "min-h-0 flex-1 border-0 bg-white"; | 36 | const className = "min-h-0 flex-1 border-0 bg-white"; |
| 37 | return props.pdf ? ( | 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 | <iframe | 39 | <iframe |
| 40 | key={props.src} | 40 | key={props.src} |
| 41 | src={`${props.src}${PDF_VIEWER_FRAGMENT}`} | 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,6 +9,7 @@ import noMobileUniwindThemeEscapeHatches from "./rules/no-mobile-uniwind-theme-e |
| 9 | import noNativeTitleTooltip from "./rules/no-native-title-tooltip.ts"; | 9 | import noNativeTitleTooltip from "./rules/no-native-title-tooltip.ts"; |
| 10 | import noTestInLoop from "./rules/no-test-in-loop.ts"; | 10 | import noTestInLoop from "./rules/no-test-in-loop.ts"; |
| 11 | import noUnscopedHas from "./rules/no-unscoped-has.ts"; | 11 | import noUnscopedHas from "./rules/no-unscoped-has.ts"; |
| 12 | import requireSuppressionReason from "./rules/require-suppression-reason.ts"; | ||
| 12 | 13 | ||
| 13 | export default definePlugin({ | 14 | export default definePlugin({ |
| 14 | meta: { | 15 | meta: { |
| ... | @@ -24,5 +25,6 @@ export default definePlugin({ | ... | @@ -24,5 +25,6 @@ export default definePlugin({ |
| 24 | "no-native-title-tooltip": noNativeTitleTooltip, | 25 | "no-native-title-tooltip": noNativeTitleTooltip, |
| 25 | "no-test-in-loop": noTestInLoop, | 26 | "no-test-in-loop": noTestInLoop, |
| 26 | "no-unscoped-has": noUnscopedHas, | 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,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 | * QR Code generator library (TypeScript) | 3 | * QR Code generator library (TypeScript) |
| 4 | * | 4 | * |
vite.config.ts+1| ... | @@ -170,6 +170,7 @@ export default defineConfig({ | ... | @@ -170,6 +170,7 @@ export default defineConfig({ |
| 170 | "t3code/no-test-in-loop": "error", | 170 | "t3code/no-test-in-loop": "error", |
| 171 | "t3code/no-unscoped-has": "error", | 171 | "t3code/no-unscoped-has": "error", |
| 172 | "t3code/namespace-node-imports": "error", | 172 | "t3code/namespace-node-imports": "error", |
| 173 | "t3code/require-suppression-reason": "error", | ||
| 173 | }, | 174 | }, |
| 174 | overrides: [ | 175 | overrides: [ |
| 175 | { | 176 | { |