From 72d165a84c5771a5d6cec692cedf10d0772e9832 Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Tue, 24 Feb 2026 08:16:30 +0100 Subject: [PATCH 1/2] JavaScript: Apply fixes from `oxlint` --- oxlint.json => .oxlintrc.json | 6 +-- .../transformers/herb-linter-transformer.mts | 3 +- javascript/packages/config/rollup.config.mjs | 1 - javascript/packages/config/src/config.ts | 4 +- javascript/packages/config/src/vscode.ts | 2 +- javascript/packages/core/src/ast-utils.ts | 2 +- .../core/test/node-type-guards.test.ts | 2 +- .../packages/dev-tools/src/error-overlay.ts | 2 +- .../packages/dev-tools/src/herb-overlay.ts | 8 +-- javascript/packages/formatter/src/cli.ts | 8 +-- .../packages/formatter/src/format-helpers.ts | 6 +-- .../packages/formatter/src/format-printer.ts | 6 +-- .../packages/formatter/src/formatter.ts | 2 +- .../test/erb/scaffold-templates.test.ts | 2 +- .../test/erb/whitespace-formatting.test.ts | 2 +- .../html/outlook-conditional-comments.test.ts | 2 +- .../test/rewriters/fixtures/uppercase-tags.js | 4 +- .../src/inline-diagnostic-renderer.ts | 2 +- .../highlighter/src/syntax-renderer.ts | 4 +- .../linter-rule/generators/app/index.mjs | 4 +- javascript/packages/linter/src/cli.ts | 4 +- .../linter/src/herb-disable-comment-utils.ts | 3 -- .../src/rules/erb-no-conditional-open-tag.ts | 2 - .../rules/erb-strict-locals-comment-syntax.ts | 4 +- .../src/rules/html-no-duplicate-meta-names.ts | 2 +- .../linter/src/rules/html-no-self-closing.ts | 2 +- .../linter/src/rules/html-no-space-in-tag.ts | 2 +- .../packages/linter/src/rules/rule-utils.ts | 6 +-- .../packages/linter/test/linter.test.ts | 12 ++--- .../printer/src/erb-to-ruby-string-printer.ts | 2 +- javascript/packages/printer/src/printer.ts | 6 +-- .../packages/rewriter/rollup.config.mjs | 7 --- .../src/built-ins/tailwind-class-sorter.ts | 2 +- javascript/packages/stimulus-lint/src/cli.ts | 12 ++--- .../stimulus-lint/src/rules/rule-utils.ts | 2 +- .../rules/stimulus-data-controller-valid.ts | 2 +- .../rules/stimulus-data-value-valid.test.ts | 2 +- .../tailwind-class-sorter/src/config.ts | 54 +++++++++---------- .../tailwind-class-sorter/src/expiring-map.ts | 6 +-- .../tailwind-class-sorter/src/resolve.ts | 4 +- .../tailwind-class-sorter/src/sorting.ts | 21 ++++---- .../tailwind-version-compatibility.test.ts | 2 +- .../packages/vscode/src/analysis-service.ts | 3 +- javascript/packages/vscode/src/client.ts | 4 +- .../vscode/src/config-details-provider.ts | 2 +- .../packages/vscode/src/config-provider.ts | 4 +- javascript/packages/vscode/src/extension.ts | 2 +- .../vscode/src/herb-analysis-provider.ts | 2 +- .../packages/vscode/src/parse-worker.js | 4 +- javascript/packages/vscode/src/utils.ts | 2 +- package.json | 4 +- .../src/controllers/playground_controller.js | 10 ++-- playground/src/monaco.js | 2 +- 53 files changed, 127 insertions(+), 143 deletions(-) rename oxlint.json => .oxlintrc.json (73%) diff --git a/oxlint.json b/.oxlintrc.json similarity index 73% rename from oxlint.json rename to .oxlintrc.json index 4c3a72c30..4841e47ba 100644 --- a/oxlint.json +++ b/.oxlintrc.json @@ -1,12 +1,12 @@ { "$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/crates/oxc_linter/src/schemas/oxlint.json", "rules": { - "@typescript-eslint/no-unused-vars": "error", + "no-unused-vars": ["error", { "argsIgnorePattern": "^_", "caughtErrorsIgnorePattern": "^_" }], + "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "caughtErrorsIgnorePattern": "^_" }], "@typescript-eslint/prefer-nullish-coalescing": "warn", "@typescript-eslint/prefer-optional-chain": "warn", "no-console": "off", "no-debugger": "error", - "no-unused-vars": "error", "prefer-const": "error", "eqeqeq": "error" }, @@ -18,7 +18,7 @@ "globals": { "globalThis": "readonly" }, - "ignore": [ + "ignorePatterns": [ "**/node_modules/**", "**/dist/**", "**/build/**", diff --git a/docs/.vitepress/transformers/herb-linter-transformer.mts b/docs/.vitepress/transformers/herb-linter-transformer.mts index 01cb4b02c..17b0a92bb 100644 --- a/docs/.vitepress/transformers/herb-linter-transformer.mts +++ b/docs/.vitepress/transformers/herb-linter-transformer.mts @@ -4,7 +4,6 @@ import { createPositionConverter } from "twoslash-protocol" import { Herb } from '@herb-tools/node' import { Linter } from '@herb-tools/linter' -import type { LintContext } from '@herb-tools/linter' export interface LinterDiagnostic { line: number @@ -17,7 +16,7 @@ export interface LinterDiagnostic { } // Create custom Twoslash function for linter diagnostics -function createCustomTwoslashFunction(optionse) { +function createCustomTwoslashFunction(options) { return (code, lang, options) => { let fileName = undefined diff --git a/javascript/packages/config/rollup.config.mjs b/javascript/packages/config/rollup.config.mjs index 61e3e6746..edf80ba9a 100644 --- a/javascript/packages/config/rollup.config.mjs +++ b/javascript/packages/config/rollup.config.mjs @@ -1,7 +1,6 @@ import typescript from "@rollup/plugin-typescript" import json from "@rollup/plugin-json" import { nodeResolve } from "@rollup/plugin-node-resolve" -import { readFileSync } from "fs" import { yaml } from "./yaml-plugin.mjs" export default [ diff --git a/javascript/packages/config/src/config.ts b/javascript/packages/config/src/config.ts index 9e6d45b6c..7a7d3a5d9 100644 --- a/javascript/packages/config/src/config.ts +++ b/javascript/packages/config/src/config.ts @@ -772,7 +772,7 @@ export class Config { } } - if (/^ [a-z][\w-]*:/.test(line) && prevLine && /^ /.test(prevLine)) { + if (/^ [a-z][\w-]*:/.test(line) && prevLine && prevLine.startsWith(' ')) { result.push('') } @@ -976,7 +976,7 @@ export class Config { if (!silent) { console.error(`✓ Created default configuration at ${configPath}`) } - } catch (error) { + } catch (_error) { if (!silent) { console.error(`⚠ Could not create config file at ${configPath}, using defaults in-memory`) } diff --git a/javascript/packages/config/src/vscode.ts b/javascript/packages/config/src/vscode.ts index 08d2e0e4d..9e5b9450c 100644 --- a/javascript/packages/config/src/vscode.ts +++ b/javascript/packages/config/src/vscode.ts @@ -49,7 +49,7 @@ function readExtensionsJson(filePath: string): VSCodeExtensionsJson { } return parsed - } catch (error) { + } catch (_error) { console.warn(`Warning: Could not parse ${filePath}, creating new file`) return { recommendations: [] } diff --git a/javascript/packages/core/src/ast-utils.ts b/javascript/packages/core/src/ast-utils.ts index 372f8e14d..e5c82c305 100644 --- a/javascript/packages/core/src/ast-utils.ts +++ b/javascript/packages/core/src/ast-utils.ts @@ -60,7 +60,7 @@ export function isERBCommentNode(node: Node): node is ERBCommentNode { if (!isERBNode(node)) return false if (!node.tag_opening?.value) return false - return node.tag_opening?.value === "<%#" || (node.tag_opening?.value !== "<%#" && (node.content?.value || "").trimStart().startsWith("#")) + return node.tag_opening?.value === "<%#" || (node.tag_opening?.value !== "<%#" && (node.content?.value || "").trimStart().startsWith("#")) } diff --git a/javascript/packages/core/test/node-type-guards.test.ts b/javascript/packages/core/test/node-type-guards.test.ts index a413ffb32..379ec3af8 100644 --- a/javascript/packages/core/test/node-type-guards.test.ts +++ b/javascript/packages/core/test/node-type-guards.test.ts @@ -322,7 +322,7 @@ describe('Node Type Guards', () => { }) it('should handle large arrays', () => { - const manyLiterals = new Array({ length: 100 }).fill(literalNode) + const manyLiterals = Array.from({ length: 100 }).fill(literalNode) expect(areAllOfType(manyLiterals, LiteralNode)).toBe(true) expect(areAllOfType(manyLiterals, HTMLTextNode)).toBe(false) }) diff --git a/javascript/packages/dev-tools/src/error-overlay.ts b/javascript/packages/dev-tools/src/error-overlay.ts index a14fdb345..f22b55b8d 100644 --- a/javascript/packages/dev-tools/src/error-overlay.ts +++ b/javascript/packages/dev-tools/src/error-overlay.ts @@ -76,7 +76,7 @@ export class ErrorOverlay { htmlTemplates.forEach((template, _index) => { try { - let htmlContent = template.innerHTML?.trim() || template.textContent?.trim(); + const htmlContent = template.innerHTML?.trim() || template.textContent?.trim(); if (htmlContent) { this.displayParserErrorOverlay(htmlContent); diff --git a/javascript/packages/dev-tools/src/herb-overlay.ts b/javascript/packages/dev-tools/src/herb-overlay.ts index 2ec5c9a7c..9f0080909 100644 --- a/javascript/packages/dev-tools/src/herb-overlay.ts +++ b/javascript/packages/dev-tools/src/herb-overlay.ts @@ -551,7 +551,7 @@ export class HerbOverlay { if (element.localName === 'html' || window.getComputedStyle(element).overflowY !== 'visible') { label.style.top = '0'; } - + if (window.getComputedStyle(element).position === 'static') { element.style.position = 'relative'; } @@ -586,7 +586,7 @@ export class HerbOverlay { const elements = document.querySelectorAll('[data-herb-debug-showing-erb') elements.forEach(element => { - const originalContent = element.getAttribute('data-herb-debug-original') || ""; + const originalContent = element.getAttribute('data-herb-debug-original') || ""; element.innerHTML = originalContent; element.removeAttribute("data-herb-debug-showing-erb") @@ -617,7 +617,7 @@ export class HerbOverlay { this.addTooltipHoverHandler(element); } } else { - const originalContent = element.getAttribute('data-herb-debug-original') || ""; + const originalContent = element.getAttribute('data-herb-debug-original') || ""; if (element && element.hasAttribute("data-herb-debug-showing-erb")) { element.innerHTML = originalContent; @@ -1055,7 +1055,7 @@ export class HerbOverlay { if (url) { try { window.open(url, '_self'); - } catch (error) { + } catch (_error) { console.log(`Open in editor: ${absolutePath}:${line}:${column}`); } } else { diff --git a/javascript/packages/formatter/src/cli.ts b/javascript/packages/formatter/src/cli.ts index 5047d0c97..fd1798481 100644 --- a/javascript/packages/formatter/src/cli.ts +++ b/javascript/packages/formatter/src/cli.ts @@ -220,8 +220,8 @@ export class CLI { formatterConfig.maxLineLength = maxLineLength } - let preRewriters: ASTRewriter[] = [] - let postRewriters: StringRewriter[] = [] + const preRewriters: ASTRewriter[] = [] + const postRewriters: StringRewriter[] = [] const rewriterNames = { pre: formatterConfig.rewriter?.pre || [], post: formatterConfig.rewriter?.post || [] } if (formatterConfig.rewriter && (rewriterNames.pre.length > 0 || rewriterNames.post.length > 0)) { @@ -420,7 +420,7 @@ export class CLI { } let formattedCount = 0 - let unformattedFiles: string[] = [] + const unformattedFiles: string[] = [] for (const filePath of files) { const displayPath = relative(process.cwd(), filePath) @@ -468,7 +468,7 @@ export class CLI { } let formattedCount = 0 - let unformattedFiles: string[] = [] + const unformattedFiles: string[] = [] for (const filePath of files) { const displayPath = relative(process.cwd(), filePath) diff --git a/javascript/packages/formatter/src/format-helpers.ts b/javascript/packages/formatter/src/format-helpers.ts index 7fa1203ba..b864827ba 100644 --- a/javascript/packages/formatter/src/format-helpers.ts +++ b/javascript/packages/formatter/src/format-helpers.ts @@ -169,7 +169,7 @@ export function isClosingPunctuation(word: string): boolean { * Check if a line ends with opening punctuation */ export function lineEndsWithOpeningPunctuation(line: string): boolean { - return /[(\[]$/.test(line) + return /[([]$/.test(line) } /** @@ -185,14 +185,14 @@ export function isERBTag(text: string): boolean { export function endsWithERBTag(text: string): boolean { const trimmed = text.trim() - return /%>$/.test(trimmed) || /%>\S+$/.test(trimmed) + return trimmed.endsWith('%>') || /%>\S+$/.test(trimmed) } /** * Check if a string starts with an ERB tag */ export function startsWithERBTag(text: string): boolean { - return /^<%/.test(text.trim()) + return text.trim().startsWith('<%') } /** diff --git a/javascript/packages/formatter/src/format-printer.ts b/javascript/packages/formatter/src/format-printer.ts index 452d21afb..c37a50356 100644 --- a/javascript/packages/formatter/src/format-printer.ts +++ b/javascript/packages/formatter/src/format-printer.ts @@ -309,11 +309,11 @@ export class FormatPrinter extends Printer { * and a trailing space (or newline for heredoc content starting with "<<"). */ private formatERBContent(content: string): string { - let trimmedContent = content.trim(); + const trimmedContent = content.trim(); // See: https://github.com/marcoroth/herb/issues/476 // TODO: revisit once we have access to Prism nodes - let suffix = trimmedContent.startsWith("<<") ? "\n" : " " + const suffix = trimmedContent.startsWith("<<") ? "\n" : " " return trimmedContent ? ` ${trimmedContent}${suffix}` : "" } @@ -1264,7 +1264,7 @@ export class FormatPrinter extends Printer { return } - let text = node.content.trim() + const text = node.content.trim() if (!text) return diff --git a/javascript/packages/formatter/src/formatter.ts b/javascript/packages/formatter/src/formatter.ts index 261c32491..4e95ffa76 100644 --- a/javascript/packages/formatter/src/formatter.ts +++ b/javascript/packages/formatter/src/formatter.ts @@ -57,7 +57,7 @@ export class Formatter { * Format a source string, optionally overriding format options per call. */ format(source: string, options: FormatOptions = {}, filePath?: string): string { - let result = this.parse(source) + const result = this.parse(source) if (result.failed) return source if (isScaffoldTemplate(result)) return source diff --git a/javascript/packages/formatter/test/erb/scaffold-templates.test.ts b/javascript/packages/formatter/test/erb/scaffold-templates.test.ts index c889453bd..a84fc70f4 100644 --- a/javascript/packages/formatter/test/erb/scaffold-templates.test.ts +++ b/javascript/packages/formatter/test/erb/scaffold-templates.test.ts @@ -1,4 +1,4 @@ -import { describe, test, expect, beforeAll } from "vitest" +import { describe, test, beforeAll } from "vitest" import { Herb } from "@herb-tools/node-wasm" import { Formatter } from "../../src" import { createExpectFormattedToMatch } from "../helpers" diff --git a/javascript/packages/formatter/test/erb/whitespace-formatting.test.ts b/javascript/packages/formatter/test/erb/whitespace-formatting.test.ts index 5bd701bc1..4735ffccd 100644 --- a/javascript/packages/formatter/test/erb/whitespace-formatting.test.ts +++ b/javascript/packages/formatter/test/erb/whitespace-formatting.test.ts @@ -403,7 +403,7 @@ describe("ERB whitespace formatting", () => { expect(result).toEqual(expected) }) - }), + }) test("documents current behavior for ERB logic tags", () => { const logicCases = ['<% if condition%>', '<%end%>'] diff --git a/javascript/packages/formatter/test/html/outlook-conditional-comments.test.ts b/javascript/packages/formatter/test/html/outlook-conditional-comments.test.ts index 5e3f69b0e..a38666e96 100644 --- a/javascript/packages/formatter/test/html/outlook-conditional-comments.test.ts +++ b/javascript/packages/formatter/test/html/outlook-conditional-comments.test.ts @@ -1,4 +1,4 @@ -import { describe, test, expect, beforeAll } from "vitest" +import { describe, test, beforeAll } from "vitest" import { Herb } from "@herb-tools/node-wasm" import { Formatter } from "../../src" import { createExpectFormattedToMatch } from "../helpers" diff --git a/javascript/packages/formatter/test/rewriters/fixtures/uppercase-tags.js b/javascript/packages/formatter/test/rewriters/fixtures/uppercase-tags.js index cc9e0b89f..e58bf8019 100644 --- a/javascript/packages/formatter/test/rewriters/fixtures/uppercase-tags.js +++ b/javascript/packages/formatter/test/rewriters/fixtures/uppercase-tags.js @@ -39,11 +39,11 @@ export default class UppercaseTagsRewriter extends ASTRewriter { return "Uppercases all HTML tag names (for testing custom rewriters)" } - async initialize(context) { + async initialize(_context) { // No initialization needed } - rewrite(node, context) { + rewrite(node, _context) { const visitor = new UppercaseTagsVisitor() visitor.visit(node) return node diff --git a/javascript/packages/highlighter/src/inline-diagnostic-renderer.ts b/javascript/packages/highlighter/src/inline-diagnostic-renderer.ts index 97c32b580..67b4ffa15 100644 --- a/javascript/packages/highlighter/src/inline-diagnostic-renderer.ts +++ b/javascript/packages/highlighter/src/inline-diagnostic-renderer.ts @@ -82,7 +82,7 @@ export class InlineDiagnosticRenderer { const highestSeverity = this.getHighestSeverity(lineDiagnostics) const lineColor = severityColor(highestSeverity) - let displayLine = line + const displayLine = line let availableWidth = maxWidth if (wrapLines && showLineNumbers) { diff --git a/javascript/packages/highlighter/src/syntax-renderer.ts b/javascript/packages/highlighter/src/syntax-renderer.ts index 56c8334d8..6df0620ad 100644 --- a/javascript/packages/highlighter/src/syntax-renderer.ts +++ b/javascript/packages/highlighter/src/syntax-renderer.ts @@ -121,7 +121,7 @@ export class SyntaxRenderer { let highlighted = "" let lastEnd = 0 - let state: SyntaxRenderState = { + const state: SyntaxRenderState = { inTag: false, inQuotes: false, quoteChar: "", @@ -281,7 +281,7 @@ export class SyntaxRenderer { if (!this.colors) { return null } - + const color = this.colors[token.type as keyof ColorScheme] return color !== undefined ? color : null } diff --git a/javascript/packages/linter/generators/linter-rule/generators/app/index.mjs b/javascript/packages/linter/generators/linter-rule/generators/app/index.mjs index 66a472554..ca8d8102b 100644 --- a/javascript/packages/linter/generators/linter-rule/generators/app/index.mjs +++ b/javascript/packages/linter/generators/linter-rule/generators/app/index.mjs @@ -156,7 +156,7 @@ export default class extends Generator { const newImport = `import { ${this.ruleClassName} } from "./rules/${this.ruleName}.js"` try { - let defaultRulesContent = await fs.readFile(defaultRulesPath, "utf8") + const defaultRulesContent = await fs.readFile(defaultRulesPath, "utf8") const lines = defaultRulesContent.split("\n") const lastImportIndex = lines.findLastIndex(line => line.startsWith("import")) @@ -174,7 +174,7 @@ export default class extends Generator { const newRule = `- [\`${this.ruleName}\`](./${this.ruleName}.md) - ${this.description}` try { - let readmeContent = await fs.readFile(readmePath, "utf8") + const readmeContent = await fs.readFile(readmePath, "utf8") if (!readmeContent.includes(newRule)) { const lines = readmeContent.split("\n") diff --git a/javascript/packages/linter/src/cli.ts b/javascript/packages/linter/src/cli.ts index e8c06f2b6..cc0d7fee4 100644 --- a/javascript/packages/linter/src/cli.ts +++ b/javascript/packages/linter/src/cli.ts @@ -144,7 +144,7 @@ export class CLI { const startTime = Date.now() const startDate = new Date() - let { patterns, configFile, formatOption, showTiming, theme, wrapLines, truncateLines, useGitHubActions, fix, fixUnsafe, ignoreDisableComments, force, init, loadCustomRules, failLevel } = this.argumentParser.parse(process.argv) + const { patterns, configFile, formatOption, showTiming, theme, wrapLines, truncateLines, useGitHubActions, fix, fixUnsafe, ignoreDisableComments, force, init, loadCustomRules, failLevel } = this.argumentParser.parse(process.argv) this.determineProjectPath(patterns) @@ -199,7 +199,7 @@ export class CLI { } let files: string[] - let explicitFiles: string[] = [] + const explicitFiles: string[] = [] if (patterns.length === 0) { files = await config.findFilesForTool('linter', this.projectPath) diff --git a/javascript/packages/linter/src/herb-disable-comment-utils.ts b/javascript/packages/linter/src/herb-disable-comment-utils.ts index a6daa27fe..89375af69 100644 --- a/javascript/packages/linter/src/herb-disable-comment-utils.ts +++ b/javascript/packages/linter/src/herb-disable-comment-utils.ts @@ -2,9 +2,6 @@ * Utilities for parsing herb:disable comments */ -import { isERBCommentNode } from "@herb-tools/core" -import type { Node } from "@herb-tools/core" - /** * Information about a single rule name in a herb:disable comment */ diff --git a/javascript/packages/linter/src/rules/erb-no-conditional-open-tag.ts b/javascript/packages/linter/src/rules/erb-no-conditional-open-tag.ts index 409060a88..adc3d4e1c 100644 --- a/javascript/packages/linter/src/rules/erb-no-conditional-open-tag.ts +++ b/javascript/packages/linter/src/rules/erb-no-conditional-open-tag.ts @@ -1,5 +1,3 @@ -import dedent from "dedent" - import { ParserRule } from "../types.js" import { BaseRuleVisitor } from "./rule-utils.js" diff --git a/javascript/packages/linter/src/rules/erb-strict-locals-comment-syntax.ts b/javascript/packages/linter/src/rules/erb-strict-locals-comment-syntax.ts index bc024cb19..91186399f 100644 --- a/javascript/packages/linter/src/rules/erb-strict-locals-comment-syntax.ts +++ b/javascript/packages/linter/src/rules/erb-strict-locals-comment-syntax.ts @@ -42,7 +42,7 @@ function detectLocalsWithoutColon(content: string): boolean { } function detectSingularLocal(content: string): boolean { - return /^local:/.test(content) + return content.startsWith('local:') } function detectMissingColonBeforeParens(content: string): boolean { @@ -50,7 +50,7 @@ function detectMissingColonBeforeParens(content: string): boolean { } function detectMissingSpaceAfterColon(content: string): boolean { - return /^locals:\(/.test(content) + return content.startsWith('locals:(') } function detectMissingParentheses(content: string): boolean { diff --git a/javascript/packages/linter/src/rules/html-no-duplicate-meta-names.ts b/javascript/packages/linter/src/rules/html-no-duplicate-meta-names.ts index 7312f2353..b8b25e6fe 100644 --- a/javascript/packages/linter/src/rules/html-no-duplicate-meta-names.ts +++ b/javascript/packages/linter/src/rules/html-no-duplicate-meta-names.ts @@ -1,5 +1,5 @@ import { isHTMLElementNode, isHTMLOpenTagNode } from "@herb-tools/core" -import { getTagName, getAttributeName, getAttributeValue, forEachAttribute, getOpenTag } from "./rule-utils" +import { getTagName, getAttributeName, getAttributeValue, forEachAttribute } from "./rule-utils" import { ControlFlowTrackingVisitor, ControlFlowType } from "./rule-utils" import { ParserRule, BaseAutofixContext } from "../types" diff --git a/javascript/packages/linter/src/rules/html-no-self-closing.ts b/javascript/packages/linter/src/rules/html-no-self-closing.ts index a6839ad5f..563b20945 100644 --- a/javascript/packages/linter/src/rules/html-no-self-closing.ts +++ b/javascript/packages/linter/src/rules/html-no-self-closing.ts @@ -62,7 +62,7 @@ export class HTMLNoSelfClosingRule extends ParserRule { const staticAttributeName = getAttributeName(attributeNode) - const originalAttributeName = getAttributeName(attributeNode, false) || "" + const originalAttributeName = getAttributeName(attributeNode, false) || "" const isDynamicName = hasDynamicAttributeName(attributeNode) const staticAttributeValue = getStaticAttributeValue(attributeNode) const valueNodes = getAttributeValueNodes(attributeNode) diff --git a/javascript/packages/linter/test/linter.test.ts b/javascript/packages/linter/test/linter.test.ts index 02b9e0587..577465034 100644 --- a/javascript/packages/linter/test/linter.test.ts +++ b/javascript/packages/linter/test/linter.test.ts @@ -11,7 +11,7 @@ import { HTMLAttributeDoubleQuotesRule } from "../src/rules/html-attribute-doubl import { HTMLAttributeValuesRequireQuotesRule } from "../src/rules/html-attribute-values-require-quotes.js" import { ParserRule, SourceRule } from "../src/types.js" -import type { LintOffense, UnboundLintOffense, LintContext, FullRuleConfig } from "../src/types.js" +import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../src/types.js" import type { ParseResult } from "@herb-tools/core" describe("@herb-tools/linter", () => { @@ -467,10 +467,9 @@ describe("@herb-tools/linter", () => { } }) - const linter = Linter.from(Herb, config) const filteredRules = Linter.filterRulesByConfig([DisabledByDefaultRule], config.linter?.rules) - const linterWithRules = new Linter(Herb, filteredRules) - const lintResult = linterWithRules.lint(html) + const linter = new Linter(Herb, filteredRules, config) + const lintResult = linter.lint(html) expect(lintResult.offenses).toHaveLength(1) expect(lintResult.offenses[0].rule).toBe("disabled-by-default-rule") @@ -488,10 +487,9 @@ describe("@herb-tools/linter", () => { } }) - const linter = Linter.from(Herb, config) const filteredRules = Linter.filterRulesByConfig([HTMLTagNameLowercaseRule], config.linter?.rules) - const linterWithRules = new Linter(Herb, filteredRules) - const lintResult = linterWithRules.lint(html) + const linter = new Linter(Herb, filteredRules, config) + const lintResult = linter.lint(html) expect(lintResult.offenses).toHaveLength(0) }) diff --git a/javascript/packages/printer/src/erb-to-ruby-string-printer.ts b/javascript/packages/printer/src/erb-to-ruby-string-printer.ts index a1fcce00f..148d48f06 100644 --- a/javascript/packages/printer/src/erb-to-ruby-string-printer.ts +++ b/javascript/packages/printer/src/erb-to-ruby-string-printer.ts @@ -2,7 +2,7 @@ import { IdentityPrinter } from "./identity-printer.js" import { PrintOptions, DEFAULT_PRINT_OPTIONS } from "./printer.js" import { isERBOutputNode, filterNodes, ERBContentNode, isERBIfNode, isERBUnlessNode, isERBElseNode, isHTMLTextNode } from "@herb-tools/core" -import { HTMLTextNode, ERBIfNode, ERBElseNode, ERBUnlessNode, Node, HTMLAttributeValueNode } from "@herb-tools/core" +import { HTMLTextNode, ERBIfNode, ERBUnlessNode, Node, HTMLAttributeValueNode } from "@herb-tools/core" export interface ERBToRubyStringOptions extends PrintOptions { /** diff --git a/javascript/packages/printer/src/printer.ts b/javascript/packages/printer/src/printer.ts index ae56187aa..f2f709cad 100644 --- a/javascript/packages/printer/src/printer.ts +++ b/javascript/packages/printer/src/printer.ts @@ -1,8 +1,6 @@ import { Node, Visitor, Token, ParseResult, isToken, isParseResult } from "@herb-tools/core" import { PrintContext } from "./print-context.js" -import type { ERBNode } from "@herb-tools/core" - /** * Options for controlling the printing behavior */ @@ -33,7 +31,7 @@ export abstract class Printer extends Visitor { * @returns The printed string representation of the input * @throws {Error} When node has parse errors and ignoreErrors is false */ - static print(input: Token | Node | ParseResult | Node[] | undefined | null, options: PrintOptions = DEFAULT_PRINT_OPTIONS): string { + static print(input: Token | Node | ParseResult | Node[] | undefined | null, options: PrintOptions = DEFAULT_PRINT_OPTIONS): string { const printer = new (this as any)() return printer.print(input, options) @@ -47,7 +45,7 @@ export abstract class Printer extends Visitor { * @returns The printed string representation of the input * @throws {Error} When node has parse errors and ignoreErrors is false */ - print(input: Token | Node | ParseResult | Node[] | undefined | null, options: PrintOptions = DEFAULT_PRINT_OPTIONS): string { + print(input: Token | Node | ParseResult | Node[] | undefined | null, options: PrintOptions = DEFAULT_PRINT_OPTIONS): string { if (!input) return "" if (isToken(input)) { diff --git a/javascript/packages/rewriter/rollup.config.mjs b/javascript/packages/rewriter/rollup.config.mjs index 74a45c4a0..cf1aea451 100644 --- a/javascript/packages/rewriter/rollup.config.mjs +++ b/javascript/packages/rewriter/rollup.config.mjs @@ -12,13 +12,6 @@ const external = [ "tinyglobby" ] -function isExternal(id) { - return ( - external.includes(id) || - external.some((pkg) => id === pkg || id.startsWith(pkg + "/")) - ) -} - export default [ // Browser-compatible entry point (core APIs only) { diff --git a/javascript/packages/rewriter/src/built-ins/tailwind-class-sorter.ts b/javascript/packages/rewriter/src/built-ins/tailwind-class-sorter.ts index 156710bf0..e68465b89 100644 --- a/javascript/packages/rewriter/src/built-ins/tailwind-class-sorter.ts +++ b/javascript/packages/rewriter/src/built-ins/tailwind-class-sorter.ts @@ -179,7 +179,7 @@ class TailwindClassSorterVisitor extends Visitor { } return this.formatSortedLiteralWithERB(literals[0], fullContent, sortedClasses, others, preserveLeadingSpace, isNested) - } catch (error) { + } catch (_error) { return [...literals, ...others] } } diff --git a/javascript/packages/stimulus-lint/src/cli.ts b/javascript/packages/stimulus-lint/src/cli.ts index f171111e1..b858c3352 100644 --- a/javascript/packages/stimulus-lint/src/cli.ts +++ b/javascript/packages/stimulus-lint/src/cli.ts @@ -110,11 +110,11 @@ class StimulusFileProcessor extends FileProcessor { } } - let totalTargets = 0, usedTargets = 0 - let totalActions = 0, usedActions = 0 - let totalValues = 0, usedValues = 0 - let totalClasses = 0, usedClasses = 0 - let totalOutlets = 0, usedOutlets = 0 + const totalTargets = 0, usedTargets = 0 + const totalActions = 0, usedActions = 0 + const totalValues = 0, usedValues = 0 + const totalClasses = 0, usedClasses = 0 + const totalOutlets = 0, usedOutlets = 0 return { targets: { total: totalTargets, used: usedTargets, unused: totalTargets - usedTargets }, @@ -153,7 +153,7 @@ export class CLI extends HerbLinterCLI { console.log(`Found ${controllerCount} Stimulus controllers`) this.fileProcessor = new StimulusFileProcessor(this.fileProcessor, this.stimulusProject) - } catch (error) { + } catch (_error) { console.log("No Stimulus project found, running without controller validation") this.stimulusProject = undefined diff --git a/javascript/packages/stimulus-lint/src/rules/rule-utils.ts b/javascript/packages/stimulus-lint/src/rules/rule-utils.ts index 19ac323ff..e95613def 100644 --- a/javascript/packages/stimulus-lint/src/rules/rule-utils.ts +++ b/javascript/packages/stimulus-lint/src/rules/rule-utils.ts @@ -213,7 +213,7 @@ export abstract class StimulusAttributeVisitor extends AttributeVisitorMixin { export abstract class HerbParserRule extends ParserRule { isEnabled(_result: ParseResult, context?: Partial): boolean { - if (!context || !context.stimulusProject) return false + if (!context || !context.stimulusProject) return false return true } diff --git a/javascript/packages/stimulus-lint/src/rules/stimulus-data-controller-valid.ts b/javascript/packages/stimulus-lint/src/rules/stimulus-data-controller-valid.ts index 1dafb23fe..48ce1e8c5 100644 --- a/javascript/packages/stimulus-lint/src/rules/stimulus-data-controller-valid.ts +++ b/javascript/packages/stimulus-lint/src/rules/stimulus-data-controller-valid.ts @@ -23,7 +23,7 @@ class DataControllerValidVisitor extends StimulusRuleVisitor { const controllers = this.getControllerIdentifiers(value) for (const controller of controllers) { - this.validateControllerIdentifier(controller, attributeNode.value?.location || attributeNode.location) + this.validateControllerIdentifier(controller, attributeNode.value?.location || attributeNode.location) } } } diff --git a/javascript/packages/stimulus-lint/test/rules/stimulus-data-value-valid.test.ts b/javascript/packages/stimulus-lint/test/rules/stimulus-data-value-valid.test.ts index 15076e2e6..e051ec547 100644 --- a/javascript/packages/stimulus-lint/test/rules/stimulus-data-value-valid.test.ts +++ b/javascript/packages/stimulus-lint/test/rules/stimulus-data-value-valid.test.ts @@ -5,7 +5,7 @@ import { StimulusDataValueValidRule } from '../../src/rules/stimulus-data-value- import type { Project } from 'stimulus-parser' describe('StimulusDataValueValidRule', () => { - let herb = Herb + const herb = Herb const rule = new StimulusDataValueValidRule() beforeAll(async () => { diff --git a/javascript/packages/tailwind-class-sorter/src/config.ts b/javascript/packages/tailwind-class-sorter/src/config.ts index e5a8190c1..cff279425 100644 --- a/javascript/packages/tailwind-class-sorter/src/config.ts +++ b/javascript/packages/tailwind-class-sorter/src/config.ts @@ -13,23 +13,23 @@ import { expiringMap } from './expiring-map.js' import { resolveCssFrom, resolveJsFrom } from './resolve' import type { ContextContainer, SortTailwindClassesOptions } from './types' -let sourceToPathMap = new Map() -let sourceToEntryMap = new Map() -let pathToContextMap = expiringMap(10_000) +const sourceToPathMap = new Map() +const sourceToEntryMap = new Map() +const pathToContextMap = expiringMap(10_000) export async function getTailwindConfig( options: SortTailwindClassesOptions = {}, ): Promise { - let pkgName = 'tailwindcss' + const pkgName = 'tailwindcss' - let key = [ + const key = [ options.baseDir ?? process.cwd(), options.tailwindStylesheet ?? '', options.tailwindConfig ?? '', pkgName, ].join(':') - let baseDir = getBaseDir(options) + const baseDir = getBaseDir(options) // Map the source file to it's associated Tailwind config file let configPath = sourceToPathMap.get(key) @@ -45,14 +45,14 @@ export async function getTailwindConfig( } // Now see if we've loaded the Tailwind config file before (and it's still valid) - let contextKey = `${pkgName}:${configPath}:${entryPoint}` - let existing = pathToContextMap.get(contextKey) + const contextKey = `${pkgName}:${configPath}:${entryPoint}` + const existing = pathToContextMap.get(contextKey) if (existing) { return existing } // By this point we know we need to load the Tailwind config file - let result = await loadTailwindConfig( + const result = await loadTailwindConfig( baseDir, pkgName, configPath, @@ -85,7 +85,7 @@ async function loadTailwindConfig( let tailwindConfig: RequiredConfig = { content: [] } try { - let pkgPath = resolveJsFrom(baseDir, pkgName) + const pkgPath = resolveJsFrom(baseDir, pkgName) let pkgJsonPath: string try { @@ -115,14 +115,14 @@ async function loadTailwindConfig( } } - let pkgDir = path.dirname(pkgJsonPath) + const pkgDir = path.dirname(pkgJsonPath) try { - let v4 = await loadV4(baseDir, pkgDir, pkgName, entryPoint) + const v4 = await loadV4(baseDir, pkgDir, pkgName, entryPoint) if (v4) { return v4 } - } catch (err) { + } catch (_error) { // V4 loading failed, will try v3 below } @@ -136,7 +136,7 @@ async function loadTailwindConfig( // Prior to `tailwindcss@3.3.0` this won't exist so we load it last loadConfig = require(path.join(pkgDir, 'loadConfig')) - } catch (err: any) { + } catch (_error: any) { // Tailwind isn't installed or loading failed, will use defaults } @@ -159,7 +159,7 @@ async function loadTailwindConfig( tailwindConfig.content = ['no-op'] - let context = createContext(resolveConfig(tailwindConfig)) + const context = createContext(resolveConfig(tailwindConfig)) return { context, @@ -183,13 +183,13 @@ function createLoader({ filepath: string onError: (id: string, error: unknown, resourceType: string) => T }) { - let cacheKey = `${+Date.now()}` + const cacheKey = `${+Date.now()}` async function loadFile(id: string, base: string, resourceType: string) { try { - let resolved = resolveJsFrom(base, id) + const resolved = resolveJsFrom(base, id) - let url = pathToFileURL(resolved) + const url = pathToFileURL(resolved) url.searchParams.append('t', cacheKey) return await jiti.import(url.href, { default: true }) @@ -199,7 +199,7 @@ function createLoader({ } if (legacy) { - let baseDir = path.dirname(filepath) + const baseDir = path.dirname(filepath) return (id: string) => loadFile(id, baseDir, 'module') } @@ -218,9 +218,9 @@ async function loadV4( entryPoint: string | null, ) { // Import Tailwind — if this is v4 it'll have APIs we can use directly - let pkgPath = resolveJsFrom(baseDir, pkgName) + const pkgPath = resolveJsFrom(baseDir, pkgName) - let tw = await import(pathToFileURL(pkgPath).toString()) + const tw = await import(pathToFileURL(pkgPath).toString()) // This is not Tailwind v4 if (!tw.__unstable__loadDesignSystem) { @@ -231,12 +231,12 @@ async function loadV4( entryPoint = entryPoint ?? `${pkgDir}/theme.css` // Create a Jiti instance that can be used to load plugins and config files - let jiti = createJiti(import.meta.url, { + const jiti = createJiti(import.meta.url, { moduleCache: false, fsCache: false, }) - let importBasePath = path.dirname(entryPoint) + const importBasePath = path.dirname(entryPoint) // Resolve imports in the entrypoint to a flat CSS tree let css = await fs.readFile(entryPoint, 'utf-8') @@ -256,14 +256,14 @@ async function loadV4( } catch {} if (!supportsImports) { - let resolveImports = postcss([postcssImport()]) - let result = await resolveImports.process(css, { from: entryPoint }) + const resolveImports = postcss([postcssImport()]) + const result = await resolveImports.process(css, { from: entryPoint }) css = result.css } // Load the design system and set up a compatible context object that is // usable by the rest of the plugin - let design = await tw.__unstable__loadDesignSystem(css, { + const design = await tw.__unstable__loadDesignSystem(css, { base: importBasePath, // v4.0.0-alpha.25+ @@ -283,7 +283,7 @@ async function loadV4( }), loadStylesheet: async (id: string, base: string) => { - let resolved = resolveCssFrom(base, id) + const resolved = resolveCssFrom(base, id) return { base: path.dirname(resolved), diff --git a/javascript/packages/tailwind-class-sorter/src/expiring-map.ts b/javascript/packages/tailwind-class-sorter/src/expiring-map.ts index 58d1816d9..366bc7bf2 100644 --- a/javascript/packages/tailwind-class-sorter/src/expiring-map.ts +++ b/javascript/packages/tailwind-class-sorter/src/expiring-map.ts @@ -4,11 +4,11 @@ interface ExpiringMap { } export function expiringMap(duration: number): ExpiringMap { - let map = new Map() + const map = new Map() return { get(key: K) { - let result = map.get(key) + const result = map.get(key) if (!result) return undefined if (result.expiration <= new Date()) { map.delete(key) @@ -19,7 +19,7 @@ export function expiringMap(duration: number): ExpiringMap { }, set(key: K, value: V) { - let expiration = new Date() + const expiration = new Date() expiration.setMilliseconds(expiration.getMilliseconds() + duration) map.set(key, { diff --git a/javascript/packages/tailwind-class-sorter/src/resolve.ts b/javascript/packages/tailwind-class-sorter/src/resolve.ts index 41063ce53..8377e83d6 100644 --- a/javascript/packages/tailwind-class-sorter/src/resolve.ts +++ b/javascript/packages/tailwind-class-sorter/src/resolve.ts @@ -53,10 +53,10 @@ export function maybeResolve(name: string) { } export async function loadIfExists(name: string): Promise { - let modpath = maybeResolve(name) + const modpath = maybeResolve(name) if (modpath) { - let mod = await import(name) + const mod = await import(name) return mod.default ?? mod } diff --git a/javascript/packages/tailwind-class-sorter/src/sorting.ts b/javascript/packages/tailwind-class-sorter/src/sorting.ts index ec05204fa..ba7c4d613 100644 --- a/javascript/packages/tailwind-class-sorter/src/sorting.ts +++ b/javascript/packages/tailwind-class-sorter/src/sorting.ts @@ -8,7 +8,7 @@ function prefixCandidate( context: TailwindContext, selector: string, ): string { - let prefix = context.tailwindConfig.prefix + const prefix = context.tailwindConfig.prefix return typeof prefix === 'function' ? prefix(selector) : prefix + selector } @@ -25,14 +25,14 @@ function getClassOrderPolyfill( // that don't exist on their own. This will result in them "not existing" and // sorting could be weird since you still require them in order to make the // host utitlies work properly. (Thanks Biology) - let parasiteUtilities = new Set([ + const parasiteUtilities = new Set([ prefixCandidate(env.context, 'group'), prefixCandidate(env.context, 'peer'), ]) - let classNamesWithOrder: [string, bigint | null][] = [] + const classNamesWithOrder: [string, bigint | null][] = [] - for (let className of classes) { + for (const className of classes) { let order: bigint | null = env .generateRules(new Set([className]), env.context) @@ -56,7 +56,7 @@ function reorderClasses(classList: string[], { env }: { env: SortEnv }) { return classList.map(name => [name, null] as [string, bigint | null]) } - let orderedClasses = env.context.getClassOrder + const orderedClasses = env.context.getClassOrder ? env.context.getClassOrder(classList) : getClassOrderPolyfill(classList, { env }) @@ -109,8 +109,9 @@ export function sortClasses( } let result = '' - let parts = classStr.split(/([\t\r\f\n ]+)/) - let classes = parts.filter((_, i) => i % 2 === 0) + const parts = classStr.split(/([\t\r\f\n ]+)/) + const classes = parts.filter((_, i) => i % 2 === 0) + let whitespace = parts.filter((_, i) => i % 2 !== 0) if (classes[classes.length - 1] === '') { @@ -131,7 +132,7 @@ export function sortClasses( suffix = `${whitespace.pop() ?? ''}${classes.pop() ?? ''}` } - let { classList, removedIndices } = sortClassList(classes, { + const { classList, removedIndices } = sortClassList(classes, { env, removeDuplicates, }) @@ -173,10 +174,10 @@ export function sortClassList( removeDuplicates = false } - let removedIndices = new Set() + const removedIndices = new Set() if (removeDuplicates) { - let seenClasses = new Set() + const seenClasses = new Set() orderedClasses = orderedClasses.filter(([cls, order], index) => { if (seenClasses.has(cls)) { diff --git a/javascript/packages/tailwind-class-sorter/test/tailwind-version-compatibility.test.ts b/javascript/packages/tailwind-class-sorter/test/tailwind-version-compatibility.test.ts index ce7e93c20..6bb4523a3 100644 --- a/javascript/packages/tailwind-class-sorter/test/tailwind-version-compatibility.test.ts +++ b/javascript/packages/tailwind-class-sorter/test/tailwind-version-compatibility.test.ts @@ -1,5 +1,5 @@ import path from 'path' -import { describe, it, expect, beforeEach, afterEach } from 'vitest' +import { describe, it, expect } from 'vitest' import { sortTailwindClasses, TailwindClassSorter } from '../src/index' const v3ConfigPath = path.join(__dirname, 'fixtures', 'tailwind.config.js') diff --git a/javascript/packages/vscode/src/analysis-service.ts b/javascript/packages/vscode/src/analysis-service.ts index 5c988c09d..86734b55a 100644 --- a/javascript/packages/vscode/src/analysis-service.ts +++ b/javascript/packages/vscode/src/analysis-service.ts @@ -35,7 +35,7 @@ export class AnalysisService { formatterMaxLineLength = projectConfig.formatter?.maxLineLength ?? 80 linterRules = projectConfig.linter?.rules ?? {} } - } catch (error) { + } catch (_error) { const vscodeConfig = vscode.workspace.getConfiguration('languageServerHerb') linterEnabled = vscodeConfig.get('linter.enabled', true) formatterEnabled = vscodeConfig.get('formatter.enabled', false) @@ -53,6 +53,7 @@ export class AnalysisService { JSON.stringify(linterRules), workspaceRoot ], { timeout: 1000 }) + const result = JSON.parse(stdout.trim()) const failed = result.errors > 0 || result.lintErrors > 0 diff --git a/javascript/packages/vscode/src/client.ts b/javascript/packages/vscode/src/client.ts index a3d49669a..da1083d35 100644 --- a/javascript/packages/vscode/src/client.ts +++ b/javascript/packages/vscode/src/client.ts @@ -89,7 +89,7 @@ export class Client { server: vscodeConfig.get('trace.server', 'verbose'), }, } - } catch (error) { + } catch (_error) { const vscodeConfig = workspace.getConfiguration('languageServerHerb') settings = { @@ -182,7 +182,7 @@ export class Client { server: vscodeConfig.get('trace.server', 'verbose'), // Trace is always from VS Code }, } - } catch (error) { + } catch (_error) { const vscodeConfig = workspace.getConfiguration('languageServerHerb') return { diff --git a/javascript/packages/vscode/src/config-details-provider.ts b/javascript/packages/vscode/src/config-details-provider.ts index bba6675be..cc7975b95 100644 --- a/javascript/packages/vscode/src/config-details-provider.ts +++ b/javascript/packages/vscode/src/config-details-provider.ts @@ -21,7 +21,7 @@ export async function showConfigDetails() { hasConfigFile = true config = await Config.loadForEditor(workspaceRoot!) - } catch (error) { + } catch (_error) { // No config file or failed to load } } diff --git a/javascript/packages/vscode/src/config-provider.ts b/javascript/packages/vscode/src/config-provider.ts index 0e5db5891..bbfb37bd9 100644 --- a/javascript/packages/vscode/src/config-provider.ts +++ b/javascript/packages/vscode/src/config-provider.ts @@ -302,7 +302,7 @@ export class HerbConfigProvider implements vscode.TreeDataProvider { try { const document = await vscode.workspace.openTextDocument(configPath) await vscode.window.showTextDocument(document) - } catch (error) { + } catch (_error) { const result = await vscode.window.showInformationMessage( "Herb configuration file (.herb.yml) not found. Would you like to create one?", "Create Config", @@ -332,7 +332,7 @@ class ConfigItem extends vscode.TreeItem { public readonly contextValue: string ) { super(label, collapsibleState) + this.tooltip = `${this.label}: ${this.description}` - this.description = description } } diff --git a/javascript/packages/vscode/src/extension.ts b/javascript/packages/vscode/src/extension.ts index 93b76521f..9a52d5a36 100644 --- a/javascript/packages/vscode/src/extension.ts +++ b/javascript/packages/vscode/src/extension.ts @@ -50,7 +50,7 @@ async function updateConfigStatusBarItem() { configStatusBarItem.command = 'herb.showConfigDetails' configStatusBarItem.show() - } catch (error) { + } catch (_error) { configStatusBarItem.text = '$(settings-gear) Herb (Personal Settings)' configStatusBarItem.tooltip = 'Herb using personal VS Code settings\n\nClick to view configuration details or create .herb.yml' configStatusBarItem.command = 'herb.showConfigDetails' diff --git a/javascript/packages/vscode/src/herb-analysis-provider.ts b/javascript/packages/vscode/src/herb-analysis-provider.ts index 8904e737b..28269a326 100644 --- a/javascript/packages/vscode/src/herb-analysis-provider.ts +++ b/javascript/packages/vscode/src/herb-analysis-provider.ts @@ -74,7 +74,7 @@ export class HerbAnalysisProvider implements TreeDataProvider { if (commonExclude.length > 0) { excludePattern = `{${commonExclude.join(',')}}` } - } catch (error) { + } catch (_error) { window.showErrorMessage( 'Cannot run Herb analysis: Configuration file has errors. Please fix .herb.yml and try again.', 'Edit Config' diff --git a/javascript/packages/vscode/src/parse-worker.js b/javascript/packages/vscode/src/parse-worker.js index a3e3b3ff2..c367f06b0 100644 --- a/javascript/packages/vscode/src/parse-worker.js +++ b/javascript/packages/vscode/src/parse-worker.js @@ -16,7 +16,7 @@ const { Config } = require('@herb-tools/config'); let linterRules = {}; try { linterRules = JSON.parse(linterRulesJson); - } catch (e) { + } catch (_error) { // Invalid JSON, use empty rules } @@ -68,7 +68,7 @@ const { Config } = require('@herb-tools/config'); silent: true }); customRules = result.rules; - } catch (customRuleError) { + } catch (_customRuleError) { // Ignore custom rule loading errors in worker } diff --git a/javascript/packages/vscode/src/utils.ts b/javascript/packages/vscode/src/utils.ts index ba8866bd2..61d9446e7 100644 --- a/javascript/packages/vscode/src/utils.ts +++ b/javascript/packages/vscode/src/utils.ts @@ -47,7 +47,7 @@ export async function getHerbSettings(): Promise { const configPath = path.join(workspaceRoot, '.herb.yml') projectConfig = await fs.readFile(configPath, 'utf8') } - } catch (error) { + } catch (_error) { projectConfig = null } diff --git a/package.json b/package.json index a5e09e230..ea603d447 100644 --- a/package.json +++ b/package.json @@ -45,8 +45,8 @@ "playground": "nx run playground:dev", "docs": "nx dev docs", "lint": "yarn prettier '**/*.{js,mjs,cjs,ts,mts,cts,css,html}' --check", - "lint:oxc": "oxlint --config oxlint.json javascript/packages/", - "lint:oxc:fix": "oxlint --config oxlint.json --fix javascript/packages/", + "lint:oxc": "oxlint javascript/packages/", + "lint:oxc:fix": "oxlint --fix javascript/packages/", "format": "yarn prettier '**/*.{js,mjs,cjs,ts,mts,cts,css,html}' --write", "release": "tsx javascript/tools/scripts/release.ts" }, diff --git a/playground/src/controllers/playground_controller.js b/playground/src/controllers/playground_controller.js index b19f9af6e..069f218df 100644 --- a/playground/src/controllers/playground_controller.js +++ b/playground/src/controllers/playground_controller.js @@ -1031,17 +1031,17 @@ export default class extends Controller { }) } - onOptionChange(event) { + onOptionChange(_event) { this.updateURL() this.analyze() } - onPrinterOptionChange(event) { + onPrinterOptionChange(_event) { this.updateURL() this.analyze() } - onFormatterOptionChange(event) { + onFormatterOptionChange(_event) { this.updateURL() this.analyze() } @@ -1266,7 +1266,7 @@ export default class extends Controller { const startLine = diagnostic.line || diagnostic.startLineNumber || 1 const startColumn = (diagnostic.column || diagnostic.startColumn || 0) + 1 const endLine = diagnostic.endLine || diagnostic.endLineNumber || startLine - const endColumn = (diagnostic.endColumn || diagnostic.endColumn || diagnostic.column || 0) + 1 + const endColumn = (diagnostic.endColumn || diagnostic.column || 0) + 1 groupHtml += `
Date: Tue, 24 Feb 2026 19:01:21 +0100 Subject: [PATCH 2/2] More fixes --- .doxygen/frame.js | 2 +- .oxlintrc.json | 4 +- docs/.vitepress/generate-rules.mts | 6 - docs/.vitepress/theme/index.ts | 1 - .../transformers/herb-linter-transformer.mts | 4 +- javascript/packages/config/src/config.ts | 97 +++-- javascript/packages/formatter/src/cli.ts | 1 - javascript/packages/highlighter/src/color.ts | 11 + .../highlighter/src/diagnostic-renderer.ts | 11 +- .../packages/highlighter/src/line-wrapper.ts | 16 +- .../highlighter/src/text-formatter.ts | 10 +- javascript/packages/highlighter/src/util.ts | 12 +- .../highlighter/test/ansi-utils.test.ts | 363 ++++++++++++++++++ .../test/diagnostic-renderer.test.ts | 121 ++++-- .../highlighter/test/highlighter.test.ts | 3 +- .../highlighter/test/syntax-renderer.test.ts | 5 +- javascript/packages/highlighter/test/util.ts | 4 +- .../src/controllers/playground_controller.js | 17 +- 18 files changed, 552 insertions(+), 136 deletions(-) create mode 100644 javascript/packages/highlighter/test/ansi-utils.test.ts diff --git a/.doxygen/frame.js b/.doxygen/frame.js index 0262b59da..14ebc62a7 100644 --- a/.doxygen/frame.js +++ b/.doxygen/frame.js @@ -1,5 +1,5 @@ document.addEventListener("DOMContentLoaded", () => { - if (window.parent.location == window.location) { + if (window.parent.location === window.location) { document.body.classList.add("not-inside-iframe") } else { document.body.classList.add("inside-iframe") diff --git a/.oxlintrc.json b/.oxlintrc.json index 4841e47ba..e1ece5f5b 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -1,8 +1,8 @@ { "$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/crates/oxc_linter/src/schemas/oxlint.json", "rules": { - "no-unused-vars": ["error", { "argsIgnorePattern": "^_", "caughtErrorsIgnorePattern": "^_" }], - "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "caughtErrorsIgnorePattern": "^_" }], + "no-unused-vars": ["error", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_", "caughtErrorsIgnorePattern": "^_" }], + "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_", "caughtErrorsIgnorePattern": "^_" }], "@typescript-eslint/prefer-nullish-coalescing": "warn", "@typescript-eslint/prefer-optional-chain": "warn", "no-console": "off", diff --git a/docs/.vitepress/generate-rules.mts b/docs/.vitepress/generate-rules.mts index 84a24f1bc..b72188961 100644 --- a/docs/.vitepress/generate-rules.mts +++ b/docs/.vitepress/generate-rules.mts @@ -18,12 +18,6 @@ export function generateRuleWrappers() { const ruleFiles = files.filter(file => file.endsWith(".md") && file !== "README.md") ruleFiles.forEach(file => { - const ruleName = file.replace(".md", "") - const displayName = ruleName - .split("-") - .map(word => word.charAt(0).toUpperCase() + word.slice(1)) - .join(" ") - const wrapperContent = `` const targetPath = path.join(targetRulesDir, file) diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts index b2bfbf90e..6c62be33a 100644 --- a/docs/.vitepress/theme/index.ts +++ b/docs/.vitepress/theme/index.ts @@ -1,6 +1,5 @@ import type { EnhanceAppContext } from "vitepress" import Theme from "vitepress/theme" -import { h } from "vue" import { onMounted, watch, nextTick } from 'vue' import { useRoute } from 'vitepress' import mediumZoom from 'medium-zoom' diff --git a/docs/.vitepress/transformers/herb-linter-transformer.mts b/docs/.vitepress/transformers/herb-linter-transformer.mts index 17b0a92bb..d4044dc09 100644 --- a/docs/.vitepress/transformers/herb-linter-transformer.mts +++ b/docs/.vitepress/transformers/herb-linter-transformer.mts @@ -16,8 +16,8 @@ export interface LinterDiagnostic { } // Create custom Twoslash function for linter diagnostics -function createCustomTwoslashFunction(options) { - return (code, lang, options) => { +function createCustomTwoslashFunction(_options) { + return (code, lang, _options) => { let fileName = undefined // kind of a hack to make sure we pass a `fileName` for the `erb-require-trailing-newline` rule diff --git a/javascript/packages/config/src/config.ts b/javascript/packages/config/src/config.ts index 7a7d3a5d9..ad7ec0ddd 100644 --- a/javascript/packages/config/src/config.ts +++ b/javascript/packages/config/src/config.ts @@ -1090,69 +1090,66 @@ export class Config { version: string, exitOnError: boolean = false ): Promise { - try { - const content = await fs.readFile(configPath, "utf8") + const content = await fs.readFile(configPath, "utf8") - let parsed: any - try { - parsed = parse(content) - } catch (error) { - if (exitOnError) { - console.error(`\n✗ Invalid YAML syntax in ${configPath}`) + let parsed: any - if (error instanceof Error) { - console.error(` ${error.message}\n`) - } - process.exit(1) - } else { - throw new Error(`Invalid YAML syntax in ${configPath}: ${error instanceof Error ? error.message : String(error)}`) + try { + parsed = parse(content) + } catch (error) { + if (exitOnError) { + console.error(`\n✗ Invalid YAML syntax in ${configPath}`) + + if (error instanceof Error) { + console.error(` ${error.message}\n`) } + process.exit(1) + } else { + throw new Error(`Invalid YAML syntax in ${configPath}: ${error instanceof Error ? error.message : String(error)}`) } + } - if (parsed === null || parsed === undefined) { - parsed = {} - } + if (parsed === null || parsed === undefined) { + parsed = {} + } - if (!parsed.version) { - parsed.version = version - } + if (!parsed.version) { + parsed.version = version + } - try { - HerbConfigSchema.parse(parsed) - } catch (error) { - if (error instanceof ZodError) { - const validationError = fromZodError(error, { - prefix: `Configuration errors in ${configPath}`, - }) - - if (exitOnError) { - console.error(`\n✗ ${validationError.toString()}\n`) - - process.exit(1) - } else { - throw new Error(validationError.toString()) - } - } + try { + HerbConfigSchema.parse(parsed) + } catch (error) { + if (error instanceof ZodError) { + const validationError = fromZodError(error, { + prefix: `Configuration errors in ${configPath}`, + }) - throw error - } + if (exitOnError) { + console.error(`\n✗ ${validationError.toString()}\n`) - if (parsed.version && parsed.version !== version) { - console.error(`\n⚠️ Configuration version mismatch in ${configPath}`) - console.error(` Config version: ${parsed.version}`) - console.error(` Current version: ${version}`) - console.error(` Consider updating your .herb.yml file.\n`) + process.exit(1) + } else { + throw new Error(validationError.toString()) + } } - const defaults = this.getDefaultConfig(version) - const resolved = deepMerge(defaults, parsed as Partial) - - resolved.version = version - - return new Config(projectRoot, resolved) - } catch (error) { throw error } + + if (parsed.version && parsed.version !== version) { + console.error(`\n⚠️ Configuration version mismatch in ${configPath}`) + console.error(` Config version: ${parsed.version}`) + console.error(` Current version: ${version}`) + console.error(` Consider updating your .herb.yml file.\n`) + } + + const defaults = this.getDefaultConfig(version) + const resolved = deepMerge(defaults, parsed as Partial) + + resolved.version = version + + return new Config(projectRoot, resolved) } /** diff --git a/javascript/packages/formatter/src/cli.ts b/javascript/packages/formatter/src/cli.ts index fd1798481..2d7825dde 100644 --- a/javascript/packages/formatter/src/cli.ts +++ b/javascript/packages/formatter/src/cli.ts @@ -156,7 +156,6 @@ export class CLI { this.determineProjectPath(positionals) const file = positionals[0] - const startPath = file || process.cwd() if (isInitMode) { const configPath = configFile || this.projectPath diff --git a/javascript/packages/highlighter/src/color.ts b/javascript/packages/highlighter/src/color.ts index 74f5aca17..bf0fd690f 100644 --- a/javascript/packages/highlighter/src/color.ts +++ b/javascript/packages/highlighter/src/color.ts @@ -1,5 +1,16 @@ import { DiagnosticSeverity } from "@herb-tools/core" +export const ANSI_ESCAPE = "\x1b" + +// eslint-disable-next-line no-control-regex +export const ANSI_REGEX = /\x1b\[[0-9;]*m/g + +// eslint-disable-next-line no-control-regex +export const ANSI_REGEX_START = /^\x1b\[[0-9;]*m/ + +// eslint-disable-next-line no-control-regex +export const ANSI_REGEX_CAPTURE = /(\x1b\[[0-9;]*m)/g + export const colors = { reset: "\x1b[0m", bold: "\x1b[1m", diff --git a/javascript/packages/highlighter/src/diagnostic-renderer.ts b/javascript/packages/highlighter/src/diagnostic-renderer.ts index dc89a75ca..bf1e1df90 100644 --- a/javascript/packages/highlighter/src/diagnostic-renderer.ts +++ b/javascript/packages/highlighter/src/diagnostic-renderer.ts @@ -1,4 +1,4 @@ -import { colorize, severityColor } from "./color.js" +import { colorize, severityColor, ANSI_REGEX, ANSI_REGEX_START, ANSI_ESCAPE } from "./color.js" import { applyDimToStyledText } from "./util.js" import { LineWrapper } from "./line-wrapper.js" import { GUTTER_WIDTH, MIN_CONTENT_WIDTH } from "./gutter-config.js" @@ -38,8 +38,7 @@ export class DiagnosticRenderer { diagnosticEnd: number, maxWidth: number ): { line: string; adjustedStart: number; adjustedEnd: number } { - const ansiRegex = /\x1b\[[0-9;]*m/g - const plainLine = line.replace(ansiRegex, "") + const plainLine = line.replace(ANSI_REGEX, "") if (plainLine.length <= maxWidth) { return { line, adjustedStart: diagnosticStart, adjustedEnd: diagnosticEnd } @@ -98,8 +97,8 @@ export class DiagnosticRenderer { while (styledIndex < styledLine.length && plainIndex <= endPos) { const char = styledLine[styledIndex] - if (char === "\x1b") { - const ansiMatch = styledLine.slice(styledIndex).match(/^\x1b\[[0-9;]*m/) + if (char === ANSI_ESCAPE) { + const ansiMatch = styledLine.slice(styledIndex).match(ANSI_REGEX_START) if (ansiMatch) { if (inRange || plainIndex >= startPos) { result += ansiMatch[0] @@ -132,7 +131,7 @@ export class DiagnosticRenderer { ): string { const { contextLines = 2, - showLineNumbers: _showLineNumbers = true, + showLineNumbers: _showLineNumbers = true, // eslint-disable-line no-unused-vars optimizeHighlighting = true, wrapLines = true, maxWidth = LineWrapper.getTerminalWidth(), diff --git a/javascript/packages/highlighter/src/line-wrapper.ts b/javascript/packages/highlighter/src/line-wrapper.ts index 3e87ff4b0..96f692866 100644 --- a/javascript/packages/highlighter/src/line-wrapper.ts +++ b/javascript/packages/highlighter/src/line-wrapper.ts @@ -1,11 +1,10 @@ -import { colorize } from "./color.js" +import { colorize, ANSI_REGEX, ANSI_REGEX_START, ANSI_ESCAPE } from "./color.js" export class LineWrapper { static wrapLine(line: string, maxWidth: number, indent: string = ""): string[] { if (maxWidth <= 0) return [line] - const ansiRegex = /\x1b\[[0-9;]*m/g - const plainLine = line.replace(ansiRegex, "") + const plainLine = line.replace(ANSI_REGEX, "") if (plainLine.length <= maxWidth) { return [line] @@ -79,8 +78,8 @@ export class LineWrapper { while (plainIndex < endIndex && styledIndex < styledLine.length) { const char = styledLine[styledIndex] - if (char === "\x1b") { - const ansiMatch = styledLine.slice(styledIndex).match(/^\x1b\[[0-9;]*m/) + if (char === ANSI_ESCAPE) { + const ansiMatch = styledLine.slice(styledIndex).match(ANSI_REGEX_START) if (ansiMatch) { result += ansiMatch[0] @@ -104,8 +103,8 @@ export class LineWrapper { while (plainIndex < startIndex && styledIndex < styledLine.length) { const char = styledLine[styledIndex] - if (char === "\x1b") { - const ansiMatch = styledLine.slice(styledIndex).match(/^\x1b\[[0-9;]*m/) + if (char === ANSI_ESCAPE) { + const ansiMatch = styledLine.slice(styledIndex).match(ANSI_REGEX_START) if (ansiMatch) { styledIndex += ansiMatch[0].length @@ -122,8 +121,7 @@ export class LineWrapper { static truncateLine(line: string, maxWidth: number): string { if (maxWidth <= 0) return line - const ansiRegex = /\x1b\[[0-9;]*m/g - const plainLine = line.replace(ansiRegex, "") + const plainLine = line.replace(ANSI_REGEX, "") if (plainLine.length <= maxWidth) { return line diff --git a/javascript/packages/highlighter/src/text-formatter.ts b/javascript/packages/highlighter/src/text-formatter.ts index a90e8cb9d..5c09baee7 100644 --- a/javascript/packages/highlighter/src/text-formatter.ts +++ b/javascript/packages/highlighter/src/text-formatter.ts @@ -1,9 +1,13 @@ +import { colors, ANSI_REGEX } from "./color.js" + export class TextFormatter { static applyDimToStyledText(text: string): string { const isColorEnabled = process.env.NO_COLOR === undefined if (!isColorEnabled) return text - return text.replace(/\x1b\[([0-9;]*)m/g, (match, codes) => { + return text.replace(ANSI_REGEX, (match) => { + const codes = match.slice(2, -1) + if (codes === "0" || codes === "") { return match } @@ -14,9 +18,7 @@ export class TextFormatter { static highlightBackticks(text: string): string { if (process.stdout.isTTY && process.env.NO_COLOR === undefined) { - const boldWhite = "\x1b[1m\x1b[37m" - const reset = "\x1b[0m" - return text.replace(/`([^`]+)`/g, `${boldWhite}$1${reset}`) + return text.replace(/`([^`]+)`/g, `${colors.bold}${colors.white}$1${colors.reset}`) } return text } diff --git a/javascript/packages/highlighter/src/util.ts b/javascript/packages/highlighter/src/util.ts index ff3843e7b..4bbff0fd8 100644 --- a/javascript/packages/highlighter/src/util.ts +++ b/javascript/packages/highlighter/src/util.ts @@ -1,20 +1,22 @@ +import { colors, ANSI_REGEX_START, ANSI_REGEX_CAPTURE } from "./color.js" + export function applyDimToStyledText(text: string): string { const isColorEnabled = process.env.NO_COLOR === undefined if (!isColorEnabled) return text - const parts = text.split(/(\x1b\[[0-9;]*m)/g) + const parts = text.split(ANSI_REGEX_CAPTURE) let result = "" for (let i = 0; i < parts.length; i++) { const part = parts[i] - if (part.match(/^\x1b\[[0-9;]*m$/)) { - if (part === "\x1b[0m") { + if (part.match(ANSI_REGEX_START)) { + if (part === colors.reset) { result += part } else { - const codes = part.match(/\x1b\[([0-9;]*)m/)?.[1] + const codes = part.slice(2, -1) if (codes && codes !== "0" && codes !== "") { result += `\x1b[2;${codes}m` @@ -23,7 +25,7 @@ export function applyDimToStyledText(text: string): string { } } } else if (part.length > 0) { - result += `\x1b[2m${part}\x1b[22m` + result += `${colors.dim}${part}\x1b[22m` } } diff --git a/javascript/packages/highlighter/test/ansi-utils.test.ts b/javascript/packages/highlighter/test/ansi-utils.test.ts new file mode 100644 index 000000000..1737d1d21 --- /dev/null +++ b/javascript/packages/highlighter/test/ansi-utils.test.ts @@ -0,0 +1,363 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest" + +import { colors, ANSI_ESCAPE, ANSI_REGEX, ANSI_REGEX_START, ANSI_REGEX_CAPTURE, colorize } from "../src/color.js" +import { applyDimToStyledText } from "../src/util.js" +import { TextFormatter } from "../src/text-formatter.js" +import { LineWrapper } from "../src/line-wrapper.js" +import { stripAnsiColors } from "./util.js" + +describe("ANSI constants", () => { + it("ANSI_ESCAPE matches the ESC control character", () => { + expect(ANSI_ESCAPE).toBe("\x1b") + expect(ANSI_ESCAPE.charCodeAt(0)).toBe(0x1b) + }) + + describe("ANSI_REGEX", () => { + it("matches ANSI color codes", () => { + expect("hello \x1b[31mworld\x1b[0m".match(ANSI_REGEX)).toEqual(["\x1b[31m", "\x1b[0m"]) + }) + + it("matches codes with multiple parameters", () => { + expect("\x1b[38;2;255;0;0m".match(ANSI_REGEX)).toEqual(["\x1b[38;2;255;0;0m"]) + }) + + it("does not match plain text", () => { + ANSI_REGEX.lastIndex = 0 + expect("hello world".match(ANSI_REGEX)).toBeNull() + }) + }) + + describe("ANSI_REGEX_START", () => { + it("matches ANSI code at string start", () => { + expect(ANSI_REGEX_START.test("\x1b[31mhello")).toBe(true) + }) + + it("does not match ANSI code mid-string", () => { + expect(ANSI_REGEX_START.test("hello\x1b[31m")).toBe(false) + }) + }) + + describe("ANSI_REGEX_CAPTURE", () => { + it("splits text preserving ANSI codes", () => { + const parts = `${colors.red}hello${colors.reset}`.split(ANSI_REGEX_CAPTURE) + expect(parts).toEqual(["", colors.red, "hello", colors.reset, ""]) + }) + + it("splits text with multiple colors", () => { + const parts = `${colors.red}a${colors.blue}b${colors.reset}`.split(ANSI_REGEX_CAPTURE) + expect(parts).toEqual(["", colors.red, "a", colors.blue, "b", colors.reset, ""]) + }) + }) +}) + +describe("stripAnsiColors", () => { + it("removes all ANSI codes from text", () => { + const styled = `${colors.red}hello${colors.reset} ${colors.blue}world${colors.reset}` + expect(stripAnsiColors(styled)).toBe("hello world") + }) + + it("returns plain text unchanged", () => { + expect(stripAnsiColors("hello world")).toBe("hello world") + }) + + it("handles empty string", () => { + expect(stripAnsiColors("")).toBe("") + }) + + it("handles text with only ANSI codes", () => { + expect(stripAnsiColors(`${colors.red}${colors.reset}`)).toBe("") + }) + + it("handles RGB color codes", () => { + const styled = "\x1b[38;2;255;0;0mred text\x1b[0m" + expect(stripAnsiColors(styled)).toBe("red text") + }) +}) + +describe("applyDimToStyledText", () => { + let originalNoColor: string | undefined + + beforeEach(() => { + originalNoColor = process.env.NO_COLOR + delete process.env.NO_COLOR + }) + + afterEach(() => { + if (originalNoColor === undefined) { + delete process.env.NO_COLOR + } else { + process.env.NO_COLOR = originalNoColor + } + }) + + it("adds dim modifier to color codes and wraps text segments with dim", () => { + const input = `${colors.red}hello${colors.reset}` + const result = applyDimToStyledText(input) + + expect(result).toBe("\x1b[2;31m\x1b[2mhello\x1b[22m\x1b[0m") + }) + + it("wraps plain text segments with dim", () => { + const input = `${colors.red}colored${colors.reset} plain` + const result = applyDimToStyledText(input) + + expect(result).toBe("\x1b[2;31m\x1b[2mcolored\x1b[22m\x1b[0m\x1b[2m plain\x1b[22m") + }) + + it("returns text unchanged when NO_COLOR is set", () => { + process.env.NO_COLOR = "1" + const input = `${colors.red}hello${colors.reset}` + expect(applyDimToStyledText(input)).toBe(input) + }) + + it("handles text with no ANSI codes", () => { + const result = applyDimToStyledText("plain text") + + expect(result).toBe("\x1b[2mplain text\x1b[22m") + }) + + it("handles empty string", () => { + expect(applyDimToStyledText("")).toBe("") + }) + + it("handles multiple color codes", () => { + const input = `${colors.red}red${colors.reset}${colors.blue}blue${colors.reset}` + const result = applyDimToStyledText(input) + + expect(result).toBe("\x1b[2;31m\x1b[2mred\x1b[22m\x1b[0m\x1b[2;34m\x1b[2mblue\x1b[22m\x1b[0m") + }) +}) + +describe("TextFormatter", () => { + describe("applyDimToStyledText", () => { + let originalNoColor: string | undefined + + beforeEach(() => { + originalNoColor = process.env.NO_COLOR + delete process.env.NO_COLOR + }) + + afterEach(() => { + if (originalNoColor === undefined) { + delete process.env.NO_COLOR + } else { + process.env.NO_COLOR = originalNoColor + } + }) + + it("converts color codes to dim versions", () => { + const input = `${colors.red}hello${colors.reset}` + const result = TextFormatter.applyDimToStyledText(input) + + expect(result).toBe("\x1b[2;31mhello\x1b[0m") + }) + + it("preserves reset codes", () => { + const input = `${colors.red}hello${colors.reset}` + const result = TextFormatter.applyDimToStyledText(input) + + expect(result).toBe("\x1b[2;31mhello\x1b[0m") + }) + + it("returns text unchanged when NO_COLOR is set", () => { + process.env.NO_COLOR = "1" + const input = `${colors.red}hello${colors.reset}` + expect(TextFormatter.applyDimToStyledText(input)).toBe(input) + }) + + it("handles text with no ANSI codes", () => { + expect(TextFormatter.applyDimToStyledText("plain text")).toBe("plain text") + }) + }) + + describe("highlightBackticks", () => { + let originalNoColor: string | undefined + let originalIsTTY: boolean | undefined + + beforeEach(() => { + originalNoColor = process.env.NO_COLOR + originalIsTTY = process.stdout.isTTY + delete process.env.NO_COLOR + process.stdout.isTTY = true + }) + + afterEach(() => { + if (originalNoColor === undefined) { + delete process.env.NO_COLOR + } else { + process.env.NO_COLOR = originalNoColor + } + + process.stdout.isTTY = originalIsTTY! + }) + + it("wraps backtick content with bold white", () => { + const result = TextFormatter.highlightBackticks("use `foo` here") + + expect(result).toBe(`use ${colors.bold}${colors.white}foo${colors.reset} here`) + }) + + it("handles multiple backtick pairs", () => { + const result = TextFormatter.highlightBackticks("`a` and `b`") + + expect(result).toBe(`${colors.bold}${colors.white}a${colors.reset} and ${colors.bold}${colors.white}b${colors.reset}`) + }) + + it("returns text unchanged when NO_COLOR is set", () => { + process.env.NO_COLOR = "1" + expect(TextFormatter.highlightBackticks("use `foo` here")).toBe("use `foo` here") + }) + + it("returns text unchanged without a TTY", () => { + process.stdout.isTTY = false + expect(TextFormatter.highlightBackticks("use `foo` here")).toBe("use `foo` here") + }) + + it("returns text without backticks unchanged", () => { + expect(TextFormatter.highlightBackticks("plain text")).toBe("plain text") + }) + }) +}) + +describe("colorize", () => { + let originalNoColor: string | undefined + + beforeEach(() => { + originalNoColor = process.env.NO_COLOR + delete process.env.NO_COLOR + }) + + afterEach(() => { + if (originalNoColor === undefined) { + delete process.env.NO_COLOR + } else { + process.env.NO_COLOR = originalNoColor + } + }) + + it("wraps text with foreground color and reset", () => { + expect(colorize("hello", "red")).toBe(`${colors.red}hello${colors.reset}`) + }) + + it("supports hex colors", () => { + expect(colorize("hello", "#ff0000")).toBe("\x1b[38;2;255;0;0mhello\x1b[0m") + }) + + it("supports background colors", () => { + expect(colorize("hello", "red", "blue")).toBe(`${colors.blue}${colors.red}hello${colors.reset}`) + }) + + it("returns plain text when NO_COLOR is set", () => { + process.env.NO_COLOR = "1" + expect(colorize("hello", "red")).toBe("hello") + }) +}) + +describe("LineWrapper", () => { + describe("wrapLine", () => { + it("returns line as-is when shorter than maxWidth", () => { + expect(LineWrapper.wrapLine("short", 80)).toEqual(["short"]) + }) + + it("returns line as-is when maxWidth is zero", () => { + expect(LineWrapper.wrapLine("hello", 0)).toEqual(["hello"]) + }) + + it("wraps long plain text at whitespace", () => { + expect(LineWrapper.wrapLine("hello world foo", 11)).toEqual(["hello ", "world foo"]) + }) + + it("preserves ANSI codes when wrapping", () => { + const styled = `${colors.red}hello world${colors.reset} ${colors.blue}and more text${colors.reset}` + const result = LineWrapper.wrapLine(styled, 15) + + expect(result.length).toBeGreaterThan(1) + expect(result[0]).toEqual(`${colors.red}hello world${colors.reset} `) + }) + + it("correctly calculates width ignoring ANSI codes", () => { + const styled = `${colors.red}short${colors.reset}` + expect(LineWrapper.wrapLine(styled, 80)).toEqual([styled]) + }) + }) + + describe("truncateLine", () => { + it("returns line as-is when shorter than maxWidth", () => { + expect(LineWrapper.truncateLine("short", 80)).toBe("short") + }) + + it("returns line as-is when maxWidth is zero", () => { + expect(LineWrapper.truncateLine("hello", 0)).toBe("hello") + }) + + it("truncates long lines with ellipsis", () => { + const longLine = "a".repeat(100) + const result = LineWrapper.truncateLine(longLine, 20) + const stripped = stripAnsiColors(result) + + expect(stripped.length).toBeLessThanOrEqual(20) + expect(stripped).toMatch(/a+…$/) + }) + + it("preserves ANSI codes in truncated output", () => { + const styled = `${colors.red}${"a".repeat(100)}${colors.reset}` + const result = LineWrapper.truncateLine(styled, 20) + + expect(result.startsWith(colors.red)).toBe(true) + expect(stripAnsiColors(result).length).toBeLessThanOrEqual(20) + }) + + it("correctly calculates width ignoring ANSI codes", () => { + const styled = `${colors.red}short${colors.reset}` + expect(LineWrapper.truncateLine(styled, 80)).toBe(styled) + }) + }) + + describe("ANSI-aware extraction via wrapLine", () => { + it("preserves color codes that span a wrap boundary", () => { + const styled = `${colors.red}${"abcdefghij".repeat(3)}${colors.reset}` + const result = LineWrapper.wrapLine(styled, 15) + + expect(result.length).toBeGreaterThan(1) + expect(result[0].startsWith(colors.red)).toBe(true) + }) + + it("handles text with only ANSI codes and no visible content beyond width", () => { + const styled = `${colors.red}${colors.blue}${colors.reset}short` + const result = LineWrapper.wrapLine(styled, 80) + + expect(result).toHaveLength(1) + expect(stripAnsiColors(result[0])).toBe("short") + }) + }) + + describe("ANSI-aware extraction via truncateLine", () => { + it("preserves color in truncated portion", () => { + const styled = `${colors.red}hello${colors.reset} ${colors.blue}${"x".repeat(100)}${colors.reset}` + const result = LineWrapper.truncateLine(styled, 20) + const stripped = stripAnsiColors(result) + + expect(result.startsWith(colors.red)).toBe(true) + expect(stripped).toMatch(/^hello/) + expect(stripped.length).toBeLessThanOrEqual(20) + }) + + it("handles adjacent ANSI codes at truncation point", () => { + const styled = `${"a".repeat(15)}${colors.red}${colors.bold}${"b".repeat(100)}${colors.reset}` + const result = LineWrapper.truncateLine(styled, 20) + const stripped = stripAnsiColors(result) + + expect(stripped.length).toBeLessThanOrEqual(20) + expect(stripped).toMatch(/^a+/) + }) + + it("handles RGB color codes in truncated content", () => { + const styled = `${colorize("x".repeat(100), "#ff8000")}` + const result = LineWrapper.truncateLine(styled, 20) + const stripped = stripAnsiColors(result) + + expect(result.startsWith(ANSI_ESCAPE)).toBe(true) + expect(stripped.length).toBeLessThanOrEqual(20) + }) + }) +}) diff --git a/javascript/packages/highlighter/test/diagnostic-renderer.test.ts b/javascript/packages/highlighter/test/diagnostic-renderer.test.ts index b71f3aa3e..cb2eaef63 100644 --- a/javascript/packages/highlighter/test/diagnostic-renderer.test.ts +++ b/javascript/packages/highlighter/test/diagnostic-renderer.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect, beforeEach } from "vitest" import { themes } from "../src/themes.js" +import { ANSI_REGEX } from "../src/color.js" import { stripAnsiColors } from "./util.js" import { DiagnosticRenderer } from "../src/diagnostic-renderer.js" @@ -46,7 +47,7 @@ describe("DiagnosticRenderer", () => { expect(result).toContain("test-rule") expect(result).toMatch(/\/test\/file\.erb.*2:5/) expect(result).toContain("→") - expect(result).toMatch(/~{5}/) // Error pointer + expect(result).toMatch(/~{5}/) }) it("should render a single warning diagnostic", () => { @@ -80,13 +81,11 @@ describe("DiagnosticRenderer", () => { { contextLines: 1 }, ) - // Should show line 4, 5, 6 (target line 5 with 1 context line each side) expect(result).toMatch(/line.*4/) expect(result).toMatch(/line.*5.*error/) expect(result).toMatch(/line.*6/) - // Check for specific line numbers in the left margin rather than content - expect(result).not.toMatch(/\s+3\s+│/) // Line number 3 should not appear - expect(result).not.toMatch(/\s+7\s+│/) // Line number 7 should not appear + expect(result).not.toMatch(/\s+3\s+│/) + expect(result).not.toMatch(/\s+7\s+│/) }) it("should hide line numbers when requested", () => { @@ -99,7 +98,6 @@ describe("DiagnosticRenderer", () => { { showLineNumbers: false }, ) - // Should not contain line number formatting expect(result).not.toMatch(/\d+\s*│/) expect(result).toContain("Test error message") }) @@ -120,7 +118,6 @@ describe("DiagnosticRenderer", () => { { contextLines: 5 }, ) - // Should handle context lines that exceed file boundaries expect(result).toMatch(/single.*line/) expect(result).toContain("→") }) @@ -153,8 +150,7 @@ describe("DiagnosticRenderer", () => { content, ) - // Should have pointer with correct length - expect(result).toMatch(/~{10}/) // 10 characters + expect(result).toMatch(/~{10}/) }) }) @@ -174,7 +170,6 @@ describe("DiagnosticRenderer", () => { content, ) - // Should not crash and should still show the diagnostic expect(result).toContain("Test error message") expect(result).toContain("test-rule") }) @@ -206,7 +201,7 @@ describe("DiagnosticRenderer", () => { const diagnostic = createDiagnostic({ message: "Class name should be shorter", location: { - start: { line: 1, column: 13 }, // Points to "this-is-a-very-long" + start: { line: 1, column: 13 }, end: { line: 1, column: 33 } }, code: "class-name-length" @@ -224,13 +219,9 @@ describe("DiagnosticRenderer", () => { const strippedResult = stripAnsiColors(result) - // Should contain ellipsis at the end expect(strippedResult).toContain("…") - // Should contain the beginning of the line expect(strippedResult).toContain("this-is-a-very-long") - // Should not contain the end of the long line expect(strippedResult).not.toContain("Content
") - // Should contain the diagnostic message expect(result).toContain("Class name should be shorter") }) @@ -241,7 +232,7 @@ describe("DiagnosticRenderer", () => { message: "Content should be more descriptive", severity: "warning", location: { - start: { line: 1, column: 95 }, // Points to "Content" + start: { line: 1, column: 95 }, end: { line: 1, column: 102 } }, code: "content-description" @@ -259,13 +250,9 @@ describe("DiagnosticRenderer", () => { const strippedResult = stripAnsiColors(result) - // Should contain ellipsis at the beginning expect(strippedResult).toContain("…") - // Should contain the end of the line expect(strippedResult).toContain("Content") - // Should not contain the beginning of the long line expect(strippedResult).not.toContain("this-is-a-very-long") - // Should contain the diagnostic message expect(result).toContain("Content should be more descriptive") }) @@ -275,7 +262,7 @@ describe("DiagnosticRenderer", () => { const diagnostic = createDiagnostic({ message: "Avoid 'should-be' in class names", location: { - start: { line: 1, column: 45 }, // Points to "should-be" in middle + start: { line: 1, column: 45 }, end: { line: 1, column: 54 } }, code: "class-naming-convention" @@ -293,15 +280,11 @@ describe("DiagnosticRenderer", () => { const strippedResult = stripAnsiColors(result) - // Should contain ellipsis on both sides const ellipsisCount = (strippedResult.match(/…/g) || []).length expect(ellipsisCount).toBeGreaterThanOrEqual(2) - // Should contain the middle portion with the diagnostic expect(strippedResult).toContain("should-be") - // Should not contain the very beginning or very end expect(strippedResult).not.toContain("this-is-a-very") expect(strippedResult).not.toContain("Content") - // Should contain the diagnostic message expect(result).toContain("Avoid 'should-be' in class names") }) @@ -311,7 +294,7 @@ describe("DiagnosticRenderer", () => { const diagnostic = createDiagnostic({ message: "Test diagnostic positioning", location: { - start: { line: 1, column: 50 }, // Points to middle of line + start: { line: 1, column: 50 }, end: { line: 1, column: 55 } }, code: "test-positioning" @@ -327,9 +310,7 @@ describe("DiagnosticRenderer", () => { } ) - // Should contain the diagnostic pointer (~) - expect(result).toMatch(/~{5}/) // Should have 5 tildes for the 5-character range - // Should contain ellipsis + expect(result).toMatch(/~{5}/) expect(result).toContain("…") }) @@ -360,12 +341,9 @@ describe("DiagnosticRenderer", () => { const strippedResult = stripAnsiColors(result) - // Should show line 1, 2, 3 (target line 2 with 1 context line each side) expect(strippedResult).toContain("short-line") expect(strippedResult).toContain("another-short-line") - // Line 2 should be truncated expect(strippedResult).toContain("…") - // Should contain the diagnostic message expect(result).toContain("Long class name detected") }) @@ -393,14 +371,86 @@ describe("DiagnosticRenderer", () => { const strippedResult = stripAnsiColors(result) - // Should not contain ellipsis expect(strippedResult).not.toContain("…") - // Should contain the full line expect(strippedResult).toContain("short") expect(strippedResult).toContain("Content") }) }) + describe("ANSI-aware truncation preserves styling", () => { + it("should preserve syntax highlighting colors in truncated output", () => { + const content = `
Content
` + + const diagnostic = createDiagnostic({ + message: "Line too long", + location: { + start: { line: 1, column: 1 }, + end: { line: 1, column: 5 }, + }, + code: "line-length", + }) + + const result = renderer.renderSingle( + "/test/file.erb", + diagnostic, + content, + { truncateLines: true, maxWidth: 50 }, + ) + + expect(result).toMatch(ANSI_REGEX) + const strippedResult = stripAnsiColors(result) + expect(strippedResult).toContain("…") + }) + + it("should preserve colors when extracting from end of styled line", () => { + const content = `
ShortEnd
` + + const diagnostic = createDiagnostic({ + message: "End content issue", + location: { + start: { line: 1, column: 95 }, + end: { line: 1, column: 103 }, + }, + code: "end-content", + }) + + const result = renderer.renderSingle( + "/test/file.erb", + diagnostic, + content, + { truncateLines: true, maxWidth: 50 }, + ) + + const strippedResult = stripAnsiColors(result) + expect(strippedResult).toContain("ShortEnd") + expect(result).toMatch(ANSI_REGEX) + }) + + it("should preserve colors when extracting from middle of styled line", () => { + const content = `
end
` + + const diagnostic = createDiagnostic({ + message: "Middle issue", + location: { + start: { line: 1, column: 30 }, + end: { line: 1, column: 44 }, + }, + code: "middle-content", + }) + + const result = renderer.renderSingle( + "/test/file.erb", + diagnostic, + content, + { truncateLines: true, maxWidth: 50 }, + ) + + const strippedResult = stripAnsiColors(result) + expect(strippedResult).toContain("MIDDLE_TARGET") + expect(result).toMatch(ANSI_REGEX) + }) + }) + describe("NO_COLOR environment", () => { it("should respect NO_COLOR environment variable", () => { const originalNoColor = process.env.NO_COLOR @@ -415,8 +465,7 @@ describe("DiagnosticRenderer", () => { content, ) - // Should not contain ANSI escape codes - expect(result).not.toMatch(/\x1b\\[[0-9;]*m/) + expect(result).not.toMatch(ANSI_REGEX) expect(result).toContain("Test error message") } finally { if (originalNoColor === undefined) { diff --git a/javascript/packages/highlighter/test/highlighter.test.ts b/javascript/packages/highlighter/test/highlighter.test.ts index c1fbd592c..142735754 100644 --- a/javascript/packages/highlighter/test/highlighter.test.ts +++ b/javascript/packages/highlighter/test/highlighter.test.ts @@ -11,6 +11,7 @@ import { } from "vitest" import { Herb } from "@herb-tools/node-wasm" +import { ANSI_REGEX } from "../src/color.js" import { Highlighter, highlightContent, highlightFile } from "../src/highlighter.js" describe("Highlighter", () => { @@ -304,7 +305,7 @@ describe("Standalone utility functions", () => { expect(result).toContain("…") expect(result).not.toContain("maximum-width") - const strippedResult = result.replace(/\x1b\[[0-9;]*m/g, "") + const strippedResult = result.replace(ANSI_REGEX, "") expect(strippedResult).toContain("Short line") }) }) diff --git a/javascript/packages/highlighter/test/syntax-renderer.test.ts b/javascript/packages/highlighter/test/syntax-renderer.test.ts index 99360a3c3..30a2ac045 100644 --- a/javascript/packages/highlighter/test/syntax-renderer.test.ts +++ b/javascript/packages/highlighter/test/syntax-renderer.test.ts @@ -4,9 +4,10 @@ import { themes } from "../src/themes.js" import { Range } from "@herb-tools/core" import { Herb } from "@herb-tools/node-wasm" import { SyntaxRenderer } from "../src/syntax-renderer.js" +import { ANSI_REGEX } from "../src/color.js" function stripAnsiColors(text: string): string { - return text.replace(/\x1b\[[0-9;]*m/g, '') + return text.replace(ANSI_REGEX, '') } describe("SyntaxRenderer", () => { @@ -136,7 +137,7 @@ describe("SyntaxRenderer", () => { const content = "
test
" const result = noColorRenderer.highlight(content) - expect(result).not.toMatch(/\x1b\\[[0-9;]*m/) + expect(result).not.toMatch(ANSI_REGEX) } finally { if (originalNoColor === undefined) { delete process.env.NO_COLOR diff --git a/javascript/packages/highlighter/test/util.ts b/javascript/packages/highlighter/test/util.ts index 9c9ec0547..86a971798 100644 --- a/javascript/packages/highlighter/test/util.ts +++ b/javascript/packages/highlighter/test/util.ts @@ -1,3 +1,5 @@ +import { ANSI_REGEX } from "../src/color.js" + export const stripAnsiColors = (text: string): string => { - return text.replace(/\x1b\[[0-9;]*m/g, "") + return text.replace(ANSI_REGEX, "") } diff --git a/playground/src/controllers/playground_controller.js b/playground/src/controllers/playground_controller.js index 069f218df..87da8f649 100644 --- a/playground/src/controllers/playground_controller.js +++ b/playground/src/controllers/playground_controller.js @@ -206,7 +206,7 @@ export default class extends Controller { this.removePrinterVerificationTooltip() } - handlePopState = async (event) => { + handlePopState = async (_event) => { if (this.urlUpdatedFromChangeEvent === false) { this.editor.setValue(this.decompressedValue) } @@ -262,7 +262,7 @@ export default class extends Controller { button.querySelector(".fa-circle-check").classList.remove("hidden") this.showShareSuccessMessage() - } catch (error) { + } catch (_error) { button.querySelector(".fa-circle-xmark").classList.remove("hidden") this.showShareErrorMessage() } @@ -519,7 +519,7 @@ export default class extends Controller { ) } - selectViewer(event) { + selectViewer(_event) { const button = this.getClosestButton(event.target) const tabName = button.dataset.viewer @@ -527,7 +527,7 @@ export default class extends Controller { this.updateTabInURL(tabName) } - showDiagnostics(event) { + showDiagnostics(_event) { this.setActiveTab('diagnostics') this.updateTabInURL('diagnostics') } @@ -543,7 +543,7 @@ export default class extends Controller { } } - enclargeViewer(event) { + enclargeViewer(_event) { this.currentViewer.style.position = "absolute" this.currentViewer.style.top = `0px` this.currentViewer.style.right = `10px` @@ -555,7 +555,7 @@ export default class extends Controller { this.currentViewer.style.cursor = "zoom-out" } - shrinkViewer(event) { + shrinkViewer(_event) { this.currentViewer.style.position = null this.currentViewer.style.left = null this.currentViewer.style.top = null @@ -758,7 +758,7 @@ export default class extends Controller { } if (this.hasTimeTarget) { - if (result.duration.toFixed(2) == 0.0) { + if (result.duration.toFixed(2) === 0.0) { this.timeTarget.textContent = `(in < 0.00 ms)` } else { this.timeTarget.textContent = `(in ${result.duration.toFixed(2)} ms)` @@ -801,7 +801,6 @@ export default class extends Controller { if (commitInfo.prNumber) { const prUrl = `https://github.com/marcoroth/herb/pull/${commitInfo.prNumber}` - const commitUrl = `https://github.com/marcoroth/herb/commit/${commitInfo.hash}` this.commitHashTarget.textContent = `PR #${commitInfo.prNumber} @ ${commitInfo.hash}` this.commitHashTarget.href = prUrl @@ -1743,7 +1742,7 @@ export default class extends Controller { } } - let containerDiv = this.noDiagnosticsTarget.querySelector('.absolute.inset-0') + const containerDiv = this.noDiagnosticsTarget.querySelector('.absolute.inset-0') if (!containerDiv) { this.noDiagnosticsTarget.innerHTML = `