From 81b72c16b3a98ae9a4a8e36d738a7fa2af4214a6 Mon Sep 17 00:00:00 2001 From: Marc Beinder Date: Thu, 22 Jan 2026 10:29:31 -0600 Subject: [PATCH 1/8] Add Structural Indentation Rule for consistent brace alignment --- src/build-plugins/index.ts | 3 +- src/cli.ts | 152 +++++++++----- src/core/di/ServiceRegistration.ts | 3 +- .../rules/style/StructuralIndentationRule.ts | 185 ++++++++++++++++++ src/core/formatters/rules/style/index.ts | 1 + src/core/pipeline/FormatterPipeline.ts | 3 +- 6 files changed, 297 insertions(+), 50 deletions(-) create mode 100644 src/core/formatters/rules/style/StructuralIndentationRule.ts diff --git a/src/build-plugins/index.ts b/src/build-plugins/index.ts index f6daf44..43e4b1c 100644 --- a/src/build-plugins/index.ts +++ b/src/build-plugins/index.ts @@ -1,5 +1,4 @@ - // Auto-generated exports - do not edit manually // Run tsfmt to regenerate -export * from "./transformGenericsPlugin" +export * from "./transformGenericsPlugin"; diff --git a/src/cli.ts b/src/cli.ts index ce71aac..ea924dc 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -4,14 +4,36 @@ import * as path from "path"; import "reflect-metadata"; import { FormatterPipeline } from "./core"; import { CoreConfig, ConfigLoader } from "./core"; -import { Container, ServiceRegistration } from "./core/di"; +import { Container, ServiceRegistration } from "./core"; import { sortPackageFile } from "./sortPackage"; import { sortTsConfigFile } from "./sortTSConfig"; -/** Format files using the FormatterPipeline */ -async function formatFiles(targetDir: string, config: CoreConfig, dryRun: boolean): Promise { - // Set up dependency injection container +/** Format a single file using the FormatterPipeline */ +async function formatSingleFile(filePath: string, config: CoreConfig, dryRun: boolean): Promise { + const container = new Container(); + ServiceRegistration.registerServices(container, config); + const pipeline = container.resolve("FormatterPipeline"); + + try { + const context = await pipeline.formatFile(filePath, dryRun); + + if (context.changed) { + if (dryRun) { + console.info(`Would format: ${filePath}`); + } else { + console.log(`📊 Formatted: ${filePath}`); + } + } else { + console.info(`No changes needed: ${filePath}`); + } + } catch (error) { + console.error(`Error formatting file ${filePath}:`, (error as Error).message); + } +} + +/** Format files in a directory using the FormatterPipeline */ +async function formatDirectory(targetDir: string, config: CoreConfig, dryRun: boolean): Promise { const container = new Container(); ServiceRegistration.registerServices(container, config); // Get include/exclude patterns @@ -25,7 +47,7 @@ async function formatFiles(targetDir: string, config: CoreConfig, dryRun: boolea cwd: targetDir, ignore: finalExclude, absolute: true, -})); + })); if (files.length === 0) { console.info("No files found to format."); @@ -62,12 +84,17 @@ async function formatFiles(targetDir: string, config: CoreConfig, dryRun: boolea } } +/** Check if a path is a supported file type */ +function isSupportedFile(filePath: string): boolean { + const supportedExtensions = [".ts", ".tsx", ".js", ".jsx"]; + return supportedExtensions.some(ext => filePath.endsWith(ext)); +} + /** Main CLI function */ async function main(): Promise { const args = process.argv.slice(2); // Parse command line arguments - - let targetDir = process.cwd(); + let target = process.cwd(); let dryRun = false; for (let i = 0; i < args.length; i++) { @@ -76,7 +103,7 @@ async function main(): Promise { if (arg === "--dry") { dryRun = true; } else if (!arg.startsWith("-")) { - targetDir = path.resolve(arg); + target = path.resolve(arg); } else { console.error(`Error: Unsupported option "${arg}". Only --dry is supported.`); process.exit(1); @@ -84,56 +111,89 @@ async function main(): Promise { } try { - // Load configuration + // Determine if target is a file or directory + const targetStat = fs.existsSync(target) ? fs.statSync(target) : null; + const isFile = targetStat?.isFile() ?? false; + const isDirectory = targetStat?.isDirectory() ?? false; - const config = ConfigLoader.loadConfig(targetDir); - // Log if custom config is being used + if (!targetStat) { + console.error(`Error: Target "${target}" does not exist.`); + process.exit(1); + } + + // For files, load config from the file's directory; for directories, use the target + const configDir = isFile ? path.dirname(target) : target; + const config = ConfigLoader.loadConfig(configDir); - if (ConfigLoader.hasConfigFile(targetDir)) { + // Log if custom config is being used + if (ConfigLoader.hasConfigFile(configDir)) { console.log("Using custom configuration from tsfmt.config.ts"); } - // Sort package.json - - if (config.packageJson?.enabled) { - const packagePath = path.join(targetDir, "package.json"); - - if (fs.existsSync(packagePath)) { - console.log(`📦 Processing ${packagePath}...`); - sortPackageFile(packagePath, { - customSortOrder: config.packageJson.customSortOrder, - indentation: config.packageJson.indentation, - dryRun, -}); + + // Handle single file formatting + if (isFile) { + if (!isSupportedFile(target)) { + console.error(`Error: Unsupported file type. Supported: .ts, .tsx, .js, .jsx`); + process.exit(1); } - } - // Sort tsconfig.json - if (config.tsConfig?.enabled) { - const tsconfigPath = path.join(targetDir, "tsconfig.json"); + if (config.codeStyle?.enabled || + config.imports?.enabled || + config.sorting?.enabled || + config.spacing?.enabled) { + await formatSingleFile(target, config, dryRun); + } - if (fs.existsSync(tsconfigPath)) { - console.log(`🔧 Processing ${tsconfigPath}...`); - sortTsConfigFile(tsconfigPath, { - indentation: config.tsConfig.indentation, - dryRun, -}); + if (dryRun) { + console.info("Dry run completed. No files were modified."); + } else { + console.info("Formatting completed successfully."); } + return; } - // Format files using the new pipeline - // Check if any formatters are enabled - if (config.codeStyle?.enabled || + // Handle directory formatting + if (isDirectory) { + // Sort package.json + if (config.packageJson?.enabled) { + const packagePath = path.join(target, "package.json"); + + if (fs.existsSync(packagePath)) { + console.log(`📦 Processing ${packagePath}...`); + sortPackageFile(packagePath, { + customSortOrder: config.packageJson.customSortOrder, + indentation: config.packageJson.indentation, + dryRun, + }); + } + } - config.imports?.enabled || - config.sorting?.enabled || - config.spacing?.enabled) { - await formatFiles(targetDir, config, dryRun); - } + // Sort tsconfig.json + if (config.tsConfig?.enabled) { + const tsconfigPath = path.join(target, "tsconfig.json"); - if (dryRun) { - console.info("Dry run completed. No files were modified."); - } else { - console.info("Formatting completed successfully."); + if (fs.existsSync(tsconfigPath)) { + console.log(`🔧 Processing ${tsconfigPath}...`); + sortTsConfigFile(tsconfigPath, { + indentation: config.tsConfig.indentation, + dryRun, + }); + } + } + + // Format files using the pipeline + if (config.codeStyle?.enabled || + config.imports?.enabled || + config.sorting?.enabled || + config.spacing?.enabled) { + await formatDirectory(target, config, dryRun); + } + + if (dryRun) { + console.info("Dry run completed. No files were modified."); + } else { + console.info("Formatting completed successfully."); + } } } catch (error) { console.error("Error during formatting:", (error as Error).message); diff --git a/src/core/di/ServiceRegistration.ts b/src/core/di/ServiceRegistration.ts index c7c613f..3527aa7 100644 --- a/src/core/di/ServiceRegistration.ts +++ b/src/core/di/ServiceRegistration.ts @@ -4,7 +4,7 @@ */ import { CoreConfig } from "../config"; -import { QuoteStyleRule, SemicolonRule, BracketSpacingRule, IndentationRule, BlockSpacingRule, DocBlockCommentRule, ImportOrganizationRule, ClassMemberSortingRule, FileDeclarationSortingRule, BlankLineBetweenDeclarationsRule, BlankLineBetweenStatementTypesRule, BlankLineBeforeReturnsRule, IndexGenerationRule } from "../formatters"; +import { QuoteStyleRule, SemicolonRule, BracketSpacingRule, IndentationRule, StructuralIndentationRule, BlockSpacingRule, DocBlockCommentRule, ImportOrganizationRule, ClassMemberSortingRule, FileDeclarationSortingRule, BlankLineBetweenDeclarationsRule, BlankLineBetweenStatementTypesRule, BlankLineBeforeReturnsRule, IndexGenerationRule } from "../formatters"; import { FormatterPipeline } from "../pipeline"; import { Container } from "./Container"; @@ -20,6 +20,7 @@ export class ServiceRegistration { container.singleton(SemicolonRule); container.singleton(BracketSpacingRule); container.singleton(IndentationRule); + container.singleton(StructuralIndentationRule); container.singleton(BlockSpacingRule); container.singleton(DocBlockCommentRule); container.singleton(ImportOrganizationRule); diff --git a/src/core/formatters/rules/style/StructuralIndentationRule.ts b/src/core/formatters/rules/style/StructuralIndentationRule.ts new file mode 100644 index 0000000..e2a53f3 --- /dev/null +++ b/src/core/formatters/rules/style/StructuralIndentationRule.ts @@ -0,0 +1,185 @@ +/* +* Copyright (c) 2026. Encore Digital Group. +* All Rights Reserved. +*/ + +import * as ts from "typescript"; +import { BaseFormattingRule } from "../../BaseFormattingRule"; + + +/** +* Fixes structural indentation issues where closing braces/brackets +* are not properly aligned with their opening statements. +* +* This rule ensures that closing braces (}, ]), and parentheses ()) +* are indented to match the indentation level of their opening line, +* not pushed to column 0 or other incorrect positions. +*/ +export class StructuralIndentationRule extends BaseFormattingRule { + readonly name = "StructuralIndentationRule"; + + apply(source: string, filePath?: string): string { + const config = this.getCodeStyleConfig(); + if (!config?.indentStyle || !config.indentWidth) { + return source; + } + + const indentWidth = config.indentWidth; + const indentChar = config.indentStyle === "tab" ? "\t" : " "; + const indentUnit = config.indentStyle === "tab" ? "\t" : " ".repeat(indentWidth); + + const sourceFile = ts.createSourceFile( + filePath || "temp.ts", + source, + ts.ScriptTarget.Latest, + true, + this.getScriptKind(filePath) + ); + + const lines = source.split("\n"); + const bracketStack: Array<{ char: string; line: number; indent: number }> = []; + const fixes: Map = new Map(); + + this.analyzeBracketStructure(sourceFile, lines, bracketStack, fixes, indentWidth, indentChar); + + if (fixes.size === 0) { + return source; + } + + const result: string[] = []; + + for (let i = 0; i < lines.length; i++) { + if (fixes.has(i)) { + const targetIndent = fixes.get(i)!; + const trimmedLine = lines[i].trimStart(); + const newIndent = indentUnit.repeat(targetIndent); + result.push(newIndent + trimmedLine); + } else { + result.push(lines[i]); + } + } + + return result.join("\n"); + } + + private getScriptKind(filePath?: string): ts.ScriptKind { + if (!filePath) { + return ts.ScriptKind.TS; + } + + if (filePath.endsWith(".tsx")) { + return ts.ScriptKind.TSX; + } + + if (filePath.endsWith(".jsx")) { + return ts.ScriptKind.JSX; + } + + if (filePath.endsWith(".js")) { + return ts.ScriptKind.JS; + } + + return ts.ScriptKind.TS; + } + + private getLineIndentLevel(line: string, indentWidth: number, indentChar: string): number { + const leadingWhitespace = line.match(/^[\t ]*/)?.[0] || ""; + + if (indentChar === "\t") { + return (leadingWhitespace.match(/\t/g) || []).length; + } + + const tabCount = (leadingWhitespace.match(/\t/g) || []).length; + const spaceCount = (leadingWhitespace.match(/ /g) || []).length; + + return tabCount + Math.floor(spaceCount / indentWidth); + } + + private analyzeBracketStructure( + sourceFile: ts.SourceFile, + lines: string[], + bracketStack: Array<{ char: string; line: number; indent: number }>, + fixes: Map, + indentWidth: number, + indentChar: string + ): void { + const visit = (node: ts.Node): void => { + this.checkNodeBrackets(node, sourceFile, lines, fixes, indentWidth, indentChar); + ts.forEachChild(node, visit); + }; + + visit(sourceFile); + } + + private checkNodeBrackets( + node: ts.Node, + sourceFile: ts.SourceFile, + lines: string[], + fixes: Map, + indentWidth: number, + indentChar: string + ): void { + const nodeStart = node.getStart(sourceFile); + const nodeEnd = node.getEnd(); + const startPos = sourceFile.getLineAndCharacterOfPosition(nodeStart); + const endPos = sourceFile.getLineAndCharacterOfPosition(nodeEnd); + + if (startPos.line === endPos.line) { + return; + } + + const startLine = lines[startPos.line]; + const endLine = lines[endPos.line]; + const startIndent = this.getLineIndentLevel(startLine, indentWidth, indentChar); + const endLineContent = endLine.trimStart(); + + const isClosingBracketLine = /^[}\])]/.test(endLineContent) || + /^[}\])][;,]?\s*$/.test(endLineContent) || + /^[}\])]\s*[;,]?\s*(\/\/.*)?$/.test(endLineContent); + + if (!isClosingBracketLine) { + return; + } + + if (this.isBlockLikeNode(node)) { + const currentEndIndent = this.getLineIndentLevel(endLine, indentWidth, indentChar); + + if (currentEndIndent !== startIndent) { + const existingFix = fixes.get(endPos.line); + + if (existingFix === undefined || startIndent > existingFix) { + fixes.set(endPos.line, startIndent); + } + } + } + } + + private isBlockLikeNode(node: ts.Node): boolean { + return ts.isBlock(node) || + ts.isObjectLiteralExpression(node) || + ts.isArrayLiteralExpression(node) || + ts.isClassDeclaration(node) || + ts.isClassExpression(node) || + ts.isInterfaceDeclaration(node) || + ts.isFunctionDeclaration(node) || + ts.isFunctionExpression(node) || + ts.isArrowFunction(node) || + ts.isMethodDeclaration(node) || + ts.isConstructorDeclaration(node) || + ts.isGetAccessorDeclaration(node) || + ts.isSetAccessorDeclaration(node) || + ts.isModuleDeclaration(node) || + ts.isModuleBlock(node) || + ts.isEnumDeclaration(node) || + ts.isTypeLiteralNode(node) || + ts.isCaseBlock(node) || + ts.isIfStatement(node) || + ts.isForStatement(node) || + ts.isForInStatement(node) || + ts.isForOfStatement(node) || + ts.isWhileStatement(node) || + ts.isDoStatement(node) || + ts.isTryStatement(node) || + ts.isCatchClause(node); + } +} diff --git a/src/core/formatters/rules/style/index.ts b/src/core/formatters/rules/style/index.ts index 9becda1..817dccc 100644 --- a/src/core/formatters/rules/style/index.ts +++ b/src/core/formatters/rules/style/index.ts @@ -5,3 +5,4 @@ export * from "./DocBlockCommentRule"; export * from "./IndentationRule"; export * from "./QuoteStyleRule"; export * from "./SemicolonRule"; +export * from "./StructuralIndentationRule"; diff --git a/src/core/pipeline/FormatterPipeline.ts b/src/core/pipeline/FormatterPipeline.ts index cff1b5c..c260335 100644 --- a/src/core/pipeline/FormatterPipeline.ts +++ b/src/core/pipeline/FormatterPipeline.ts @@ -7,7 +7,7 @@ import * as fs from "fs/promises"; import * as path from "path"; import { CoreConfig, FormatterOrder } from "../config"; import { Container } from "../di"; -import { BlankLineBeforeReturnsRule, BlankLineBetweenDeclarationsRule, BlankLineBetweenStatementTypesRule, BlockSpacingRule, BracketSpacingRule, ClassMemberSortingRule, DocBlockCommentRule, FileDeclarationSortingRule, IFormattingRule, ImportOrganizationRule, IndentationRule, IndexGenerationRule, QuoteStyleRule, SemicolonRule } from "../formatters"; +import { BlankLineBeforeReturnsRule, BlankLineBetweenDeclarationsRule, BlankLineBetweenStatementTypesRule, BlockSpacingRule, BracketSpacingRule, ClassMemberSortingRule, DocBlockCommentRule, FileDeclarationSortingRule, IFormattingRule, ImportOrganizationRule, IndentationRule, IndexGenerationRule, QuoteStyleRule, SemicolonRule, StructuralIndentationRule } from "../formatters"; /* @@ -296,6 +296,7 @@ export class FormatterPipeline { this.addRule(FormatterOrder.CodeStyle); this.addRule(FormatterOrder.CodeStyle); this.addRule(FormatterOrder.CodeStyle); + this.addRule(FormatterOrder.CodeStyle); this.addRule(FormatterOrder.CodeStyle); this.addRule(FormatterOrder.CodeStyle); } From 3d828d92f1ff1946ecc8fee7efd02663f5c3d57b Mon Sep 17 00:00:00 2001 From: Marc Beinder Date: Thu, 22 Jan 2026 10:30:28 -0600 Subject: [PATCH 2/8] Fix missing newlines at the end of files and ensure consistent brace alignment --- src/build-plugins/index.ts | 3 +- src/build-plugins/transformGenericsPlugin.ts | 2 +- src/cli.ts | 46 ++-- src/core/ast/ASTTransformer.ts | 4 +- src/core/ast/DependencyResolver.ts | 2 +- src/core/config/ConfigDefaults.ts | 28 +-- src/core/config/ConfigLoader.ts | 6 +- src/core/config/ConfigMerger.ts | 4 +- src/core/config/ConfigValidator.ts | 2 +- .../config/__tests__/ConfigMerger.test.ts | 30 +-- .../config/__tests__/ConfigValidator.test.ts | 32 +-- .../rules/ast/ClassMemberSortingRule.ts | 2 +- .../rules/ast/FileDeclarationSortingRule.ts | 6 +- .../rules/imports/ImportOrganizationRule.ts | 4 +- .../index-generation/IndexGenerationRule.ts | 2 +- .../BlankLineBetweenDeclarationsRule.ts | 2 +- .../formatters/rules/style/QuoteStyleRule.ts | 2 +- .../rules/style/StructuralIndentationRule.ts | 224 +++++++++--------- .../__tests__/DocBlockCommentRule.test.ts | 8 +- src/core/pipeline/FormatterPipeline.ts | 4 +- .../__tests__/FormatterPipeline.test.ts | 28 +-- src/sortPackage.ts | 2 +- 22 files changed, 222 insertions(+), 221 deletions(-) diff --git a/src/build-plugins/index.ts b/src/build-plugins/index.ts index 43e4b1c..f6daf44 100644 --- a/src/build-plugins/index.ts +++ b/src/build-plugins/index.ts @@ -1,4 +1,5 @@ + // Auto-generated exports - do not edit manually // Run tsfmt to regenerate -export * from "./transformGenericsPlugin"; +export * from "./transformGenericsPlugin" diff --git a/src/build-plugins/transformGenericsPlugin.ts b/src/build-plugins/transformGenericsPlugin.ts index e44bafd..347e954 100644 --- a/src/build-plugins/transformGenericsPlugin.ts +++ b/src/build-plugins/transformGenericsPlugin.ts @@ -116,5 +116,5 @@ export function transformGenericsPlugin() { } } } -}; + }; } \ No newline at end of file diff --git a/src/cli.ts b/src/cli.ts index ea924dc..c220c6a 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -9,29 +9,6 @@ import { sortPackageFile } from "./sortPackage"; import { sortTsConfigFile } from "./sortTSConfig"; -/** Format a single file using the FormatterPipeline */ -async function formatSingleFile(filePath: string, config: CoreConfig, dryRun: boolean): Promise { - const container = new Container(); - ServiceRegistration.registerServices(container, config); - const pipeline = container.resolve("FormatterPipeline"); - - try { - const context = await pipeline.formatFile(filePath, dryRun); - - if (context.changed) { - if (dryRun) { - console.info(`Would format: ${filePath}`); - } else { - console.log(`📊 Formatted: ${filePath}`); - } - } else { - console.info(`No changes needed: ${filePath}`); - } - } catch (error) { - console.error(`Error formatting file ${filePath}:`, (error as Error).message); - } -} - /** Format files in a directory using the FormatterPipeline */ async function formatDirectory(targetDir: string, config: CoreConfig, dryRun: boolean): Promise { const container = new Container(); @@ -84,6 +61,29 @@ async function formatDirectory(targetDir: string, config: CoreConfig, dryRun: bo } } +/** Format a single file using the FormatterPipeline */ +async function formatSingleFile(filePath: string, config: CoreConfig, dryRun: boolean): Promise { + const container = new Container(); + ServiceRegistration.registerServices(container, config); + const pipeline = container.resolve("FormatterPipeline"); + + try { + const context = await pipeline.formatFile(filePath, dryRun); + + if (context.changed) { + if (dryRun) { + console.info(`Would format: ${filePath}`); + } else { + console.log(`📊 Formatted: ${filePath}`); + } + } else { + console.info(`No changes needed: ${filePath}`); + } + } catch (error) { + console.error(`Error formatting file ${filePath}:`, (error as Error).message); + } +} + /** Check if a path is a supported file type */ function isSupportedFile(filePath: string): boolean { const supportedExtensions = [".ts", ".tsx", ".js", ".jsx"]; diff --git a/src/core/ast/ASTTransformer.ts b/src/core/ast/ASTTransformer.ts index 63d53ff..da8907e 100644 --- a/src/core/ast/ASTTransformer.ts +++ b/src/core/ast/ASTTransformer.ts @@ -24,7 +24,7 @@ export class ASTTransformer { const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed, removeComments, -}); + }); return printer.printNode(ts.EmitHint.Unspecified, node, sourceFile); } @@ -53,7 +53,7 @@ export class ASTTransformer { const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed, removeComments: false, -}); + }); // Print the file content diff --git a/src/core/ast/DependencyResolver.ts b/src/core/ast/DependencyResolver.ts index 6860f6d..fd8a2a5 100644 --- a/src/core/ast/DependencyResolver.ts +++ b/src/core/ast/DependencyResolver.ts @@ -99,7 +99,7 @@ export class DependencyResolver { dependencies: getDependencies(item), originalIndex: index, sortedIndex: index, -}); + }); }); // Filter dependencies to only include symbols in our scope diff --git a/src/core/config/ConfigDefaults.ts b/src/core/config/ConfigDefaults.ts index f23d0f6..aaaa2d0 100644 --- a/src/core/config/ConfigDefaults.ts +++ b/src/core/config/ConfigDefaults.ts @@ -32,7 +32,7 @@ export class ConfigDefaults { lineWidth: 120, trailingCommas: "all" as const, arrowParens: "avoid" as const, -}; + }; } /** Get default directories for index generation */ @@ -49,9 +49,9 @@ export class ConfigDefaults { fileExtension: ".ts", indexFileName: "index.ts", recursive: true -}, + }, updateMainIndex: true, -}; + }; } /** Get default import configuration */ @@ -64,7 +64,7 @@ export class ConfigDefaults { groupImports: true, groupOrder: ConfigTypes.getImportGroupOptions(), separateGroups: false, -}; + }; } /** Get default include patterns for TypeScript files */ @@ -86,21 +86,21 @@ export class ConfigDefaults { order: DEFAULT_CLASS_ORDER, groupByVisibility: false, respectDependencies: true, -}, + }, reactComponents: { enabled: true, order: DEFAULT_CLASS_ORDER, groupByVisibility: false, respectDependencies: true, -}, + }, fileDeclarations: { enabled: true, order: DEFAULT_FILE_ORDER, respectDependencies: true, -}, + }, include: this.getDefaultIncludePatterns(), exclude: this.getDefaultExcludePatterns(), -}; + }; } /** Get default spacing configuration */ @@ -110,7 +110,7 @@ export class ConfigDefaults { betweenDeclarations: true, beforeReturns: true, betweenStatementTypes: true, -}; + }; } /** Get default package.json configuration */ @@ -119,7 +119,7 @@ export class ConfigDefaults { enabled: true, customSortOrder: DefaultSortOptions.customSortOrder, indentation: 4, -}; + }; } /** Get default tsconfig.json configuration */ @@ -127,7 +127,7 @@ export class ConfigDefaults { return { enabled: true, indentation: 4, -}; + }; } /** Get default formatter order */ @@ -152,7 +152,7 @@ export class ConfigDefaults { packageJson: this.getDefaultPackageJsonConfig(), tsConfig: this.getDefaultTsConfigConfig(), formatterOrder: this.getDefaultFormatterOrder(), -}; + }; } /** Get default include patterns for JavaScript files */ @@ -170,7 +170,7 @@ export class ConfigDefaults { spacing: {enabled: false}, packageJson: {enabled: false}, tsConfig: {enabled: false}, -}; + }; } /** Create a minimal configuration with only enabled features */ @@ -183,6 +183,6 @@ export class ConfigDefaults { spacing: {enabled: false}, packageJson: {enabled: true}, tsConfig: {enabled: true}, -}; + }; } } \ No newline at end of file diff --git a/src/core/config/ConfigLoader.ts b/src/core/config/ConfigLoader.ts index eca063c..2ab943f 100644 --- a/src/core/config/ConfigLoader.ts +++ b/src/core/config/ConfigLoader.ts @@ -114,7 +114,7 @@ export default config; return { size: this.configCache.size, keys: Array.from(this.configCache.keys()) -}; + }; } /** @@ -152,8 +152,8 @@ export default config; target: ts.ScriptTarget.ES2015, esModuleInterop: true, allowSyntheticDefaultImports: true, -}, -}); + }, + }); return result.outputText; } diff --git a/src/core/config/ConfigMerger.ts b/src/core/config/ConfigMerger.ts index a10134f..abf0c87 100644 --- a/src/core/config/ConfigMerger.ts +++ b/src/core/config/ConfigMerger.ts @@ -28,9 +28,9 @@ export class ConfigMerger { result[key] !== null && !Array.isArray(result[key])) { result[key] = this.deepMerge(result[key] as any, source[key] as any); - } else { + } else { result[key] = source[key] as T[Extract]; - } + } } } diff --git a/src/core/config/ConfigValidator.ts b/src/core/config/ConfigValidator.ts index 5c29cf7..34bfc6c 100644 --- a/src/core/config/ConfigValidator.ts +++ b/src/core/config/ConfigValidator.ts @@ -75,7 +75,7 @@ export class ConfigValidator { valid: errors.length === 0, errors, warnings, -}; + }; } /** diff --git a/src/core/config/__tests__/ConfigMerger.test.ts b/src/core/config/__tests__/ConfigMerger.test.ts index 9f38ee2..9110dac 100644 --- a/src/core/config/__tests__/ConfigMerger.test.ts +++ b/src/core/config/__tests__/ConfigMerger.test.ts @@ -13,8 +13,8 @@ describe("ConfigMerger", () => { const userConfig: Partial = { codeStyle: { quoteStyle: "single", -}, -}; + }, + }; const result = ConfigMerger.merge(userConfig); @@ -27,9 +27,9 @@ describe("ConfigMerger", () => { sorting: { classMembers: { groupByVisibility: true, -}, -}, -}; + }, + }, + }; const result = ConfigMerger.merge(userConfig); @@ -41,8 +41,8 @@ describe("ConfigMerger", () => { const userConfig: Partial = { imports: { groupOrder: ["relative", "external"], -}, -}; + }, + }; const result = ConfigMerger.merge(userConfig); @@ -53,8 +53,8 @@ describe("ConfigMerger", () => { const userConfig: Partial = { codeStyle: { quoteStyle: undefined, -}, -}; + }, + }; const result = ConfigMerger.merge(userConfig); @@ -66,20 +66,20 @@ describe("ConfigMerger", () => { const config1: Partial = { codeStyle: { quoteStyle: "single", -}, -}; + }, + }; const config2: Partial = { codeStyle: { semicolons: "never", -}, -}; + }, + }; const config3: Partial = { codeStyle: { quoteStyle: "double", // Override config1 -}, -}; + }, + }; const result = ConfigMerger.mergeMultiple(config1, config2, config3); diff --git a/src/core/config/__tests__/ConfigValidator.test.ts b/src/core/config/__tests__/ConfigValidator.test.ts index fe975ac..5af9868 100644 --- a/src/core/config/__tests__/ConfigValidator.test.ts +++ b/src/core/config/__tests__/ConfigValidator.test.ts @@ -17,8 +17,8 @@ describe("ConfigValidator", () => { semicolons: "always", indentWidth: 4, lineWidth: 120, -}, -}; + }, + }; const result = ConfigValidator.validate(config); @@ -30,8 +30,8 @@ describe("ConfigValidator", () => { codeStyle: { enabled: true, quoteStyle: "triple" as any, -}, -}; + }, + }; const result = ConfigValidator.validate(config); @@ -43,8 +43,8 @@ describe("ConfigValidator", () => { codeStyle: { enabled: true, semicolons: "sometimes" as any, -}, -}; + }, + }; const result = ConfigValidator.validate(config); @@ -56,8 +56,8 @@ describe("ConfigValidator", () => { codeStyle: { enabled: true, indentWidth: 10, -}, -}; + }, + }; const result = ConfigValidator.validate(config); @@ -69,8 +69,8 @@ describe("ConfigValidator", () => { codeStyle: { enabled: true, lineWidth: 250, -}, -}; + }, + }; const result = ConfigValidator.validate(config); @@ -82,8 +82,8 @@ describe("ConfigValidator", () => { imports: { enabled: true, groupOrder: ["external", "invalid", "relative"], -}, -}; + }, + }; const result = ConfigValidator.validate(config); @@ -97,8 +97,8 @@ describe("ConfigValidator", () => { codeStyle: { enabled: true, quoteStyle: "double", -}, -}; + }, + }; expect(() => ConfigValidator.validateOrThrow(config)).not.toThrow(); }); it("should throw for invalid config", () => { @@ -106,8 +106,8 @@ describe("ConfigValidator", () => { codeStyle: { enabled: true, quoteStyle: "invalid" as any, -}, -}; + }, + }; expect(() => ConfigValidator.validateOrThrow(config)).toThrow("Invalid configuration"); }); }); diff --git a/src/core/formatters/rules/ast/ClassMemberSortingRule.ts b/src/core/formatters/rules/ast/ClassMemberSortingRule.ts index 691c38e..a05d9a4 100644 --- a/src/core/formatters/rules/ast/ClassMemberSortingRule.ts +++ b/src/core/formatters/rules/ast/ClassMemberSortingRule.ts @@ -116,7 +116,7 @@ export class ClassMemberSortingRule extends BaseFormattingRule { text, dependencies, originalIndex: index, -}; + }; } private createSourceFile(source: string, filePath: string): ts.SourceFile { diff --git a/src/core/formatters/rules/ast/FileDeclarationSortingRule.ts b/src/core/formatters/rules/ast/FileDeclarationSortingRule.ts index e1c0a37..9b1e617 100644 --- a/src/core/formatters/rules/ast/FileDeclarationSortingRule.ts +++ b/src/core/formatters/rules/ast/FileDeclarationSortingRule.ts @@ -4,8 +4,8 @@ */ import * as ts from "typescript"; -import { ASTAnalyzer } from "../../../ast/ASTAnalyzer"; -import { DependencyResolver } from "../../../ast/DependencyResolver"; +import { ASTAnalyzer } from "../../../ast"; +import { DependencyResolver } from "../../../ast"; import { BaseFormattingRule } from "../../BaseFormattingRule"; @@ -119,7 +119,7 @@ export class FileDeclarationSortingRule extends BaseFormattingRule { text, dependencies, originalIndex: index, -}; + }; } private createSourceFile(source: string, filePath: string): ts.SourceFile { diff --git a/src/core/formatters/rules/imports/ImportOrganizationRule.ts b/src/core/formatters/rules/imports/ImportOrganizationRule.ts index c6f6fb9..e8ad470 100644 --- a/src/core/formatters/rules/imports/ImportOrganizationRule.ts +++ b/src/core/formatters/rules/imports/ImportOrganizationRule.ts @@ -54,7 +54,7 @@ export class ImportOrganizationRule extends BaseFormattingRule { isTypeOnly, isSideEffect, group, -}); + }); } } @@ -211,7 +211,7 @@ export class ImportOrganizationRule extends BaseFormattingRule { const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed, removeComments: false, -}); + }); const importLines: string[] = []; diff --git a/src/core/formatters/rules/index-generation/IndexGenerationRule.ts b/src/core/formatters/rules/index-generation/IndexGenerationRule.ts index 82ef7b9..eb02a11 100644 --- a/src/core/formatters/rules/index-generation/IndexGenerationRule.ts +++ b/src/core/formatters/rules/index-generation/IndexGenerationRule.ts @@ -42,7 +42,7 @@ export class IndexGenerationRule extends BaseFormattingRule { fileExtension: ".ts", indexFileName: "index.ts", recursive: true -}; + }; readonly name = "IndexGenerationRule"; diff --git a/src/core/formatters/rules/spacing/BlankLineBetweenDeclarationsRule.ts b/src/core/formatters/rules/spacing/BlankLineBetweenDeclarationsRule.ts index 84a3c23..7fa0d88 100644 --- a/src/core/formatters/rules/spacing/BlankLineBetweenDeclarationsRule.ts +++ b/src/core/formatters/rules/spacing/BlankLineBetweenDeclarationsRule.ts @@ -112,7 +112,7 @@ export class BlankLineBetweenDeclarationsRule extends BaseFormattingRule { result[result.length - 1].trim() !== "") { result.push(""); lastNonBlankLineWasDeclarationEnd = false; - } + } // Add blank line before declaration starts ONLY if the keyword is different else if (isDeclarationStart && diff --git a/src/core/formatters/rules/style/QuoteStyleRule.ts b/src/core/formatters/rules/style/QuoteStyleRule.ts index 5407137..7d3c988 100644 --- a/src/core/formatters/rules/style/QuoteStyleRule.ts +++ b/src/core/formatters/rules/style/QuoteStyleRule.ts @@ -50,7 +50,7 @@ export class QuoteStyleRule extends BaseFormattingRule { start: node.getStart(sourceFile), end: node.getEnd(), text: newText, -}); + }); } } } diff --git a/src/core/formatters/rules/style/StructuralIndentationRule.ts b/src/core/formatters/rules/style/StructuralIndentationRule.ts index e2a53f3..1e0112b 100644 --- a/src/core/formatters/rules/style/StructuralIndentationRule.ts +++ b/src/core/formatters/rules/style/StructuralIndentationRule.ts @@ -16,73 +16,9 @@ import { BaseFormattingRule } from "../../BaseFormattingRule"; * not pushed to column 0 or other incorrect positions. */ export class StructuralIndentationRule extends BaseFormattingRule { - readonly name = "StructuralIndentationRule"; +readonly name = "StructuralIndentationRule"; - apply(source: string, filePath?: string): string { - const config = this.getCodeStyleConfig(); - if (!config?.indentStyle || !config.indentWidth) { - return source; - } - - const indentWidth = config.indentWidth; - const indentChar = config.indentStyle === "tab" ? "\t" : " "; - const indentUnit = config.indentStyle === "tab" ? "\t" : " ".repeat(indentWidth); - - const sourceFile = ts.createSourceFile( - filePath || "temp.ts", - source, - ts.ScriptTarget.Latest, - true, - this.getScriptKind(filePath) - ); - - const lines = source.split("\n"); - const bracketStack: Array<{ char: string; line: number; indent: number }> = []; - const fixes: Map = new Map(); - - this.analyzeBracketStructure(sourceFile, lines, bracketStack, fixes, indentWidth, indentChar); - - if (fixes.size === 0) { - return source; - } - - const result: string[] = []; - - for (let i = 0; i < lines.length; i++) { - if (fixes.has(i)) { - const targetIndent = fixes.get(i)!; - const trimmedLine = lines[i].trimStart(); - const newIndent = indentUnit.repeat(targetIndent); - result.push(newIndent + trimmedLine); - } else { - result.push(lines[i]); - } - } - - return result.join("\n"); - } - - private getScriptKind(filePath?: string): ts.ScriptKind { - if (!filePath) { - return ts.ScriptKind.TS; - } - - if (filePath.endsWith(".tsx")) { - return ts.ScriptKind.TSX; - } - - if (filePath.endsWith(".jsx")) { - return ts.ScriptKind.JSX; - } - - if (filePath.endsWith(".js")) { - return ts.ScriptKind.JS; - } - - return ts.ScriptKind.TS; - } - - private getLineIndentLevel(line: string, indentWidth: number, indentChar: string): number { +private getLineIndentLevel(line: string, indentWidth: number, indentChar: string): number { const leadingWhitespace = line.match(/^[\t ]*/)?.[0] || ""; if (indentChar === "\t") { @@ -93,25 +29,38 @@ export class StructuralIndentationRule extends BaseFormattingRule { const spaceCount = (leadingWhitespace.match(/ /g) || []).length; return tabCount + Math.floor(spaceCount / indentWidth); - } - - private analyzeBracketStructure( - sourceFile: ts.SourceFile, - lines: string[], - bracketStack: Array<{ char: string; line: number; indent: number }>, - fixes: Map, - indentWidth: number, - indentChar: string - ): void { - const visit = (node: ts.Node): void => { - this.checkNodeBrackets(node, sourceFile, lines, fixes, indentWidth, indentChar); - ts.forEachChild(node, visit); - }; +} - visit(sourceFile); - } +private isBlockLikeNode(node: ts.Node): boolean { + return ts.isBlock(node) || + ts.isObjectLiteralExpression(node) || + ts.isArrayLiteralExpression(node) || + ts.isClassDeclaration(node) || + ts.isClassExpression(node) || + ts.isInterfaceDeclaration(node) || + ts.isFunctionDeclaration(node) || + ts.isFunctionExpression(node) || + ts.isArrowFunction(node) || + ts.isMethodDeclaration(node) || + ts.isConstructorDeclaration(node) || + ts.isGetAccessorDeclaration(node) || + ts.isSetAccessorDeclaration(node) || + ts.isModuleDeclaration(node) || + ts.isModuleBlock(node) || + ts.isEnumDeclaration(node) || + ts.isTypeLiteralNode(node) || + ts.isCaseBlock(node) || + ts.isIfStatement(node) || + ts.isForStatement(node) || + ts.isForInStatement(node) || + ts.isForOfStatement(node) || + ts.isWhileStatement(node) || + ts.isDoStatement(node) || + ts.isTryStatement(node) || + ts.isCatchClause(node); +} - private checkNodeBrackets( +private checkNodeBrackets( node: ts.Node, sourceFile: ts.SourceFile, lines: string[], @@ -152,34 +101,85 @@ export class StructuralIndentationRule extends BaseFormattingRule { } } } - } +} + +private analyzeBracketStructure( + sourceFile: ts.SourceFile, + lines: string[], + bracketStack: Array<{ char: string; line: number; indent: number }>, + fixes: Map, + indentWidth: number, + indentChar: string + ): void { + const visit = (node: ts.Node): void => { + this.checkNodeBrackets(node, sourceFile, lines, fixes, indentWidth, indentChar); + ts.forEachChild(node, visit); + }; + + visit(sourceFile); +} + +private getScriptKind(filePath?: string): ts.ScriptKind { + if (!filePath) { + return ts.ScriptKind.TS; + } + + if (filePath.endsWith(".tsx")) { + return ts.ScriptKind.TSX; + } + + if (filePath.endsWith(".jsx")) { + return ts.ScriptKind.JSX; + } + + if (filePath.endsWith(".js")) { + return ts.ScriptKind.JS; + } + + return ts.ScriptKind.TS; +} + +apply(source: string, filePath?: string): string { + const config = this.getCodeStyleConfig(); + if (!config?.indentStyle || !config.indentWidth) { + return source; + } + + const indentWidth = config.indentWidth; + const indentChar = config.indentStyle === "tab" ? "\t" : " "; + const indentUnit = config.indentStyle === "tab" ? "\t" : " ".repeat(indentWidth); + + const sourceFile = ts.createSourceFile( + filePath || "temp.ts", + source, + ts.ScriptTarget.Latest, + true, + this.getScriptKind(filePath) + ); + + const lines = source.split("\n"); + const bracketStack: Array<{ char: string; line: number; indent: number }> = []; + const fixes: Map = new Map(); + + this.analyzeBracketStructure(sourceFile, lines, bracketStack, fixes, indentWidth, indentChar); + + if (fixes.size === 0) { + return source; + } + + const result: string[] = []; + + for (let i = 0; i < lines.length; i++) { + if (fixes.has(i)) { + const targetIndent = fixes.get(i)!; + const trimmedLine = lines[i].trimStart(); + const newIndent = indentUnit.repeat(targetIndent); + result.push(newIndent + trimmedLine); + } else { + result.push(lines[i]); + } + } - private isBlockLikeNode(node: ts.Node): boolean { - return ts.isBlock(node) || - ts.isObjectLiteralExpression(node) || - ts.isArrayLiteralExpression(node) || - ts.isClassDeclaration(node) || - ts.isClassExpression(node) || - ts.isInterfaceDeclaration(node) || - ts.isFunctionDeclaration(node) || - ts.isFunctionExpression(node) || - ts.isArrowFunction(node) || - ts.isMethodDeclaration(node) || - ts.isConstructorDeclaration(node) || - ts.isGetAccessorDeclaration(node) || - ts.isSetAccessorDeclaration(node) || - ts.isModuleDeclaration(node) || - ts.isModuleBlock(node) || - ts.isEnumDeclaration(node) || - ts.isTypeLiteralNode(node) || - ts.isCaseBlock(node) || - ts.isIfStatement(node) || - ts.isForStatement(node) || - ts.isForInStatement(node) || - ts.isForOfStatement(node) || - ts.isWhileStatement(node) || - ts.isDoStatement(node) || - ts.isTryStatement(node) || - ts.isCatchClause(node); - } + return result.join("\n"); +} } diff --git a/src/core/formatters/rules/style/__tests__/DocBlockCommentRule.test.ts b/src/core/formatters/rules/style/__tests__/DocBlockCommentRule.test.ts index 7caf882..371469e 100644 --- a/src/core/formatters/rules/style/__tests__/DocBlockCommentRule.test.ts +++ b/src/core/formatters/rules/style/__tests__/DocBlockCommentRule.test.ts @@ -24,13 +24,13 @@ describe("DocBlockCommentRule", () => { indentation: { type: "spaces", size: 4 -}, + }, blockSpacing: true, docBlockComments: { consolidateSingleLine: true -} -} -} as CoreConfig; + } + } + } as CoreConfig; container.singleton(config); rule = new DocBlockCommentRule(container); }); diff --git a/src/core/pipeline/FormatterPipeline.ts b/src/core/pipeline/FormatterPipeline.ts index c260335..7644da5 100644 --- a/src/core/pipeline/FormatterPipeline.ts +++ b/src/core/pipeline/FormatterPipeline.ts @@ -184,7 +184,7 @@ export class FormatterPipeline { executions: [], changed: false, dryRun, -}; + }; // Execute rules in order @@ -200,7 +200,7 @@ export class FormatterPipeline { formatterName: rule.name, order, changed: false, -}; + }; try { // Execute rule diff --git a/src/core/pipeline/__tests__/FormatterPipeline.test.ts b/src/core/pipeline/__tests__/FormatterPipeline.test.ts index 2618842..b6f6273 100644 --- a/src/core/pipeline/__tests__/FormatterPipeline.test.ts +++ b/src/core/pipeline/__tests__/FormatterPipeline.test.ts @@ -30,7 +30,7 @@ describe("FormatterPipeline", () => { ...ConfigDefaults.getDefaultConfig(), codeStyle: {enabled: true, quoteStyle: "double"}, -}; + }; const container = new Container(); ServiceRegistration.registerServices(container, config); @@ -50,7 +50,7 @@ describe("FormatterPipeline", () => { ...ConfigDefaults.getDefaultConfig(), formatterOrder: [FormatterOrder.Spacing, FormatterOrder.CodeStyle], -}; + }; const container = new Container(); ServiceRegistration.registerServices(container, config); @@ -68,7 +68,7 @@ describe("FormatterPipeline", () => { sorting: {enabled: false}, imports: {enabled: false}, spacing: {enabled: false}, -}; + }; const container = new Container(); ServiceRegistration.registerServices(container, config); @@ -87,7 +87,7 @@ describe("FormatterPipeline", () => { sorting: {enabled: false}, codeStyle: {enabled: false}, spacing: {enabled: false}, -}; + }; const container = new Container(); ServiceRegistration.registerServices(container, config); @@ -106,7 +106,7 @@ describe("FormatterPipeline", () => { imports: {enabled: false}, sorting: {enabled: false}, spacing: {enabled: false}, -}; + }; const container = new Container(); ServiceRegistration.registerServices(container, config); @@ -129,7 +129,7 @@ describe("FormatterPipeline", () => { imports: {enabled: false}, // Disable imports to test only code style sorting: {enabled: false}, // Disable sorting for this test spacing: {enabled: false}, // Disable spacing for this test -}; + }; const container = new Container(); ServiceRegistration.registerServices(container, config); @@ -161,7 +161,7 @@ describe("FormatterPipeline", () => { sorting: {enabled: false}, // Disable sorting for this test imports: {enabled: false}, // Disable imports for this test spacing: {enabled: false}, // Disable spacing for this test -}; + }; const container = new Container(); ServiceRegistration.registerServices(container, config); @@ -189,7 +189,7 @@ describe("FormatterPipeline", () => { imports: {enabled: true, sortImports: true}, sorting: {enabled: false}, // Disable sorting for this test spacing: {enabled: false}, // Disable spacing for this test -}; + }; const container = new Container(); ServiceRegistration.registerServices(container, config); @@ -217,7 +217,7 @@ describe("FormatterPipeline", () => { sorting: {enabled: false}, // Disable sorting for this test imports: {enabled: false}, // Disable imports for this test spacing: {enabled: false}, // Disable spacing for this test -}; + }; const container = new Container(); ServiceRegistration.registerServices(container, config); @@ -245,7 +245,7 @@ describe("FormatterPipeline", () => { sorting: {enabled: false}, // Disable sorting for this test imports: {enabled: false}, // Disable imports for this test spacing: {enabled: false}, // Disable spacing for this test -}; + }; const container = new Container(); ServiceRegistration.registerServices(container, config); @@ -272,7 +272,7 @@ describe("FormatterPipeline", () => { sorting: {enabled: false}, // Disable sorting for this test imports: {enabled: false}, // Disable imports for this test spacing: {enabled: false}, // Disable spacing for this test -}; + }; const container = new Container(); ServiceRegistration.registerServices(container, config); @@ -304,7 +304,7 @@ describe("FormatterPipeline", () => { sorting: {enabled: false}, // Disable sorting for this test imports: {enabled: false}, // Disable imports for this test spacing: {enabled: false}, // Disable spacing for this test -}; + }; const container = new Container(); ServiceRegistration.registerServices(container, config); @@ -325,7 +325,7 @@ describe("FormatterPipeline", () => { indexGeneration: {enabled: false}, codeStyle: {enabled: true, quoteStyle: "double"}, spacing: {enabled: false}, -}; + }; const container = new Container(); ServiceRegistration.registerServices(container, config); @@ -346,7 +346,7 @@ describe("FormatterPipeline", () => { sorting: {enabled: false}, // Disable sorting for this test imports: {enabled: false}, // Disable imports for this test spacing: {enabled: false}, // Disable spacing for this test -}; + }; const container = new Container(); ServiceRegistration.registerServices(container, config); diff --git a/src/sortPackage.ts b/src/sortPackage.ts index 734932a..6346716 100644 --- a/src/sortPackage.ts +++ b/src/sortPackage.ts @@ -16,7 +16,7 @@ export function sortPackageJson(packageObj: Record, options: SortOp let sortedPackage = baseSortPackageJson(packageObj, { sortOrder, -}); + }); if (sortedPackage.exports) { sortedPackage.exports = sortExportsKeys(sortedPackage.exports); From 0427c1836b5f18ea52445e4f0734bc6e3c643bde Mon Sep 17 00:00:00 2001 From: Marc Beinder Date: Thu, 22 Jan 2026 10:38:30 -0600 Subject: [PATCH 3/8] Fix inconsistent brace alignment and remove unnecessary blank lines --- src/cli.ts | 4 +- src/core/ast/ASTAnalyzer.ts | 6 +- src/core/config/ConfigMerger.ts | 2 +- src/core/di/Container.ts | 4 +- .../rules/ast/ClassMemberSortingRule.ts | 11 --- .../rules/ast/FileDeclarationSortingRule.ts | 9 --- .../rules/imports/ImportOrganizationRule.ts | 19 +---- .../index-generation/IndexGenerationRule.ts | 11 --- .../spacing/BlankLineBeforeReturnsRule.ts | 1 - .../BlankLineBetweenDeclarationsRule.ts | 26 +++---- .../BlankLineBetweenStatementTypesRule.ts | 23 ++---- .../rules/spacing/BlockSpacingRule.ts | 1 - .../rules/spacing/BracketSpacingRule.ts | 8 -- .../formatters/rules/style/IndentationRule.ts | 6 -- .../formatters/rules/style/QuoteStyleRule.ts | 3 - .../formatters/rules/style/SemicolonRule.ts | 7 +- .../rules/style/StructuralIndentationRule.ts | 4 +- src/core/pipeline/FormatterPipeline.ts | 73 ++++++++++--------- 18 files changed, 69 insertions(+), 149 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index c220c6a..ac98aef 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -142,7 +142,7 @@ async function main(): Promise { config.sorting?.enabled || config.spacing?.enabled) { await formatSingleFile(target, config, dryRun); - } + } if (dryRun) { console.info("Dry run completed. No files were modified."); @@ -187,7 +187,7 @@ async function main(): Promise { config.sorting?.enabled || config.spacing?.enabled) { await formatDirectory(target, config, dryRun); - } + } if (dryRun) { console.info("Dry run completed. No files were modified."); diff --git a/src/core/ast/ASTAnalyzer.ts b/src/core/ast/ASTAnalyzer.ts index e4503eb..fffbdb5 100644 --- a/src/core/ast/ASTAnalyzer.ts +++ b/src/core/ast/ASTAnalyzer.ts @@ -115,7 +115,7 @@ export class ASTAnalyzer { ts.isImportEqualsDeclaration(declaration) || ts.isExportDeclaration(declaration)) { return new Set(); - } + } const refs = this.extractReferences(declaration, name => availableDeclarations.has(name)); // For file declarations, we care about all identifiers @@ -142,7 +142,7 @@ export class ASTAnalyzer { if (ts.isStringLiteral(member.name)) { return member.name.text; } - } + } return ""; } @@ -156,7 +156,7 @@ export class ASTAnalyzer { ts.isFunctionDeclaration(declaration) || ts.isClassDeclaration(declaration)) { return declaration.name?.text || ""; - } + } if (ts.isVariableStatement(declaration)) { const firstDecl = declaration.declarationList.declarations[0]; diff --git a/src/core/config/ConfigMerger.ts b/src/core/config/ConfigMerger.ts index abf0c87..c4382e6 100644 --- a/src/core/config/ConfigMerger.ts +++ b/src/core/config/ConfigMerger.ts @@ -30,7 +30,7 @@ export class ConfigMerger { result[key] = this.deepMerge(result[key] as any, source[key] as any); } else { result[key] = source[key] as T[Extract]; - } + } } } diff --git a/src/core/di/Container.ts b/src/core/di/Container.ts index 346517c..cdd8af9 100644 --- a/src/core/di/Container.ts +++ b/src/core/di/Container.ts @@ -46,7 +46,7 @@ export class Container { if (line.includes("Container.extractGenericTypeName") || line.includes("Container.resolve")) { continue; - } + } // Find the first external call location const match = line.match(/at\s+.*\s+\((.+):(\d+):(\d+)\)/); @@ -88,7 +88,7 @@ export class Container { if (line.includes("Container.extractGenericTypeNameForRegistration") || line.includes("Container.singleton")) { continue; - } + } // Find the first external call location const match = line.match(/at\s+.*\s+\((.+):(\d+):(\d+)\)/); diff --git a/src/core/formatters/rules/ast/ClassMemberSortingRule.ts b/src/core/formatters/rules/ast/ClassMemberSortingRule.ts index a05d9a4..95593c6 100644 --- a/src/core/formatters/rules/ast/ClassMemberSortingRule.ts +++ b/src/core/formatters/rules/ast/ClassMemberSortingRule.ts @@ -10,7 +10,6 @@ import { BaseFormattingRule } from "../../BaseFormattingRule"; /** Types of class members */ - export enum MemberType { StaticProperty = "static_property", InstanceProperty = "instance_property", @@ -22,7 +21,6 @@ export enum MemberType { } /** Analyzed class member with metadata */ - export interface ClassMember { node: ts.ClassElement; type: MemberType; @@ -38,7 +36,6 @@ export interface ClassMember { } /** Default order for class members */ - export const DEFAULT_CLASS_ORDER: MemberType[] = [ MemberType.StaticProperty, @@ -51,7 +48,6 @@ export const DEFAULT_CLASS_ORDER: MemberType[] = [ ]; /** Sorts class members according to configured order */ - export class ClassMemberSortingRule extends BaseFormattingRule { readonly name = "ClassMemberSortingRule"; @@ -126,7 +122,6 @@ export class ClassMemberSortingRule extends BaseFormattingRule { /** Compare two class members for sorting */ private compareMembers(a: ClassMember, b: ClassMember, aTypeIndex: number, bTypeIndex: number): number { // First, sort by member type according to the defined order - if (aTypeIndex !== bTypeIndex) { return aTypeIndex - bTypeIndex; } @@ -173,7 +168,6 @@ export class ClassMemberSortingRule extends BaseFormattingRule { let formatted = source; // Find all class declarations and reorder their members - const classes: ts.ClassDeclaration[] = []; const visit = (node: ts.Node) => { if (ts.isClassDeclaration(node)) { @@ -184,7 +178,6 @@ export class ClassMemberSortingRule extends BaseFormattingRule { visit(sourceFile); // Process classes in reverse order to maintain correct positions - for (let i = classes.length - 1; i >= 0; i--) { const classNode = classes[i]; @@ -193,7 +186,6 @@ export class ClassMemberSortingRule extends BaseFormattingRule { } // Analyze and sort members - const allMemberNames = new Set(classNode.members .map(m => ASTAnalyzer.getClassMemberName(m)) @@ -211,7 +203,6 @@ export class ClassMemberSortingRule extends BaseFormattingRule { } // Check if reordering is needed - const orderChanged = sortedMembers.some((member, index) => member.originalIndex !== index); if (!orderChanged) { @@ -219,7 +210,6 @@ export class ClassMemberSortingRule extends BaseFormattingRule { } // Reconstruct class body with reordered members using original text - const firstMember = classNode.members[0]; const lastMember = classNode.members[classNode.members.length - 1]; const classBodyStart = firstMember.getFullStart(); @@ -230,7 +220,6 @@ export class ClassMemberSortingRule extends BaseFormattingRule { const newClassBody = memberTexts.join("\n\n"); // Replace the class body (add leading newline for proper spacing) - formatted = formatted.substring(0, classBodyStart) + "\n" + newClassBody + formatted.substring(classBodyEnd); } diff --git a/src/core/formatters/rules/ast/FileDeclarationSortingRule.ts b/src/core/formatters/rules/ast/FileDeclarationSortingRule.ts index 9b1e617..1ccc1c8 100644 --- a/src/core/formatters/rules/ast/FileDeclarationSortingRule.ts +++ b/src/core/formatters/rules/ast/FileDeclarationSortingRule.ts @@ -10,7 +10,6 @@ import { BaseFormattingRule } from "../../BaseFormattingRule"; /** Types of top-level declarations in a file */ - export enum DeclarationType { Interface = "interface", TypeAlias = "type_alias", @@ -25,7 +24,6 @@ export enum DeclarationType { } /** Analyzed file declaration with metadata */ - export interface FileDeclaration { node: ts.Statement; type: DeclarationType; @@ -38,7 +36,6 @@ export interface FileDeclaration { } /** Default order for file declarations */ - export const DEFAULT_FILE_ORDER: DeclarationType[] = [ DeclarationType.Interface, @@ -54,7 +51,6 @@ export const DEFAULT_FILE_ORDER: DeclarationType[] = [ ]; /** Sorts file-level declarations according to configured order */ - export class FileDeclarationSortingRule extends BaseFormattingRule { readonly name = "FileDeclarationSortingRule"; @@ -170,7 +166,6 @@ export class FileDeclarationSortingRule extends BaseFormattingRule { } // Analyze and sort declarations - const allDeclarationNames = new Set(otherStatements.map(stmt => ASTAnalyzer.getDeclarationName(stmt)).filter(n => n)); @@ -187,7 +182,6 @@ export class FileDeclarationSortingRule extends BaseFormattingRule { } // Check if reordering is needed - const orderChanged = sortedDeclarations.some((decl, index) => decl.originalIndex !== index); if (!orderChanged) { @@ -195,7 +189,6 @@ export class FileDeclarationSortingRule extends BaseFormattingRule { } // Reconstruct file with reordered declarations using original text - const firstDeclaration = otherStatements[0]; const lastDeclaration = otherStatements[otherStatements.length - 1]; const declarationsStart = firstDeclaration.getFullStart(); @@ -206,11 +199,9 @@ export class FileDeclarationSortingRule extends BaseFormattingRule { const newDeclarations = declarationTexts.join("\n\n"); // Replace the declarations section (add spacing between imports and declarations) - let formatted = source.substring(0, declarationsStart) + "\n\n" + newDeclarations + source.substring(declarationsEnd); // Remove trailing semicolons that TypeScript printer adds after closing braces - formatted = formatted.replace(/(\n;)+\s*$/, "\n"); return formatted; diff --git a/src/core/formatters/rules/imports/ImportOrganizationRule.ts b/src/core/formatters/rules/imports/ImportOrganizationRule.ts index e8ad470..1200685 100644 --- a/src/core/formatters/rules/imports/ImportOrganizationRule.ts +++ b/src/core/formatters/rules/imports/ImportOrganizationRule.ts @@ -4,7 +4,7 @@ */ import * as ts from "typescript"; -import { BaseFormattingRule } from "../../BaseFormattingRule"; +import {BaseFormattingRule} from "../../BaseFormattingRule"; interface ImportInfo { @@ -17,7 +17,6 @@ interface ImportInfo { } /** Organizes and formats import statements */ - export class ImportOrganizationRule extends BaseFormattingRule { readonly name = "ImportOrganizationRule"; @@ -68,12 +67,10 @@ export class ImportOrganizationRule extends BaseFormattingRule { return identifiers; } // Default import - if (importInfo.importClause.name) { identifiers.push(importInfo.importClause.name.text); } // Named imports - if (importInfo.importClause.namedBindings) { if (ts.isNamedImports(importInfo.importClause.namedBindings)) { for (const element of importInfo.importClause.namedBindings.elements) { @@ -97,7 +94,6 @@ export class ImportOrganizationRule extends BaseFormattingRule { if (ts.isIdentifier(node) && node.text === identifier) { // Make sure it's not the import declaration itself - const parent = node.parent; if (!ts.isImportSpecifier(parent) && !ts.isImportClause(parent)) { @@ -113,7 +109,6 @@ export class ImportOrganizationRule extends BaseFormattingRule { private isImportUsed(importInfo: ImportInfo, sourceFile: ts.SourceFile): boolean { // Side-effect imports are considered "used" - if (importInfo.isSideEffect) { return true; } @@ -133,7 +128,6 @@ export class ImportOrganizationRule extends BaseFormattingRule { return imports; } // Don't remove side-effect imports unless configured - if (!config.removeSideEffects) { return imports.filter(imp => imp.isSideEffect || this.isImportUsed(imp, sourceFile)); } @@ -181,7 +175,6 @@ export class ImportOrganizationRule extends BaseFormattingRule { let leadingComments = leadingCommentsMatch ? leadingCommentsMatch[1].trim() : ""; // Deduplicate consecutive identical block comments (fixes copyright duplication) - if (leadingComments) { const commentBlocks = leadingComments.match(/\/\*[\s\S]*?\*\//g) || []; const uniqueBlocks = new Set(commentBlocks.map(block => block.trim())); @@ -190,16 +183,13 @@ export class ImportOrganizationRule extends BaseFormattingRule { } // Find the last import statement position - const importStatements = sourceFile.statements.filter(stmt => ts.isImportDeclaration(stmt)); const lastImport = importStatements[importStatements.length - 1]; const afterImportsPos = lastImport ? lastImport.getEnd() : (leadingCommentsMatch ? leadingCommentsMatch[0].length : 0); // Extract everything after imports, preserving original formatting - let restOfFile = fullText.substring(afterImportsPos); // Ensure restOfFile starts with a newline - if (restOfFile && !restOfFile.startsWith("\n")) { restOfFile = "\n" + restOfFile; } @@ -207,7 +197,6 @@ export class ImportOrganizationRule extends BaseFormattingRule { restOfFile = restOfFile.replace(/^\n{2,}/, "\n"); // Build import section (only reprint imports, not everything else) - const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed, removeComments: false, @@ -219,9 +208,7 @@ export class ImportOrganizationRule extends BaseFormattingRule { for (const importInfo of imports) { // Add blank line between groups if configured - if (config?.separateGroups && - lastGroup !== null && lastGroup !== importInfo.group) { importLines.push(""); @@ -231,14 +218,12 @@ export class ImportOrganizationRule extends BaseFormattingRule { // Strip any leading block comments from individual imports // (file-level copyright is handled separately at the top) - importText = importText.replace(/^((?:\/\*[\s\S]*?\*\/\s*)+)/, "").trim(); importLines.push(importText); lastGroup = importInfo.group; } // Combine sections - const sections: string[] = []; if (leadingComments) { @@ -256,7 +241,6 @@ export class ImportOrganizationRule extends BaseFormattingRule { let combined = sections.join("\n\n"); // Remove trailing semicolons that TypeScript printer adds after closing braces - combined = combined.replace(/(;\n+)+;?\s*$/, "\n"); return combined; @@ -273,7 +257,6 @@ export class ImportOrganizationRule extends BaseFormattingRule { // Apply all transformations in sequence // Each method checks its own config and returns early if disabled - let processedImports = this.filterUnusedImports(imports, sourceFile); processedImports = this.sortImports(processedImports); diff --git a/src/core/formatters/rules/index-generation/IndexGenerationRule.ts b/src/core/formatters/rules/index-generation/IndexGenerationRule.ts index eb02a11..07238e3 100644 --- a/src/core/formatters/rules/index-generation/IndexGenerationRule.ts +++ b/src/core/formatters/rules/index-generation/IndexGenerationRule.ts @@ -61,9 +61,7 @@ export class IndexGenerationRule extends BaseFormattingRule { private isTestDirectory(dirName: string): boolean { // Common test directory patterns (not configurable) - const testDirectories = [ - "__tests__", "tests", "test", @@ -79,9 +77,7 @@ export class IndexGenerationRule extends BaseFormattingRule { private isTestFile(fileName: string): boolean { // Common test file patterns (not configurable) - const testPatterns = [ - /\.test\.(ts|tsx|js|jsx)$/, /\.spec\.(ts|tsx|js|jsx)$/, /\.(test|spec)\.(ts|tsx|js|jsx)$/, @@ -108,7 +104,6 @@ export class IndexGenerationRule extends BaseFormattingRule { continue; } // Check if subdirectory has an index file - const subIndexPath = path.join(dir, entry.name, options.indexFileName); if (fs.existsSync(subIndexPath)) { @@ -116,12 +111,10 @@ export class IndexGenerationRule extends BaseFormattingRule { } } else if (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) { // Skip .d.ts files when processing regular .ts files - if (entry.name.endsWith(".d.ts")) { continue; } // Always skip test files (not configurable) - if (this.isTestFile(entry.name)) { continue; } @@ -194,13 +187,11 @@ ${exports.join("\n")} for (const entry of entries) { // Skip index.ts itself - if (entry.name === "index.ts") { continue; } // Check for directories with index.ts - if (entry.isDirectory()) { // Always skip test directories (not configurable) if (this.isTestDirectory(entry.name)) { @@ -214,7 +205,6 @@ ${exports.join("\n")} } } // Check for .d.ts files (like generated.d.ts) - else if (entry.name.endsWith(".d.ts")) { const moduleName = entry.name.slice(0, -3); // Remove .ts but keep .d @@ -265,7 +255,6 @@ ${exports} } // Update main src/index.ts if configured - if (config?.updateMainIndex !== false) { const srcDir = path.join(projectRoot, "src"); const mainIndexPath = path.join(srcDir, "index.ts"); diff --git a/src/core/formatters/rules/spacing/BlankLineBeforeReturnsRule.ts b/src/core/formatters/rules/spacing/BlankLineBeforeReturnsRule.ts index bbbfccf..19849bd 100644 --- a/src/core/formatters/rules/spacing/BlankLineBeforeReturnsRule.ts +++ b/src/core/formatters/rules/spacing/BlankLineBeforeReturnsRule.ts @@ -10,7 +10,6 @@ import { BaseFormattingRule } from "../../BaseFormattingRule"; * Adds blank lines before return statements * Works at all brace depths (not just top level) */ - export class BlankLineBeforeReturnsRule extends BaseFormattingRule { readonly name = "BlankLineBeforeReturnsRule"; diff --git a/src/core/formatters/rules/spacing/BlankLineBetweenDeclarationsRule.ts b/src/core/formatters/rules/spacing/BlankLineBetweenDeclarationsRule.ts index 7fa0d88..3fa8bee 100644 --- a/src/core/formatters/rules/spacing/BlankLineBetweenDeclarationsRule.ts +++ b/src/core/formatters/rules/spacing/BlankLineBetweenDeclarationsRule.ts @@ -3,18 +3,18 @@ * All Rights Reserved. */ -import { BaseFormattingRule } from "../../BaseFormattingRule"; +import {BaseFormattingRule} from "../../BaseFormattingRule"; /** -* Adds blank lines between declarations with different keywords -* KEY ENHANCEMENT: Works at ALL brace depths (not just top level) -* -* Examples: -* - No blank line between consecutive "const" declarations -* - No blank line between consecutive "export" statements -* - Blank line when keyword changes (const → let, export → const, etc.) -*/ + * Adds blank lines between declarations with different keywords + * KEY ENHANCEMENT: Works at ALL brace depths (not just top level) + * + * Examples: + * - No blank line between consecutive "const" declarations + * - No blank line between consecutive "export" statements + * - Blank line when keyword changes (const → let, export → const, etc.) + */ export class BlankLineBetweenDeclarationsRule extends BaseFormattingRule { readonly name = "BlankLineBetweenDeclarationsRule"; @@ -96,13 +96,11 @@ export class BlankLineBetweenDeclarationsRule extends BaseFormattingRule { const isDeclarationStart = declarationKeyword !== null; // Check if we've left the import section - if (inImportSection && !isImport && !isBlankLine && !isComment) { inImportSection = false; } // KEY ENHANCEMENT: Removed "braceDepth === 0" check // Now works at ALL depths, not just top level - if (!inImportSection) { // Add blank line before block comments that precede declarations if (isBlockCommentStart && @@ -112,9 +110,8 @@ export class BlankLineBetweenDeclarationsRule extends BaseFormattingRule { result[result.length - 1].trim() !== "") { result.push(""); lastNonBlankLineWasDeclarationEnd = false; - } + } // Add blank line before declaration starts ONLY if the keyword is different - else if (isDeclarationStart && lastNonBlankLineWasDeclarationEnd && @@ -127,7 +124,6 @@ export class BlankLineBetweenDeclarationsRule extends BaseFormattingRule { } result.push(line); // Track declaration ends BEFORE updating brace depth - const hasClosingElement = trimmedLine === "}" || trimmedLine.endsWith("}") || @@ -138,13 +134,11 @@ export class BlankLineBetweenDeclarationsRule extends BaseFormattingRule { if (!isBlankLine && hasClosingElement) { lastNonBlankLineWasDeclarationEnd = true; // Update the last declaration keyword when a declaration ends - if (isDeclarationStart) { lastDeclarationKeyword = declarationKeyword; } } else if (!isBlankLine && !isComment) { // Don't reset if the line is just closing braces - if (!isBlockCommentStart && trimmedLine !== "" && !isJustClosingBraces) { lastNonBlankLineWasDeclarationEnd = isDeclarationStart; diff --git a/src/core/formatters/rules/spacing/BlankLineBetweenStatementTypesRule.ts b/src/core/formatters/rules/spacing/BlankLineBetweenStatementTypesRule.ts index 57398b6..848f19e 100644 --- a/src/core/formatters/rules/spacing/BlankLineBetweenStatementTypesRule.ts +++ b/src/core/formatters/rules/spacing/BlankLineBetweenStatementTypesRule.ts @@ -3,11 +3,10 @@ * All Rights Reserved. */ -import { BaseFormattingRule } from "../../BaseFormattingRule"; +import {BaseFormattingRule} from "../../BaseFormattingRule"; /** Statement types for categorization */ - enum StatementType { Declaration = "declaration",// const, let, var, function, class, etc. Control = "control",// if, else, switch, case @@ -18,12 +17,12 @@ enum StatementType { } /** -* Adds blank lines when switching between different statement types -* Examples: -* - Blank line between declarations and control flow -* - Blank line between loops and expressions -* - No blank line within the same statement type -*/ + * Adds blank lines when switching between different statement types + * Examples: + * - Blank line between declarations and control flow + * - Blank line between loops and expressions + * - No blank line within the same statement type + */ export class BlankLineBetweenStatementTypesRule extends BaseFormattingRule { readonly name = "BlankLineBetweenStatementTypesRule"; @@ -31,7 +30,6 @@ export class BlankLineBetweenStatementTypesRule extends BaseFormattingRule { /** Determine the type of a statement */ private getStatementType(trimmedLine: string): StatementType { // Control flow - if (trimmedLine.startsWith("if ") || trimmedLine.startsWith("if(") || @@ -44,7 +42,6 @@ export class BlankLineBetweenStatementTypesRule extends BaseFormattingRule { return StatementType.Control; } // Loops - if (trimmedLine.startsWith("for ") || trimmedLine.startsWith("for(") || @@ -55,7 +52,6 @@ export class BlankLineBetweenStatementTypesRule extends BaseFormattingRule { return StatementType.Loop; } // Exceptions - if (trimmedLine.startsWith("try ") || trimmedLine.startsWith("try{") || @@ -67,7 +63,6 @@ export class BlankLineBetweenStatementTypesRule extends BaseFormattingRule { return StatementType.Exception; } // Declarations - if (trimmedLine.startsWith("const ") || trimmedLine.startsWith("let ") || @@ -107,21 +102,17 @@ export class BlankLineBetweenStatementTypesRule extends BaseFormattingRule { const isImport = trimmedLine.startsWith("import "); // Check if we've left the import section - if (inImportSection && !isImport && !isBlankLine && !isComment) { inImportSection = false; } // Skip import section - if (inImportSection || isBlankLine || isComment) { result.push(line); continue; } // Get statement type for non-blank, non-comment lines - const currentStatementType = this.getStatementType(trimmedLine); // Add blank line if statement type changed - if (lastStatementType !== null && lastStatementType !== currentStatementType && diff --git a/src/core/formatters/rules/spacing/BlockSpacingRule.ts b/src/core/formatters/rules/spacing/BlockSpacingRule.ts index 2c92491..5332ee2 100644 --- a/src/core/formatters/rules/spacing/BlockSpacingRule.ts +++ b/src/core/formatters/rules/spacing/BlockSpacingRule.ts @@ -21,7 +21,6 @@ export class BlockSpacingRule extends BaseFormattingRule { // Remove blank lines after opening braces of interfaces, classes, enums, functions // Pattern: { followed by newlines and whitespace before content - result = result.replace(/\{\n\n+(\s*(?:\/\*\*|[a-zA-Z_]))/g, "{\n$1"); // Remove blank lines between JSDoc comments and what they describe diff --git a/src/core/formatters/rules/spacing/BracketSpacingRule.ts b/src/core/formatters/rules/spacing/BracketSpacingRule.ts index 4ef95ff..3e3bb97 100644 --- a/src/core/formatters/rules/spacing/BracketSpacingRule.ts +++ b/src/core/formatters/rules/spacing/BracketSpacingRule.ts @@ -33,7 +33,6 @@ export class BracketSpacingRule extends BaseFormattingRule { const fullText = sourceFile.getFullText(); const visit = (node: ts.Node) => { // Handle object literals - if (ts.isObjectLiteralExpression(node)) { const openBraceEnd = node.getStart(sourceFile) + 1; // Position after '{' const closeBraceStart = node.getEnd() - 1; // Position of '}' @@ -41,14 +40,12 @@ export class BracketSpacingRule extends BaseFormattingRule { if (node.properties.length > 0) { if (config.bracketSpacing) { // Add spacing after opening brace - const afterOpenBrace = fullText[openBraceEnd]; if (afterOpenBrace !== " " && afterOpenBrace !== "\n") { changes.push({pos: openBraceEnd, type: "add", text: " "}); } // Add spacing before closing brace - const beforeCloseBrace = fullText[closeBraceStart - 1]; if (beforeCloseBrace !== " " && beforeCloseBrace !== "\n") { @@ -56,7 +53,6 @@ export class BracketSpacingRule extends BaseFormattingRule { } } else { // Remove spacing after opening brace - let pos = openBraceEnd; while (fullText[pos] === " " || fullText[pos] === "\t") { @@ -74,7 +70,6 @@ export class BracketSpacingRule extends BaseFormattingRule { } } // Handle named imports - if (ts.isNamedImports(node)) { const parent = node.parent; @@ -85,14 +80,12 @@ export class BracketSpacingRule extends BaseFormattingRule { if (node.elements.length > 0) { if (config.bracketSpacing) { // Add spacing after opening brace - const afterOpenBrace = fullText[openBraceEnd]; if (afterOpenBrace !== " ") { changes.push({pos: openBraceEnd, type: "add", text: " "}); } // Add spacing before closing brace - const beforeCloseBrace = fullText[closeBraceStart - 1]; if (beforeCloseBrace !== " ") { @@ -100,7 +93,6 @@ export class BracketSpacingRule extends BaseFormattingRule { } } else { // Remove spacing after opening brace - let pos = openBraceEnd; while (fullText[pos] === " " || fullText[pos] === "\t") { diff --git a/src/core/formatters/rules/style/IndentationRule.ts b/src/core/formatters/rules/style/IndentationRule.ts index 424ae12..d01c68d 100644 --- a/src/core/formatters/rules/style/IndentationRule.ts +++ b/src/core/formatters/rules/style/IndentationRule.ts @@ -31,36 +31,30 @@ export class IndentationRule extends BaseFormattingRule { for (const line of lines) { // Skip empty lines - if (line.trim() === "") { result.push(line); continue; } // Get the leading whitespace - const leadingWhitespace = line.match(/^\s*/)?.[0] || ""; const content = line.substring(leadingWhitespace.length); // Calculate indent level based on current whitespace - let indentLevel = 0; if (config.indentStyle === "space") { // Count tabs and spaces - const tabCount = (leadingWhitespace.match(/\t/g) || []).length; const spaceCount = (leadingWhitespace.match(/ /g) || []).length; indentLevel = tabCount + Math.floor(spaceCount / indentWidth); } else { // Converting to tabs - const spaceCount = (leadingWhitespace.match(/ /g) || []).length; const tabCount = (leadingWhitespace.match(/\t/g) || []).length; indentLevel = tabCount + Math.floor(spaceCount / indentWidth); } // Create new indentation - let newIndent: string; if (config.indentStyle === "space") { diff --git a/src/core/formatters/rules/style/QuoteStyleRule.ts b/src/core/formatters/rules/style/QuoteStyleRule.ts index 7d3c988..7f090ff 100644 --- a/src/core/formatters/rules/style/QuoteStyleRule.ts +++ b/src/core/formatters/rules/style/QuoteStyleRule.ts @@ -28,7 +28,6 @@ export class QuoteStyleRule extends BaseFormattingRule { const visit = (node: ts.Node) => { // Handle string literals (but not template literals) - if (ts.isStringLiteral(node)) { const nodeText = node.getText(sourceFile); const currentQuote = nodeText[0]; @@ -37,12 +36,10 @@ export class QuoteStyleRule extends BaseFormattingRule { if (currentQuote !== desiredQuote) { // Get the string content (without quotes) - const content = node.text; // Check if the new quote style would require escaping const needsEscape = content.includes(desiredQuote); // If it needs escaping, skip this string literal - if (!needsEscape) { const newText = desiredQuote + content + desiredQuote; diff --git a/src/core/formatters/rules/style/SemicolonRule.ts b/src/core/formatters/rules/style/SemicolonRule.ts index ec11e46..d85caf9 100644 --- a/src/core/formatters/rules/style/SemicolonRule.ts +++ b/src/core/formatters/rules/style/SemicolonRule.ts @@ -4,7 +4,7 @@ */ import * as ts from "typescript"; -import { BaseFormattingRule } from "../../BaseFormattingRule"; +import {BaseFormattingRule} from "../../BaseFormattingRule"; /** Adds or removes semicolons based on configuration using AST */ @@ -27,7 +27,6 @@ export class SemicolonRule extends BaseFormattingRule { const visit = (node: ts.Node) => { // Check statements that should have semicolons // NOTE: Interfaces, classes, and enums should NOT have semicolons after their closing braces - if (ts.isVariableStatement(node) || ts.isExpressionStatement(node) || @@ -44,17 +43,14 @@ export class SemicolonRule extends BaseFormattingRule { if (config.semicolons === "always" && !hasSemicolon) { // Add semicolon - changes.push({pos: nodeEnd, type: "add"}); } else if (config.semicolons === "never" && hasSemicolon) { // Remove semicolon - changes.push({pos: nodeEnd - 1, type: "remove"}); } } // Remove incorrect semicolons from interfaces, classes, and enums - if (ts.isInterfaceDeclaration(node) || ts.isClassDeclaration(node) || ts.isEnumDeclaration(node)) { const nodeEnd = node.getEnd(); const fullText = sourceFile.getFullText(); @@ -62,7 +58,6 @@ export class SemicolonRule extends BaseFormattingRule { if (hasSemicolon) { // Remove the incorrect semicolon after the closing brace - changes.push({pos: nodeEnd, type: "remove"}); } } diff --git a/src/core/formatters/rules/style/StructuralIndentationRule.ts b/src/core/formatters/rules/style/StructuralIndentationRule.ts index 1e0112b..c5daae1 100644 --- a/src/core/formatters/rules/style/StructuralIndentationRule.ts +++ b/src/core/formatters/rules/style/StructuralIndentationRule.ts @@ -101,7 +101,7 @@ private checkNodeBrackets( } } } -} + } private analyzeBracketStructure( sourceFile: ts.SourceFile, @@ -117,7 +117,7 @@ private analyzeBracketStructure( }; visit(sourceFile); -} + } private getScriptKind(filePath?: string): ts.ScriptKind { if (!filePath) { diff --git a/src/core/pipeline/FormatterPipeline.ts b/src/core/pipeline/FormatterPipeline.ts index 7644da5..8cb0a11 100644 --- a/src/core/pipeline/FormatterPipeline.ts +++ b/src/core/pipeline/FormatterPipeline.ts @@ -5,15 +5,30 @@ import * as fs from "fs/promises"; import * as path from "path"; -import { CoreConfig, FormatterOrder } from "../config"; -import { Container } from "../di"; -import { BlankLineBeforeReturnsRule, BlankLineBetweenDeclarationsRule, BlankLineBetweenStatementTypesRule, BlockSpacingRule, BracketSpacingRule, ClassMemberSortingRule, DocBlockCommentRule, FileDeclarationSortingRule, IFormattingRule, ImportOrganizationRule, IndentationRule, IndexGenerationRule, QuoteStyleRule, SemicolonRule, StructuralIndentationRule } from "../formatters"; +import {CoreConfig, FormatterOrder} from "../config"; +import {Container} from "../di"; +import { + BlankLineBeforeReturnsRule, + BlankLineBetweenDeclarationsRule, + BlankLineBetweenStatementTypesRule, + BlockSpacingRule, + BracketSpacingRule, + ClassMemberSortingRule, + DocBlockCommentRule, + FileDeclarationSortingRule, + IFormattingRule, + ImportOrganizationRule, + IndentationRule, + IndexGenerationRule, + QuoteStyleRule, + SemicolonRule, + StructuralIndentationRule +} from "../formatters"; /* * Tracks the state of a single formatter execution */ - export interface FormatterExecution { formatterName: string; order: FormatterOrder; @@ -22,7 +37,6 @@ export interface FormatterExecution { } /** Context object tracking the entire pipeline execution */ - export interface PipelineContext { filePath: string; originalSource: string; @@ -33,7 +47,6 @@ export interface PipelineContext { } /** Error thrown when a formatter fails during pipeline execution */ - export class FormatterError extends Error { constructor(public readonly formatterName: string, public readonly filePath: string, public readonly originalError: Error) { super(`Formatter '${formatterName}' failed for file '${filePath}': ${originalError.message}`); @@ -42,10 +55,9 @@ export class FormatterError extends Error { } /** -* Orchestrates the execution of multiple formatters in a defined order. -* Implements fail-fast error handling and supports dry-run mode. -*/ - + * Orchestrates the execution of multiple formatters in a defined order. + * Implements fail-fast error handling and supports dry-run mode. + */ export class FormatterPipeline { private formatterOrder: FormatterOrder[]; @@ -165,15 +177,14 @@ export class FormatterPipeline { } /** - * Format a file using the configured formatters in sequence - * @param filePath - Absolute path to the file to format - * @param dryRun - If true, don't write changes to disk - * @returns Pipeline context with execution details - * @throws FormatterError if any formatter fails (fail-fast) - */ + * Format a file using the configured formatters in sequence + * @param filePath - Absolute path to the file to format + * @param dryRun - If true, don't write changes to disk + * @returns Pipeline context with execution details + * @throws FormatterError if any formatter fails (fail-fast) + */ async formatFile(filePath: string, dryRun = false): Promise { // Read original source - const originalSource = await fs.readFile(filePath, "utf-8"); // Initialize pipeline context @@ -187,7 +198,6 @@ export class FormatterPipeline { }; // Execute rules in order - for (const order of this.formatterOrder) { const rulesAtOrder = this.rules.get(order); @@ -204,12 +214,10 @@ export class FormatterPipeline { try { // Execute rule - const beforeSource = context.currentSource; const afterSource = rule.apply(context.currentSource, filePath); // Track changes - execution.changed = beforeSource !== afterSource; context.currentSource = afterSource; @@ -228,7 +236,6 @@ export class FormatterPipeline { } // Write to disk if changes were made and not in dry-run mode - if (context.changed && !dryRun) { await fs.writeFile(filePath, context.currentSource, "utf-8"); } @@ -237,12 +244,12 @@ export class FormatterPipeline { } /** - * Format multiple files in sequence - * @param filePaths - Array of file paths to format - * @param dryRun - If true, don't write changes to disk - * @returns Array of pipeline contexts for each file - * @throws FormatterError if any formatter fails for any file - */ + * Format multiple files in sequence + * @param filePaths - Array of file paths to format + * @param dryRun - If true, don't write changes to disk + * @returns Array of pipeline contexts for each file + * @throws FormatterError if any formatter fails for any file + */ async formatFiles(filePaths: string[], dryRun = false): Promise { const results: PipelineContext[] = []; @@ -256,12 +263,12 @@ export class FormatterPipeline { } /** - * Format all files in a directory recursively - * @param dirPath - Directory path to format - * @param dryRun - If true, don't write changes to disk - * @param extensions - File extensions to include (default: .ts, .tsx, .js, .jsx) - * @returns Array of pipeline contexts for each file - */ + * Format all files in a directory recursively + * @param dirPath - Directory path to format + * @param dryRun - If true, don't write changes to disk + * @param extensions - File extensions to include (default: .ts, .tsx, .js, .jsx) + * @returns Array of pipeline contexts for each file + */ async formatDirectory(dirPath: string, dryRun = false, extensions: string[] = [".ts", ".tsx", ".js", ".jsx"]): Promise { const files = await this.getFilesRecursively(dirPath, extensions); From 787769dd68680afdf9592d799c93d8eeed857f2f Mon Sep 17 00:00:00 2001 From: Marc Beinder Date: Thu, 22 Jan 2026 10:38:42 -0600 Subject: [PATCH 4/8] Add Structural Indentation Rule to service registration and formatting pipeline --- .gitignore | 1 + dist/cli.js | 108 +++++++++++++++++------- dist/core/di/ServiceRegistration.js | 2 + dist/core/pipeline/FormatterPipeline.js | 1 + dist/index.js | 2 + vite.config.ts | 12 +-- 6 files changed, 90 insertions(+), 36 deletions(-) diff --git a/.gitignore b/.gitignore index b4865f8..63ba673 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules /.claude/settings.local.json +tmpclaude* \ No newline at end of file diff --git a/dist/cli.js b/dist/cli.js index a7f848a..0dc9618 100644 --- a/dist/cli.js +++ b/dist/cli.js @@ -30,7 +30,26 @@ function _interopNamespaceDefault(e) { const fs__namespace = /* @__PURE__ */ _interopNamespaceDefault(fs); const glob__namespace = /* @__PURE__ */ _interopNamespaceDefault(glob); const path__namespace = /* @__PURE__ */ _interopNamespaceDefault(path); -async function formatFiles(targetDir, config, dryRun) { +async function formatSingleFile(filePath, config, dryRun) { + const container = new Container.Container(); + ServiceRegistration.ServiceRegistration.registerServices(container, config); + const pipeline = container.resolve("FormatterPipeline"); + try { + const context = await pipeline.formatFile(filePath, dryRun); + if (context.changed) { + if (dryRun) { + console.info(`Would format: ${filePath}`); + } else { + console.log(`📊 Formatted: ${filePath}`); + } + } else { + console.info(`No changes needed: ${filePath}`); + } + } catch (error) { + console.error(`Error formatting file ${filePath}:`, error.message); + } +} +async function formatDirectory(targetDir, config, dryRun) { const container = new Container.Container(); ServiceRegistration.ServiceRegistration.registerServices(container, config); const include = config.sorting?.include || ["**/*.{ts,tsx,js,jsx}"]; @@ -68,54 +87,83 @@ async function formatFiles(targetDir, config, dryRun) { console.info(`Formatted ${formattedCount} of ${files.length} files.`); } } +function isSupportedFile(filePath) { + const supportedExtensions = [".ts", ".tsx", ".js", ".jsx"]; + return supportedExtensions.some((ext) => filePath.endsWith(ext)); +} async function main() { const args = process.argv.slice(2); - let targetDir = process.cwd(); + let target = process.cwd(); let dryRun = false; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg === "--dry") { dryRun = true; } else if (!arg.startsWith("-")) { - targetDir = path__namespace.resolve(arg); + target = path__namespace.resolve(arg); } else { console.error(`Error: Unsupported option "${arg}". Only --dry is supported.`); process.exit(1); } } try { - const config = ConfigLoader.ConfigLoader.loadConfig(targetDir); - if (ConfigLoader.ConfigLoader.hasConfigFile(targetDir)) { + const targetStat = fs__namespace.existsSync(target) ? fs__namespace.statSync(target) : null; + const isFile = targetStat?.isFile() ?? false; + const isDirectory = targetStat?.isDirectory() ?? false; + if (!targetStat) { + console.error(`Error: Target "${target}" does not exist.`); + process.exit(1); + } + const configDir = isFile ? path__namespace.dirname(target) : target; + const config = ConfigLoader.ConfigLoader.loadConfig(configDir); + if (ConfigLoader.ConfigLoader.hasConfigFile(configDir)) { console.log("Using custom configuration from tsfmt.config.ts"); } - if (config.packageJson?.enabled) { - const packagePath = path__namespace.join(targetDir, "package.json"); - if (fs__namespace.existsSync(packagePath)) { - console.log(`📦 Processing ${packagePath}...`); - sortPackage.sortPackageFile(packagePath, { - customSortOrder: config.packageJson.customSortOrder, - indentation: config.packageJson.indentation, - dryRun - }); + if (isFile) { + if (!isSupportedFile(target)) { + console.error(`Error: Unsupported file type. Supported: .ts, .tsx, .js, .jsx`); + process.exit(1); } - } - if (config.tsConfig?.enabled) { - const tsconfigPath = path__namespace.join(targetDir, "tsconfig.json"); - if (fs__namespace.existsSync(tsconfigPath)) { - console.log(`🔧 Processing ${tsconfigPath}...`); - sortTSConfig.sortTsConfigFile(tsconfigPath, { - indentation: config.tsConfig.indentation, - dryRun - }); + if (config.codeStyle?.enabled || config.imports?.enabled || config.sorting?.enabled || config.spacing?.enabled) { + await formatSingleFile(target, config, dryRun); } + if (dryRun) { + console.info("Dry run completed. No files were modified."); + } else { + console.info("Formatting completed successfully."); + } + return; } - if (config.codeStyle?.enabled || config.imports?.enabled || config.sorting?.enabled || config.spacing?.enabled) { - await formatFiles(targetDir, config, dryRun); - } - if (dryRun) { - console.info("Dry run completed. No files were modified."); - } else { - console.info("Formatting completed successfully."); + if (isDirectory) { + if (config.packageJson?.enabled) { + const packagePath = path__namespace.join(target, "package.json"); + if (fs__namespace.existsSync(packagePath)) { + console.log(`📦 Processing ${packagePath}...`); + sortPackage.sortPackageFile(packagePath, { + customSortOrder: config.packageJson.customSortOrder, + indentation: config.packageJson.indentation, + dryRun + }); + } + } + if (config.tsConfig?.enabled) { + const tsconfigPath = path__namespace.join(target, "tsconfig.json"); + if (fs__namespace.existsSync(tsconfigPath)) { + console.log(`🔧 Processing ${tsconfigPath}...`); + sortTSConfig.sortTsConfigFile(tsconfigPath, { + indentation: config.tsConfig.indentation, + dryRun + }); + } + } + if (config.codeStyle?.enabled || config.imports?.enabled || config.sorting?.enabled || config.spacing?.enabled) { + await formatDirectory(target, config, dryRun); + } + if (dryRun) { + console.info("Dry run completed. No files were modified."); + } else { + console.info("Formatting completed successfully."); + } } } catch (error) { console.error("Error during formatting:", error.message); diff --git a/dist/core/di/ServiceRegistration.js b/dist/core/di/ServiceRegistration.js index 2939229..a97ca3a 100644 --- a/dist/core/di/ServiceRegistration.js +++ b/dist/core/di/ServiceRegistration.js @@ -13,6 +13,7 @@ const DocBlockCommentRule = require("../formatters/rules/style/DocBlockCommentRu const IndentationRule = require("../formatters/rules/style/IndentationRule.js"); const QuoteStyleRule = require("../formatters/rules/style/QuoteStyleRule.js"); const SemicolonRule = require("../formatters/rules/style/SemicolonRule.js"); +const StructuralIndentationRule = require("../formatters/rules/style/StructuralIndentationRule.js"); const FormatterPipeline = require("../pipeline/FormatterPipeline.js"); class ServiceRegistration { static registerServices(container, config) { @@ -21,6 +22,7 @@ class ServiceRegistration { container.singleton(SemicolonRule.SemicolonRule); container.singleton(BracketSpacingRule.BracketSpacingRule); container.singleton(IndentationRule.IndentationRule); + container.singleton(StructuralIndentationRule.StructuralIndentationRule); container.singleton(BlockSpacingRule.BlockSpacingRule); container.singleton(DocBlockCommentRule.DocBlockCommentRule); container.singleton(ImportOrganizationRule.ImportOrganizationRule); diff --git a/dist/core/pipeline/FormatterPipeline.js b/dist/core/pipeline/FormatterPipeline.js index b137b13..a2eb24e 100644 --- a/dist/core/pipeline/FormatterPipeline.js +++ b/dist/core/pipeline/FormatterPipeline.js @@ -211,6 +211,7 @@ class FormatterPipeline { this.addRuleByName("SemicolonRule", ConfigTypes.FormatterOrder.CodeStyle); this.addRuleByName("BracketSpacingRule", ConfigTypes.FormatterOrder.CodeStyle); this.addRuleByName("IndentationRule", ConfigTypes.FormatterOrder.CodeStyle); + this.addRuleByName("StructuralIndentationRule", ConfigTypes.FormatterOrder.CodeStyle); this.addRuleByName("BlockSpacingRule", ConfigTypes.FormatterOrder.CodeStyle); this.addRuleByName("DocBlockCommentRule", ConfigTypes.FormatterOrder.CodeStyle); } diff --git a/dist/index.js b/dist/index.js index 17a5915..4970b8c 100644 --- a/dist/index.js +++ b/dist/index.js @@ -25,6 +25,7 @@ const DocBlockCommentRule = require("./core/formatters/rules/style/DocBlockComme const IndentationRule = require("./core/formatters/rules/style/IndentationRule.js"); const QuoteStyleRule = require("./core/formatters/rules/style/QuoteStyleRule.js"); const SemicolonRule = require("./core/formatters/rules/style/SemicolonRule.js"); +const StructuralIndentationRule = require("./core/formatters/rules/style/StructuralIndentationRule.js"); const FormatterPipeline = require("./core/pipeline/FormatterPipeline.js"); const _package = require("./formatters/package.js"); const types = require("./shared/types.js"); @@ -58,6 +59,7 @@ exports.DocBlockCommentRule = DocBlockCommentRule.DocBlockCommentRule; exports.IndentationRule = IndentationRule.IndentationRule; exports.QuoteStyleRule = QuoteStyleRule.QuoteStyleRule; exports.SemicolonRule = SemicolonRule.SemicolonRule; +exports.StructuralIndentationRule = StructuralIndentationRule.StructuralIndentationRule; exports.FormatterError = FormatterPipeline.FormatterError; exports.FormatterPipeline = FormatterPipeline.FormatterPipeline; exports.sortExportsKeys = _package.sortExportsKeys; diff --git a/vite.config.ts b/vite.config.ts index f3e6fbe..9ca695d 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -13,10 +13,10 @@ export default defineConfig({ index: resolve(__dirname, "src/index.ts"), "sortPackage": resolve(__dirname, "src/sortPackage.ts"), "sortTSConfig": resolve(__dirname, "src/sortTSConfig.ts"), -}, + }, formats: ["cjs"], fileName: (format, entryName) => `${entryName}.js`, -}, + }, rollupOptions: { external: [ "fs", @@ -34,12 +34,12 @@ export default defineConfig({ preserveModulesRoot: "src", entryFileNames: "[name].js", format: "cjs" -}, -}, + }, + }, minify: false, -}, + }, plugins: [transformGenericsPlugin()], esbuild: { target: "node22", -}, + }, }); \ No newline at end of file From 9fb28fd232559368e3698d98baad8e305e4f106f Mon Sep 17 00:00:00 2001 From: Marc Beinder Date: Thu, 22 Jan 2026 10:49:17 -0600 Subject: [PATCH 5/8] Refactor indentation handling in structural formatting rules for improved consistency --- dist/cli.js | 38 +- dist/core/config/ConfigLoader.js | 4 +- src/core/config/ConfigLoader.ts | 14 +- src/core/config/ConfigMerger.ts | 2 +- .../config/__tests__/ConfigLoader.test.ts | 8 +- .../rules/imports/ImportOrganizationRule.ts | 4 +- .../index-generation/IndexGenerationRule.ts | 8 +- .../BlankLineBetweenDeclarationsRule.ts | 20 +- .../BlankLineBetweenStatementTypesRule.ts | 24 +- .../formatters/rules/style/SemicolonRule.ts | 4 +- .../rules/style/StructuralIndentationRule.ts | 377 ++++++++++++------ src/core/pipeline/FormatterPipeline.ts | 66 ++- 12 files changed, 348 insertions(+), 221 deletions(-) diff --git a/dist/cli.js b/dist/cli.js index 0dc9618..bf272df 100644 --- a/dist/cli.js +++ b/dist/cli.js @@ -30,25 +30,6 @@ function _interopNamespaceDefault(e) { const fs__namespace = /* @__PURE__ */ _interopNamespaceDefault(fs); const glob__namespace = /* @__PURE__ */ _interopNamespaceDefault(glob); const path__namespace = /* @__PURE__ */ _interopNamespaceDefault(path); -async function formatSingleFile(filePath, config, dryRun) { - const container = new Container.Container(); - ServiceRegistration.ServiceRegistration.registerServices(container, config); - const pipeline = container.resolve("FormatterPipeline"); - try { - const context = await pipeline.formatFile(filePath, dryRun); - if (context.changed) { - if (dryRun) { - console.info(`Would format: ${filePath}`); - } else { - console.log(`📊 Formatted: ${filePath}`); - } - } else { - console.info(`No changes needed: ${filePath}`); - } - } catch (error) { - console.error(`Error formatting file ${filePath}:`, error.message); - } -} async function formatDirectory(targetDir, config, dryRun) { const container = new Container.Container(); ServiceRegistration.ServiceRegistration.registerServices(container, config); @@ -87,6 +68,25 @@ async function formatDirectory(targetDir, config, dryRun) { console.info(`Formatted ${formattedCount} of ${files.length} files.`); } } +async function formatSingleFile(filePath, config, dryRun) { + const container = new Container.Container(); + ServiceRegistration.ServiceRegistration.registerServices(container, config); + const pipeline = container.resolve("FormatterPipeline"); + try { + const context = await pipeline.formatFile(filePath, dryRun); + if (context.changed) { + if (dryRun) { + console.info(`Would format: ${filePath}`); + } else { + console.log(`📊 Formatted: ${filePath}`); + } + } else { + console.info(`No changes needed: ${filePath}`); + } + } catch (error) { + console.error(`Error formatting file ${filePath}:`, error.message); + } +} function isSupportedFile(filePath) { const supportedExtensions = [".ts", ".tsx", ".js", ".jsx"]; return supportedExtensions.some((ext) => filePath.endsWith(ext)); diff --git a/dist/core/config/ConfigLoader.js b/dist/core/config/ConfigLoader.js index 8fde041..14704df 100644 --- a/dist/core/config/ConfigLoader.js +++ b/dist/core/config/ConfigLoader.js @@ -79,7 +79,7 @@ const config: CoreConfig = { semicolons: "always", indentWidth: 4, lineWidth: 120, - }, + }, // Import organization imports: { @@ -106,7 +106,7 @@ const config: CoreConfig = { // JSON file sorting packageJson: { enabled: true }, tsConfig: { enabled: true }, -}; + }; export default config; `; diff --git a/src/core/config/ConfigLoader.ts b/src/core/config/ConfigLoader.ts index 2ab943f..a2652b5 100644 --- a/src/core/config/ConfigLoader.ts +++ b/src/core/config/ConfigLoader.ts @@ -71,7 +71,7 @@ const config: CoreConfig = { semicolons: "always", indentWidth: 4, lineWidth: 120, - }, + }, // Import organization imports: { @@ -98,7 +98,7 @@ const config: CoreConfig = { // JSON file sorting packageJson: { enabled: true }, tsConfig: { enabled: true }, -}; + }; export default config; `; @@ -114,7 +114,7 @@ export default config; return { size: this.configCache.size, keys: Array.from(this.configCache.keys()) - }; +}; } /** @@ -128,7 +128,7 @@ export default config; } catch { return 0; } - } + } /** * Checks if a tsfmt.config.ts file exists in the project @@ -152,8 +152,8 @@ export default config; target: ts.ScriptTarget.ES2015, esModuleInterop: true, allowSyntheticDefaultImports: true, - }, - }); +}, +}); return result.outputText; } @@ -198,7 +198,7 @@ export default config; } return config; - } catch (error) { + } catch (error) { throw new Error(`Failed to load ${this.CONFIG_FILE_NAME}: ${error instanceof Error ? error.message : String(error)}`); } } diff --git a/src/core/config/ConfigMerger.ts b/src/core/config/ConfigMerger.ts index c4382e6..abf0c87 100644 --- a/src/core/config/ConfigMerger.ts +++ b/src/core/config/ConfigMerger.ts @@ -30,7 +30,7 @@ export class ConfigMerger { result[key] = this.deepMerge(result[key] as any, source[key] as any); } else { result[key] = source[key] as T[Extract]; - } + } } } diff --git a/src/core/config/__tests__/ConfigLoader.test.ts b/src/core/config/__tests__/ConfigLoader.test.ts index e7ebda9..c8489ff 100644 --- a/src/core/config/__tests__/ConfigLoader.test.ts +++ b/src/core/config/__tests__/ConfigLoader.test.ts @@ -174,7 +174,7 @@ describe("ConfigLoader", () => { const result = ConfigLoader.loadConfig(tempDir, false); expect(result.codeStyle?.quoteStyle).toBe("invalid"); - }); +}); }); describe("loadConfigWithoutValidation", () => { @@ -230,8 +230,8 @@ describe("ConfigLoader", () => { const stats = ConfigLoader.getCacheStats(); expect(stats.size).toBe(0); expect(stats.keys).toHaveLength(0); - }); - }); +}); + }); describe("getCacheStats", () => { it("should return cache statistics", () => { @@ -263,7 +263,7 @@ describe("ConfigLoader", () => { expect(() => { ConfigLoader.createSampleConfig(tempDir); }).toThrow("Configuration file already exists"); - }); +}); it("should overwrite existing file when overwrite is true", () => { mockedFs.existsSync.mockReturnValue(true); diff --git a/src/core/formatters/rules/imports/ImportOrganizationRule.ts b/src/core/formatters/rules/imports/ImportOrganizationRule.ts index 1200685..65a22b3 100644 --- a/src/core/formatters/rules/imports/ImportOrganizationRule.ts +++ b/src/core/formatters/rules/imports/ImportOrganizationRule.ts @@ -4,7 +4,7 @@ */ import * as ts from "typescript"; -import {BaseFormattingRule} from "../../BaseFormattingRule"; +import { BaseFormattingRule } from "../../BaseFormattingRule"; interface ImportInfo { @@ -212,7 +212,7 @@ export class ImportOrganizationRule extends BaseFormattingRule { lastGroup !== null && lastGroup !== importInfo.group) { importLines.push(""); - } + } let importText = printer.printNode(ts.EmitHint.Unspecified, importInfo.statement, sourceFile); diff --git a/src/core/formatters/rules/index-generation/IndexGenerationRule.ts b/src/core/formatters/rules/index-generation/IndexGenerationRule.ts index 07238e3..773f18c 100644 --- a/src/core/formatters/rules/index-generation/IndexGenerationRule.ts +++ b/src/core/formatters/rules/index-generation/IndexGenerationRule.ts @@ -209,11 +209,11 @@ ${exports.join("\n")} const moduleName = entry.name.slice(0, -3); // Remove .ts but keep .d modules.push(moduleName); - } + } } return modules.sort(); - } catch (error) { + } catch (error) { console.warn(`Warning: Failed to discover modules in ${srcDir}: ${(error as Error).message}`); return []; @@ -234,7 +234,7 @@ ${exports} } catch (error) { console.warn(`Warning: Failed to write main index file: ${(error as Error).message}`); } - } + } private generateIndexFiles(currentFilePath: string): void { try { @@ -274,7 +274,7 @@ ${exports} const config = this.getIndexGenerationConfig(); if (!config?.enabled || !filePath) { return source; - } +} // This rule operates on the file system, not on individual file content // We'll trigger index generation when processing any file in the project diff --git a/src/core/formatters/rules/spacing/BlankLineBetweenDeclarationsRule.ts b/src/core/formatters/rules/spacing/BlankLineBetweenDeclarationsRule.ts index 3fa8bee..3bb46c7 100644 --- a/src/core/formatters/rules/spacing/BlankLineBetweenDeclarationsRule.ts +++ b/src/core/formatters/rules/spacing/BlankLineBetweenDeclarationsRule.ts @@ -3,18 +3,18 @@ * All Rights Reserved. */ -import {BaseFormattingRule} from "../../BaseFormattingRule"; +import { BaseFormattingRule } from "../../BaseFormattingRule"; /** - * Adds blank lines between declarations with different keywords - * KEY ENHANCEMENT: Works at ALL brace depths (not just top level) - * - * Examples: - * - No blank line between consecutive "const" declarations - * - No blank line between consecutive "export" statements - * - Blank line when keyword changes (const → let, export → const, etc.) - */ +* Adds blank lines between declarations with different keywords +* KEY ENHANCEMENT: Works at ALL brace depths (not just top level) +* +* Examples: +* - No blank line between consecutive "const" declarations +* - No blank line between consecutive "export" statements +* - Blank line when keyword changes (const → let, export → const, etc.) +*/ export class BlankLineBetweenDeclarationsRule extends BaseFormattingRule { readonly name = "BlankLineBetweenDeclarationsRule"; @@ -120,7 +120,7 @@ export class BlankLineBetweenDeclarationsRule extends BaseFormattingRule { declarationKeyword !== lastDeclarationKeyword) { result.push(""); lastNonBlankLineWasDeclarationEnd = false; - } + } } result.push(line); // Track declaration ends BEFORE updating brace depth diff --git a/src/core/formatters/rules/spacing/BlankLineBetweenStatementTypesRule.ts b/src/core/formatters/rules/spacing/BlankLineBetweenStatementTypesRule.ts index 848f19e..70fdeb4 100644 --- a/src/core/formatters/rules/spacing/BlankLineBetweenStatementTypesRule.ts +++ b/src/core/formatters/rules/spacing/BlankLineBetweenStatementTypesRule.ts @@ -3,7 +3,7 @@ * All Rights Reserved. */ -import {BaseFormattingRule} from "../../BaseFormattingRule"; +import { BaseFormattingRule } from "../../BaseFormattingRule"; /** Statement types for categorization */ @@ -17,12 +17,12 @@ enum StatementType { } /** - * Adds blank lines when switching between different statement types - * Examples: - * - Blank line between declarations and control flow - * - Blank line between loops and expressions - * - No blank line within the same statement type - */ +* Adds blank lines when switching between different statement types +* Examples: +* - Blank line between declarations and control flow +* - Blank line between loops and expressions +* - No blank line within the same statement type +*/ export class BlankLineBetweenStatementTypesRule extends BaseFormattingRule { readonly name = "BlankLineBetweenStatementTypesRule"; @@ -40,7 +40,7 @@ export class BlankLineBetweenStatementTypesRule extends BaseFormattingRule { trimmedLine.startsWith("case ") || trimmedLine.startsWith("default:")) { return StatementType.Control; - } + } // Loops if (trimmedLine.startsWith("for ") || @@ -50,7 +50,7 @@ export class BlankLineBetweenStatementTypesRule extends BaseFormattingRule { trimmedLine.startsWith("do ") || trimmedLine.startsWith("do{")) { return StatementType.Loop; - } + } // Exceptions if (trimmedLine.startsWith("try ") || @@ -61,7 +61,7 @@ export class BlankLineBetweenStatementTypesRule extends BaseFormattingRule { trimmedLine.startsWith("finally{") || trimmedLine.startsWith("throw ")) { return StatementType.Exception; - } + } // Declarations if (trimmedLine.startsWith("const ") || @@ -74,7 +74,7 @@ export class BlankLineBetweenStatementTypesRule extends BaseFormattingRule { trimmedLine.startsWith("enum ") || trimmedLine.startsWith("export ")) { return StatementType.Declaration; - } + } // Everything else (expressions, calls, etc.) return StatementType.Expression; } @@ -119,7 +119,7 @@ export class BlankLineBetweenStatementTypesRule extends BaseFormattingRule { result.length > 0 && result[result.length - 1].trim() !== "") { result.push(""); - } + } result.push(line); lastStatementType = currentStatementType; } diff --git a/src/core/formatters/rules/style/SemicolonRule.ts b/src/core/formatters/rules/style/SemicolonRule.ts index d85caf9..ea0fb29 100644 --- a/src/core/formatters/rules/style/SemicolonRule.ts +++ b/src/core/formatters/rules/style/SemicolonRule.ts @@ -4,7 +4,7 @@ */ import * as ts from "typescript"; -import {BaseFormattingRule} from "../../BaseFormattingRule"; +import { BaseFormattingRule } from "../../BaseFormattingRule"; /** Adds or removes semicolons based on configuration using AST */ @@ -48,7 +48,7 @@ export class SemicolonRule extends BaseFormattingRule { // Remove semicolon changes.push({pos: nodeEnd - 1, type: "remove"}); } - } + } // Remove incorrect semicolons from interfaces, classes, and enums if (ts.isInterfaceDeclaration(node) || ts.isClassDeclaration(node) || ts.isEnumDeclaration(node)) { diff --git a/src/core/formatters/rules/style/StructuralIndentationRule.ts b/src/core/formatters/rules/style/StructuralIndentationRule.ts index c5daae1..7e77d12 100644 --- a/src/core/formatters/rules/style/StructuralIndentationRule.ts +++ b/src/core/formatters/rules/style/StructuralIndentationRule.ts @@ -3,141 +3,283 @@ * All Rights Reserved. */ -import * as ts from "typescript"; import { BaseFormattingRule } from "../../BaseFormattingRule"; +/** A fix to apply to a closing bracket */ +interface BracketFix { + position: number; + line: number; + column: number; + targetIndent: number; +} + +/** Information about an opening bracket */ +interface BracketInfo { + char: string; + position: number; + line: number; + lineIndent: number; +} /** * Fixes structural indentation issues where closing braces/brackets * are not properly aligned with their opening statements. * -* This rule ensures that closing braces (}, ]), and parentheses ()) -* are indented to match the indentation level of their opening line, -* not pushed to column 0 or other incorrect positions. +* Uses a stack-based approach to properly match opening and closing +* brackets, ensuring each closing bracket is indented to match its +* corresponding opening bracket's line indentation. */ export class StructuralIndentationRule extends BaseFormattingRule { readonly name = "StructuralIndentationRule"; -private getLineIndentLevel(line: string, indentWidth: number, indentChar: string): number { - const leadingWhitespace = line.match(/^[\t ]*/)?.[0] || ""; +private skipString(source: string, start: number, quote: string): number { + let i = start + 1; + const isTemplate = quote === "`"; - if (indentChar === "\t") { - return (leadingWhitespace.match(/\t/g) || []).length; - } + while (i < source.length) { + const char = source[i]; - const tabCount = (leadingWhitespace.match(/\t/g) || []).length; - const spaceCount = (leadingWhitespace.match(/ /g) || []).length; + // Handle escape sequences + if (char === "\\") { + i += 2; + continue; + } - return tabCount + Math.floor(spaceCount / indentWidth); -} + // Handle template literal expressions ${...} + if (isTemplate && char === "$" && source[i + 1] === "{") { + i += 2; + let braceCount = 1; + while (i < source.length && braceCount > 0) { + if (source[i] === "{") braceCount++; + else if (source[i] === "}") braceCount--; + else if (source[i] === '"' || source[i] === "'" || source[i] === "`") { + i = this.skipString(source, i, source[i]); + continue; + } + i++; + } + continue; + } -private isBlockLikeNode(node: ts.Node): boolean { - return ts.isBlock(node) || - ts.isObjectLiteralExpression(node) || - ts.isArrayLiteralExpression(node) || - ts.isClassDeclaration(node) || - ts.isClassExpression(node) || - ts.isInterfaceDeclaration(node) || - ts.isFunctionDeclaration(node) || - ts.isFunctionExpression(node) || - ts.isArrowFunction(node) || - ts.isMethodDeclaration(node) || - ts.isConstructorDeclaration(node) || - ts.isGetAccessorDeclaration(node) || - ts.isSetAccessorDeclaration(node) || - ts.isModuleDeclaration(node) || - ts.isModuleBlock(node) || - ts.isEnumDeclaration(node) || - ts.isTypeLiteralNode(node) || - ts.isCaseBlock(node) || - ts.isIfStatement(node) || - ts.isForStatement(node) || - ts.isForInStatement(node) || - ts.isForOfStatement(node) || - ts.isWhileStatement(node) || - ts.isDoStatement(node) || - ts.isTryStatement(node) || - ts.isCatchClause(node); -} + // End of string + if (char === quote) { + return i + 1; + } + + i++; + } + + return i; + } -private checkNodeBrackets( - node: ts.Node, - sourceFile: ts.SourceFile, - lines: string[], - fixes: Map, - indentWidth: number, - indentChar: string - ): void { - const nodeStart = node.getStart(sourceFile); - const nodeEnd = node.getEnd(); - const startPos = sourceFile.getLineAndCharacterOfPosition(nodeStart); - const endPos = sourceFile.getLineAndCharacterOfPosition(nodeEnd); - - if (startPos.line === endPos.line) { - return; +private isRegexStart(source: string, index: number): boolean { + // Look backwards to determine if this / starts a regex + let i = index - 1; + while (i >= 0 && (source[i] === " " || source[i] === "\t")) { + i--; } - const startLine = lines[startPos.line]; - const endLine = lines[endPos.line]; - const startIndent = this.getLineIndentLevel(startLine, indentWidth, indentChar); - const endLineContent = endLine.trimStart(); + if (i < 0) return true; - const isClosingBracketLine = /^[}\])]/.test(endLineContent) || - /^[}\])][;,]?\s*$/.test(endLineContent) || - /^[}\])]\s*[;,]?\s*(\/\/.*)?$/.test(endLineContent); + const char = source[i]; + // After these characters, / is likely a regex + const regexPreceders = ["(", ",", "=", ":", "[", "!", "&", "|", "?", "{", "}", ";", "\n", "return", "case"]; - if (!isClosingBracketLine) { - return; + if (regexPreceders.includes(char)) { + return true; } - if (this.isBlockLikeNode(node)) { - const currentEndIndent = this.getLineIndentLevel(endLine, indentWidth, indentChar); + // Check for keywords + const keywords = ["return", "case", "typeof", "void", "delete", "throw", "in", "instanceof"]; + for (const kw of keywords) { + if (index >= kw.length && source.substring(index - kw.length, index).endsWith(kw)) { + return true; + } + } + + return false; + } + +private skipRegex(source: string, start: number): number { + let i = start + 1; + let inCharClass = false; - if (currentEndIndent !== startIndent) { - const existingFix = fixes.get(endPos.line); + while (i < source.length) { + const char = source[i]; - if (existingFix === undefined || startIndent > existingFix) { - fixes.set(endPos.line, startIndent); + if (char === "\\") { + i += 2; + continue; + } + + if (char === "[") { + inCharClass = true; + } else if (char === "]") { + inCharClass = false; + } else if (char === "/" && !inCharClass) { + // Skip flags + i++; + while (i < source.length && /[gimsuy]/.test(source[i])) { + i++; } + return i; + } else if (char === "\n") { + // Regex can't span lines without escaping + return i; } + + i++; } + + return i; } -private analyzeBracketStructure( - sourceFile: ts.SourceFile, - lines: string[], - bracketStack: Array<{ char: string; line: number; indent: number }>, - fixes: Map, - indentWidth: number, - indentChar: string - ): void { - const visit = (node: ts.Node): void => { - this.checkNodeBrackets(node, sourceFile, lines, fixes, indentWidth, indentChar); - ts.forEachChild(node, visit); - }; +private getLineIndentLevel(line: string, indentWidth: number): number { + const leadingWhitespace = line.match(/^[\t ]*/)?.[0] || ""; + + const tabCount = (leadingWhitespace.match(/\t/g) || []).length; + const spaceCount = (leadingWhitespace.match(/ /g) || []).length; - visit(sourceFile); + return tabCount + Math.floor(spaceCount / indentWidth); } -private getScriptKind(filePath?: string): ts.ScriptKind { - if (!filePath) { - return ts.ScriptKind.TS; - } +private startsWithClosingBracket(trimmedLine: string): boolean { + return /^[}\])]/.test(trimmedLine); +} - if (filePath.endsWith(".tsx")) { - return ts.ScriptKind.TSX; - } +private findBracketFixes(source: string, lines: string[], indentWidth: number): BracketFix[] { + const fixes: BracketFix[] = []; + const stack: BracketInfo[] = []; + // Track corrected indentation for lines that have fixes + const lineIndentCorrections = new Map(); + + let i = 0; + let line = 0; + let column = 0; + let lineStart = 0; + + const openBrackets: Record = { + "{": "}", + "[": "]", + "(": ")" + }; - if (filePath.endsWith(".jsx")) { - return ts.ScriptKind.JSX; - } + const closeBrackets: Record = { + "}": "{", + "]": "[", + ")": "(" + }; + + while (i < source.length) { + const char = source[i]; + + // Handle newlines + if (char === "\n") { + line++; + column = 0; + lineStart = i + 1; + i++; + continue; + } + + // Skip string literals + if (char === '"' || char === "'" || char === "`") { + i = this.skipString(source, i, char); + column = i - lineStart; + continue; + } + + // Skip single-line comments + if (char === "/" && source[i + 1] === "/") { + while (i < source.length && source[i] !== "\n") { + i++; + } + continue; + } + + // Skip multi-line comments + if (char === "/" && source[i + 1] === "*") { + i += 2; + while (i < source.length - 1 && !(source[i] === "*" && source[i + 1] === "/")) { + if (source[i] === "\n") { + line++; + lineStart = i + 1; + } + i++; + } + i += 2; + column = i - lineStart; + continue; + } + + // Skip regex literals (basic detection) + if (char === "/" && this.isRegexStart(source, i)) { + i = this.skipRegex(source, i); + column = i - lineStart; + continue; + } - if (filePath.endsWith(".js")) { - return ts.ScriptKind.JS; + // Handle opening brackets + if (openBrackets[char]) { + // Use corrected indent if this line has a fix, otherwise use current indent + const lineIndent = lineIndentCorrections.has(line) + ? lineIndentCorrections.get(line)! + : this.getLineIndentLevel(lines[line], indentWidth); + stack.push({ + char, + position: i, + line, + lineIndent + }); + } + + // Handle closing brackets + if (closeBrackets[char]) { + const expectedOpen = closeBrackets[char]; + // Find matching opening bracket + let matchIndex = -1; + for (let j = stack.length - 1; j >= 0; j--) { + if (stack[j].char === expectedOpen) { + matchIndex = j; + break; + } + } + + if (matchIndex !== -1) { + const openBracket = stack[matchIndex]; + stack.splice(matchIndex, 1); + + // Only fix if the closing bracket is on a different line + if (line !== openBracket.line) { + const currentIndent = this.getLineIndentLevel(lines[line], indentWidth); + const trimmedLine = lines[line].trimStart(); + + // Only fix if this is a line that starts with closing brackets + if (this.startsWithClosingBracket(trimmedLine)) { + if (currentIndent !== openBracket.lineIndent) { + fixes.push({ + position: i, + line, + column, + targetIndent: openBracket.lineIndent + }); + // Record the corrected indent for this line + // so any opening brackets on this line use the corrected value + if (!lineIndentCorrections.has(line)) { + lineIndentCorrections.set(line, openBracket.lineIndent); + } + } + } + } + } + } + + i++; + column++; } - return ts.ScriptKind.TS; -} + return fixes; + } apply(source: string, filePath?: string): string { const config = this.getCodeStyleConfig(); @@ -146,34 +288,35 @@ apply(source: string, filePath?: string): string { } const indentWidth = config.indentWidth; - const indentChar = config.indentStyle === "tab" ? "\t" : " "; const indentUnit = config.indentStyle === "tab" ? "\t" : " ".repeat(indentWidth); - const sourceFile = ts.createSourceFile( - filePath || "temp.ts", - source, - ts.ScriptTarget.Latest, - true, - this.getScriptKind(filePath) - ); - const lines = source.split("\n"); - const bracketStack: Array<{ char: string; line: number; indent: number }> = []; - const fixes: Map = new Map(); - - this.analyzeBracketStructure(sourceFile, lines, bracketStack, fixes, indentWidth, indentChar); + const fixes = this.findBracketFixes(source, lines, indentWidth); - if (fixes.size === 0) { + if (fixes.length === 0) { return source; } - const result: string[] = []; + // Group fixes by line number + const fixesByLine = new Map(); + for (const fix of fixes) { + if (!fixesByLine.has(fix.line)) { + fixesByLine.set(fix.line, []); + } + fixesByLine.get(fix.line)!.push(fix); + } + // Apply fixes line by line + const result: string[] = []; for (let i = 0; i < lines.length; i++) { - if (fixes.has(i)) { - const targetIndent = fixes.get(i)!; + const lineFixes = fixesByLine.get(i); + if (lineFixes && lineFixes.length > 0) { + // For lines with closing brackets, use the indent of the first (outermost) bracket + // Sort by column to get the leftmost bracket first + lineFixes.sort((a, b) => a.column - b.column); + const primaryFix = lineFixes[0]; const trimmedLine = lines[i].trimStart(); - const newIndent = indentUnit.repeat(targetIndent); + const newIndent = indentUnit.repeat(primaryFix.targetIndent); result.push(newIndent + trimmedLine); } else { result.push(lines[i]); @@ -181,5 +324,5 @@ apply(source: string, filePath?: string): string { } return result.join("\n"); -} + } } diff --git a/src/core/pipeline/FormatterPipeline.ts b/src/core/pipeline/FormatterPipeline.ts index 8cb0a11..7fec2c1 100644 --- a/src/core/pipeline/FormatterPipeline.ts +++ b/src/core/pipeline/FormatterPipeline.ts @@ -5,25 +5,9 @@ import * as fs from "fs/promises"; import * as path from "path"; -import {CoreConfig, FormatterOrder} from "../config"; -import {Container} from "../di"; -import { - BlankLineBeforeReturnsRule, - BlankLineBetweenDeclarationsRule, - BlankLineBetweenStatementTypesRule, - BlockSpacingRule, - BracketSpacingRule, - ClassMemberSortingRule, - DocBlockCommentRule, - FileDeclarationSortingRule, - IFormattingRule, - ImportOrganizationRule, - IndentationRule, - IndexGenerationRule, - QuoteStyleRule, - SemicolonRule, - StructuralIndentationRule -} from "../formatters"; +import { CoreConfig, FormatterOrder } from "../config"; +import { Container } from "../di"; +import { BlankLineBeforeReturnsRule, BlankLineBetweenDeclarationsRule, BlankLineBetweenStatementTypesRule, BlockSpacingRule, BracketSpacingRule, ClassMemberSortingRule, DocBlockCommentRule, FileDeclarationSortingRule, IFormattingRule, ImportOrganizationRule, IndentationRule, IndexGenerationRule, QuoteStyleRule, SemicolonRule, StructuralIndentationRule } from "../formatters"; /* @@ -55,9 +39,9 @@ export class FormatterError extends Error { } /** - * Orchestrates the execution of multiple formatters in a defined order. - * Implements fail-fast error handling and supports dry-run mode. - */ +* Orchestrates the execution of multiple formatters in a defined order. +* Implements fail-fast error handling and supports dry-run mode. +*/ export class FormatterPipeline { private formatterOrder: FormatterOrder[]; @@ -91,7 +75,7 @@ export class FormatterPipeline { if (line.includes("FormatterPipeline.extractTypeNameFromStack") || line.includes("FormatterPipeline.addRule")) { continue; - } + } // Find the first external call location const match = line.match(/at\s+.*\s+\((.+):(\d+):(\d+)\)/); @@ -177,12 +161,12 @@ export class FormatterPipeline { } /** - * Format a file using the configured formatters in sequence - * @param filePath - Absolute path to the file to format - * @param dryRun - If true, don't write changes to disk - * @returns Pipeline context with execution details - * @throws FormatterError if any formatter fails (fail-fast) - */ + * Format a file using the configured formatters in sequence + * @param filePath - Absolute path to the file to format + * @param dryRun - If true, don't write changes to disk + * @returns Pipeline context with execution details + * @throws FormatterError if any formatter fails (fail-fast) + */ async formatFile(filePath: string, dryRun = false): Promise { // Read original source const originalSource = await fs.readFile(filePath, "utf-8"); @@ -244,12 +228,12 @@ export class FormatterPipeline { } /** - * Format multiple files in sequence - * @param filePaths - Array of file paths to format - * @param dryRun - If true, don't write changes to disk - * @returns Array of pipeline contexts for each file - * @throws FormatterError if any formatter fails for any file - */ + * Format multiple files in sequence + * @param filePaths - Array of file paths to format + * @param dryRun - If true, don't write changes to disk + * @returns Array of pipeline contexts for each file + * @throws FormatterError if any formatter fails for any file + */ async formatFiles(filePaths: string[], dryRun = false): Promise { const results: PipelineContext[] = []; @@ -263,12 +247,12 @@ export class FormatterPipeline { } /** - * Format all files in a directory recursively - * @param dirPath - Directory path to format - * @param dryRun - If true, don't write changes to disk - * @param extensions - File extensions to include (default: .ts, .tsx, .js, .jsx) - * @returns Array of pipeline contexts for each file - */ + * Format all files in a directory recursively + * @param dirPath - Directory path to format + * @param dryRun - If true, don't write changes to disk + * @param extensions - File extensions to include (default: .ts, .tsx, .js, .jsx) + * @returns Array of pipeline contexts for each file + */ async formatDirectory(dirPath: string, dryRun = false, extensions: string[] = [".ts", ".tsx", ".js", ".jsx"]): Promise { const files = await this.getFilesRecursively(dirPath, extensions); From 7fdf245294ca240d72a7d7d23fae0c0794aba20a Mon Sep 17 00:00:00 2001 From: Marc Beinder Date: Thu, 22 Jan 2026 10:49:25 -0600 Subject: [PATCH 6/8] Add StructuralIndentationRule for fixing brace alignment and indentation --- .../rules/style/StructuralIndentationRule.js | 229 +++++++++ .../StructuralIndentationRule.test.ts | 450 ++++++++++++++++++ 2 files changed, 679 insertions(+) create mode 100644 dist/core/formatters/rules/style/StructuralIndentationRule.js create mode 100644 src/core/formatters/rules/style/__tests__/StructuralIndentationRule.test.ts diff --git a/dist/core/formatters/rules/style/StructuralIndentationRule.js b/dist/core/formatters/rules/style/StructuralIndentationRule.js new file mode 100644 index 0000000..95ddac5 --- /dev/null +++ b/dist/core/formatters/rules/style/StructuralIndentationRule.js @@ -0,0 +1,229 @@ +"use strict"; +Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); +const BaseFormattingRule = require("../../BaseFormattingRule.js"); +class StructuralIndentationRule extends BaseFormattingRule.BaseFormattingRule { + constructor() { + super(...arguments); + this.name = "StructuralIndentationRule"; + } + skipString(source, start, quote) { + let i = start + 1; + const isTemplate = quote === "`"; + while (i < source.length) { + const char = source[i]; + if (char === "\\") { + i += 2; + continue; + } + if (isTemplate && char === "$" && source[i + 1] === "{") { + i += 2; + let braceCount = 1; + while (i < source.length && braceCount > 0) { + if (source[i] === "{") braceCount++; + else if (source[i] === "}") braceCount--; + else if (source[i] === '"' || source[i] === "'" || source[i] === "`") { + i = this.skipString(source, i, source[i]); + continue; + } + i++; + } + continue; + } + if (char === quote) { + return i + 1; + } + i++; + } + return i; + } + isRegexStart(source, index) { + let i = index - 1; + while (i >= 0 && (source[i] === " " || source[i] === " ")) { + i--; + } + if (i < 0) return true; + const char = source[i]; + const regexPreceders = ["(", ",", "=", ":", "[", "!", "&", "|", "?", "{", "}", ";", "\n", "return", "case"]; + if (regexPreceders.includes(char)) { + return true; + } + const keywords = ["return", "case", "typeof", "void", "delete", "throw", "in", "instanceof"]; + for (const kw of keywords) { + if (index >= kw.length && source.substring(index - kw.length, index).endsWith(kw)) { + return true; + } + } + return false; + } + skipRegex(source, start) { + let i = start + 1; + let inCharClass = false; + while (i < source.length) { + const char = source[i]; + if (char === "\\") { + i += 2; + continue; + } + if (char === "[") { + inCharClass = true; + } else if (char === "]") { + inCharClass = false; + } else if (char === "/" && !inCharClass) { + i++; + while (i < source.length && /[gimsuy]/.test(source[i])) { + i++; + } + return i; + } else if (char === "\n") { + return i; + } + i++; + } + return i; + } + getLineIndentLevel(line, indentWidth) { + const leadingWhitespace = line.match(/^[\t ]*/)?.[0] || ""; + const tabCount = (leadingWhitespace.match(/\t/g) || []).length; + const spaceCount = (leadingWhitespace.match(/ /g) || []).length; + return tabCount + Math.floor(spaceCount / indentWidth); + } + startsWithClosingBracket(trimmedLine) { + return /^[}\])]/.test(trimmedLine); + } + findBracketFixes(source, lines, indentWidth) { + const fixes = []; + const stack = []; + const lineIndentCorrections = /* @__PURE__ */ new Map(); + let i = 0; + let line = 0; + let column = 0; + let lineStart = 0; + const openBrackets = { + "{": "}", + "[": "]", + "(": ")" + }; + const closeBrackets = { + "}": "{", + "]": "[", + ")": "(" + }; + while (i < source.length) { + const char = source[i]; + if (char === "\n") { + line++; + column = 0; + lineStart = i + 1; + i++; + continue; + } + if (char === '"' || char === "'" || char === "`") { + i = this.skipString(source, i, char); + column = i - lineStart; + continue; + } + if (char === "/" && source[i + 1] === "/") { + while (i < source.length && source[i] !== "\n") { + i++; + } + continue; + } + if (char === "/" && source[i + 1] === "*") { + i += 2; + while (i < source.length - 1 && !(source[i] === "*" && source[i + 1] === "/")) { + if (source[i] === "\n") { + line++; + lineStart = i + 1; + } + i++; + } + i += 2; + column = i - lineStart; + continue; + } + if (char === "/" && this.isRegexStart(source, i)) { + i = this.skipRegex(source, i); + column = i - lineStart; + continue; + } + if (openBrackets[char]) { + const lineIndent = lineIndentCorrections.has(line) ? lineIndentCorrections.get(line) : this.getLineIndentLevel(lines[line], indentWidth); + stack.push({ + char, + position: i, + line, + lineIndent + }); + } + if (closeBrackets[char]) { + const expectedOpen = closeBrackets[char]; + let matchIndex = -1; + for (let j = stack.length - 1; j >= 0; j--) { + if (stack[j].char === expectedOpen) { + matchIndex = j; + break; + } + } + if (matchIndex !== -1) { + const openBracket = stack[matchIndex]; + stack.splice(matchIndex, 1); + if (line !== openBracket.line) { + const currentIndent = this.getLineIndentLevel(lines[line], indentWidth); + const trimmedLine = lines[line].trimStart(); + if (this.startsWithClosingBracket(trimmedLine)) { + if (currentIndent !== openBracket.lineIndent) { + fixes.push({ + position: i, + line, + column, + targetIndent: openBracket.lineIndent + }); + if (!lineIndentCorrections.has(line)) { + lineIndentCorrections.set(line, openBracket.lineIndent); + } + } + } + } + } + } + i++; + column++; + } + return fixes; + } + apply(source, filePath) { + const config = this.getCodeStyleConfig(); + if (!config?.indentStyle || !config.indentWidth) { + return source; + } + const indentWidth = config.indentWidth; + const indentUnit = config.indentStyle === "tab" ? " " : " ".repeat(indentWidth); + const lines = source.split("\n"); + const fixes = this.findBracketFixes(source, lines, indentWidth); + if (fixes.length === 0) { + return source; + } + const fixesByLine = /* @__PURE__ */ new Map(); + for (const fix of fixes) { + if (!fixesByLine.has(fix.line)) { + fixesByLine.set(fix.line, []); + } + fixesByLine.get(fix.line).push(fix); + } + const result = []; + for (let i = 0; i < lines.length; i++) { + const lineFixes = fixesByLine.get(i); + if (lineFixes && lineFixes.length > 0) { + lineFixes.sort((a, b) => a.column - b.column); + const primaryFix = lineFixes[0]; + const trimmedLine = lines[i].trimStart(); + const newIndent = indentUnit.repeat(primaryFix.targetIndent); + result.push(newIndent + trimmedLine); + } else { + result.push(lines[i]); + } + } + return result.join("\n"); + } +} +exports.StructuralIndentationRule = StructuralIndentationRule; diff --git a/src/core/formatters/rules/style/__tests__/StructuralIndentationRule.test.ts b/src/core/formatters/rules/style/__tests__/StructuralIndentationRule.test.ts new file mode 100644 index 0000000..280e666 --- /dev/null +++ b/src/core/formatters/rules/style/__tests__/StructuralIndentationRule.test.ts @@ -0,0 +1,450 @@ +/* +* Copyright (c) 2026. Encore Digital Group. +* All Rights Reserved. +*/ + +import { CoreConfig } from "../../../../config"; +import { Container } from "../../../../di"; +import { StructuralIndentationRule } from "../StructuralIndentationRule"; + + +describe("StructuralIndentationRule", () => { + let rule: StructuralIndentationRule; + let container: Container; + let config: CoreConfig; + + beforeEach(() => { + container = new Container(); + config = { + codeStyle: { + enabled: true, + indentStyle: "space", + indentWidth: 4 + } + } as CoreConfig; + container.singleton(config); + rule = new StructuralIndentationRule(container); + }); + + describe("apply", () => { + it("should fix a single misaligned closing brace", () => { + const input = `function test() { + const obj = { + a: 1 +}; + return obj; +}`; + + const expected = `function test() { + const obj = { + a: 1 + }; + return obj; +}`; + + const result = rule.apply(input); + expect(result).toBe(expected); + }); + + it("should fix nested misaligned closing braces", () => { + const input = `function test() { + const outer = { + inner: { + value: 1 +} +}; + return outer; +}`; + + const expected = `function test() { + const outer = { + inner: { + value: 1 + } + }; + return outer; +}`; + + const result = rule.apply(input); + expect(result).toBe(expected); + }); + + it("should fix deeply nested structures", () => { + const input = `const obj = { + a: { + b: { + c: { + d: 1 +} +} +} +};`; + + const expected = `const obj = { + a: { + b: { + c: { + d: 1 + } + } + } +};`; + + const result = rule.apply(input); + expect(result).toBe(expected); + }); + + it("should fix misaligned array brackets", () => { + const input = `const arr = [ + 1, + 2, + 3 +];`; + + const expected = `const arr = [ + 1, + 2, + 3 +];`; + + const result = rule.apply(input); + expect(result).toBe(expected); + }); + + it("should fix nested array brackets", () => { + const input = `const matrix = [ + [1, 2], + [3, 4] +];`; + + const expected = `const matrix = [ + [1, 2], + [3, 4] +];`; + + const result = rule.apply(input); + expect(result).toBe(expected); + }); + + it("should fix mixed bracket types", () => { + const input = `function test() { + const obj = { + arr: [ + 1, + 2 +] +}; + return obj; +}`; + + const expected = `function test() { + const obj = { + arr: [ + 1, + 2 + ] + }; + return obj; +}`; + + const result = rule.apply(input); + expect(result).toBe(expected); + }); + + it("should not modify correctly indented code", () => { + const input = `function test() { + const obj = { + a: 1, + b: 2 + }; + return obj; +}`; + + const result = rule.apply(input); + expect(result).toBe(input); + }); + + it("should ignore braces inside string literals", () => { + const input = `const str = "{ not a real brace }"; +const obj = { + value: 1 +};`; + + const expected = `const str = "{ not a real brace }"; +const obj = { + value: 1 +};`; + + const result = rule.apply(input); + expect(result).toBe(expected); + }); + + it("should ignore braces inside template literals", () => { + const input = `const template = \`{ + fake brace +}\`; +const obj = { + value: 1 +};`; + + const expected = `const template = \`{ + fake brace +}\`; +const obj = { + value: 1 +};`; + + const result = rule.apply(input); + expect(result).toBe(expected); + }); + + it("should ignore braces inside single-line comments", () => { + const input = `// { comment brace } +const obj = { + value: 1 +};`; + + const expected = `// { comment brace } +const obj = { + value: 1 +};`; + + const result = rule.apply(input); + expect(result).toBe(expected); + }); + + it("should ignore braces inside multi-line comments", () => { + const input = `/* + * { comment brace } + */ +const obj = { + value: 1 +};`; + + const expected = `/* + * { comment brace } + */ +const obj = { + value: 1 +};`; + + const result = rule.apply(input); + expect(result).toBe(expected); + }); + + it("should handle class declarations", () => { + const input = `class Example { + method() { + return { + a: 1 +}; +} +}`; + + const expected = `class Example { + method() { + return { + a: 1 + }; + } +}`; + + const result = rule.apply(input); + expect(result).toBe(expected); + }); + + it("should handle if statements", () => { + const input = `function test() { + if (condition) { + doSomething(); +} +}`; + + const expected = `function test() { + if (condition) { + doSomething(); + } +}`; + + const result = rule.apply(input); + expect(result).toBe(expected); + }); + + it("should handle arrow functions", () => { + const input = `const fn = () => { + return { + value: 1 +}; +};`; + + const expected = `const fn = () => { + return { + value: 1 + }; +};`; + + const result = rule.apply(input); + expect(result).toBe(expected); + }); + + it("should handle empty source code", () => { + const result = rule.apply(""); + expect(result).toBe(""); + }); + + it("should handle source code without braces", () => { + const input = `const x = 1; +const y = 2;`; + + const result = rule.apply(input); + expect(result).toBe(input); + }); + + it("should handle single-line objects (no change needed)", () => { + const input = `const obj = { a: 1, b: 2 };`; + + const result = rule.apply(input); + expect(result).toBe(input); + }); + + it("should handle multiple closing brackets on same line at column 0", () => { + const input = `const obj = { + nested: { + value: 1 +}};`; + + const expected = `const obj = { + nested: { + value: 1 + }};`; + + const result = rule.apply(input); + expect(result).toBe(expected); + }); + + it("should preserve trailing content after closing braces", () => { + const input = `const obj = { + a: 1 +}; // trailing comment`; + + const expected = `const obj = { + a: 1 +}; // trailing comment`; + + const result = rule.apply(input); + expect(result).toBe(expected); + }); + + it("should handle interface declarations", () => { + const input = `interface Example { + prop: { + nested: string; +}; +}`; + + const expected = `interface Example { + prop: { + nested: string; + }; +}`; + + const result = rule.apply(input); + expect(result).toBe(expected); + }); + + it("should handle try-catch blocks", () => { + const input = `function test() { + try { + doSomething(); +} catch (e) { + handleError(); +} +}`; + + const expected = `function test() { + try { + doSomething(); + } catch (e) { + handleError(); + } +}`; + + const result = rule.apply(input); + expect(result).toBe(expected); + }); + }); + + describe("with tab indentation", () => { + beforeEach(() => { + config = { + codeStyle: { + enabled: true, + indentStyle: "tab", + indentWidth: 4 + } + } as CoreConfig; + container = new Container(); + container.singleton(config); + rule = new StructuralIndentationRule(container); + }); + + it("should fix misaligned braces using tabs", () => { + const input = `function test() { +\tconst obj = { +\t\ta: 1 +}; +\treturn obj; +}`; + + const expected = `function test() { +\tconst obj = { +\t\ta: 1 +\t}; +\treturn obj; +}`; + + const result = rule.apply(input); + expect(result).toBe(expected); + }); + }); + + describe("disabled config", () => { + it("should return source unchanged when indentStyle is not set", () => { + config = { + codeStyle: { + enabled: true, + indentWidth: 4 + } + } as CoreConfig; + container = new Container(); + container.singleton(config); + rule = new StructuralIndentationRule(container); + + const input = `const obj = { + a: 1 +};`; + + const result = rule.apply(input); + expect(result).toBe(input); + }); + + it("should return source unchanged when indentWidth is not set", () => { + config = { + codeStyle: { + enabled: true, + indentStyle: "space" + } + } as CoreConfig; + container = new Container(); + container.singleton(config); + rule = new StructuralIndentationRule(container); + + const input = `const obj = { + a: 1 +};`; + + const result = rule.apply(input); + expect(result).toBe(input); + }); + }); +}); From 89c1e0d890217820b9504c71df75c40b05a87f3d Mon Sep 17 00:00:00 2001 From: Marc Beinder Date: Fri, 23 Jan 2026 11:28:24 -0600 Subject: [PATCH 7/8] Fix brace alignment and indentation issues in multiple files --- .../rules/style/StructuralIndentationRule.js | 35 +- dist/core/pipeline/FormatterPipeline.js | 15 + src/build-plugins/index.ts | 3 +- .../index-generation/IndexGenerationRule.ts | 8 +- .../BlankLineBetweenDeclarationsRule.ts | 2 +- .../rules/style/StructuralIndentationRule.ts | 53 +- .../StructuralIndentationRule.test.ts | 555 ++++++++++-------- src/core/pipeline/FormatterPipeline.ts | 26 + .../__tests__/FormatterPipeline.test.ts | 76 +++ 9 files changed, 497 insertions(+), 276 deletions(-) diff --git a/dist/core/formatters/rules/style/StructuralIndentationRule.js b/dist/core/formatters/rules/style/StructuralIndentationRule.js index 95ddac5..81d84c3 100644 --- a/dist/core/formatters/rules/style/StructuralIndentationRule.js +++ b/dist/core/formatters/rules/style/StructuralIndentationRule.js @@ -8,9 +8,15 @@ class StructuralIndentationRule extends BaseFormattingRule.BaseFormattingRule { } skipString(source, start, quote) { let i = start + 1; + let newlines = 0; const isTemplate = quote === "`"; while (i < source.length) { const char = source[i]; + if (char === "\n") { + newlines++; + i++; + continue; + } if (char === "\\") { i += 2; continue; @@ -19,10 +25,16 @@ class StructuralIndentationRule extends BaseFormattingRule.BaseFormattingRule { i += 2; let braceCount = 1; while (i < source.length && braceCount > 0) { - if (source[i] === "{") braceCount++; - else if (source[i] === "}") braceCount--; - else if (source[i] === '"' || source[i] === "'" || source[i] === "`") { - i = this.skipString(source, i, source[i]); + if (source[i] === "\n") { + newlines++; + } else if (source[i] === "{") { + braceCount++; + } else if (source[i] === "}") { + braceCount--; + } else if (source[i] === '"' || source[i] === "'" || source[i] === "`") { + const result = this.skipString(source, i, source[i]); + i = result.pos; + newlines += result.newlines; continue; } i++; @@ -30,11 +42,11 @@ class StructuralIndentationRule extends BaseFormattingRule.BaseFormattingRule { continue; } if (char === quote) { - return i + 1; + return { pos: i + 1, newlines }; } i++; } - return i; + return { pos: i, newlines }; } isRegexStart(source, index) { let i = index - 1; @@ -118,7 +130,16 @@ class StructuralIndentationRule extends BaseFormattingRule.BaseFormattingRule { continue; } if (char === '"' || char === "'" || char === "`") { - i = this.skipString(source, i, char); + const result = this.skipString(source, i, char); + i = result.pos; + line += result.newlines; + if (result.newlines > 0) { + let lastNewline = i - 1; + while (lastNewline >= 0 && source[lastNewline] !== "\n") { + lastNewline--; + } + lineStart = lastNewline + 1; + } column = i - lineStart; continue; } diff --git a/dist/core/pipeline/FormatterPipeline.js b/dist/core/pipeline/FormatterPipeline.js index a2eb24e..0aade64 100644 --- a/dist/core/pipeline/FormatterPipeline.js +++ b/dist/core/pipeline/FormatterPipeline.js @@ -114,6 +114,11 @@ class FormatterPipeline { } return files; } + /** Check if source code contains a tsfmt-ignore directive */ + shouldIgnoreFile(source) { + const header = source.slice(0, 1e3); + return /(?:\/\/|\/\*|\*)\s*tsfmt-ignore|^\s*tsfmt-ignore\s*$/m.test(header); + } /** * Format a file using the configured formatters in sequence * @param filePath - Absolute path to the file to format @@ -123,6 +128,16 @@ class FormatterPipeline { */ async formatFile(filePath, dryRun = false) { const originalSource = await fs__namespace.readFile(filePath, "utf-8"); + if (this.shouldIgnoreFile(originalSource)) { + return { + filePath, + originalSource, + currentSource: originalSource, + executions: [], + changed: false, + dryRun + }; + } const context = { filePath, originalSource, diff --git a/src/build-plugins/index.ts b/src/build-plugins/index.ts index f6daf44..43e4b1c 100644 --- a/src/build-plugins/index.ts +++ b/src/build-plugins/index.ts @@ -1,5 +1,4 @@ - // Auto-generated exports - do not edit manually // Run tsfmt to regenerate -export * from "./transformGenericsPlugin" +export * from "./transformGenericsPlugin"; diff --git a/src/core/formatters/rules/index-generation/IndexGenerationRule.ts b/src/core/formatters/rules/index-generation/IndexGenerationRule.ts index 773f18c..07238e3 100644 --- a/src/core/formatters/rules/index-generation/IndexGenerationRule.ts +++ b/src/core/formatters/rules/index-generation/IndexGenerationRule.ts @@ -209,11 +209,11 @@ ${exports.join("\n")} const moduleName = entry.name.slice(0, -3); // Remove .ts but keep .d modules.push(moduleName); - } + } } return modules.sort(); - } catch (error) { + } catch (error) { console.warn(`Warning: Failed to discover modules in ${srcDir}: ${(error as Error).message}`); return []; @@ -234,7 +234,7 @@ ${exports} } catch (error) { console.warn(`Warning: Failed to write main index file: ${(error as Error).message}`); } - } + } private generateIndexFiles(currentFilePath: string): void { try { @@ -274,7 +274,7 @@ ${exports} const config = this.getIndexGenerationConfig(); if (!config?.enabled || !filePath) { return source; -} + } // This rule operates on the file system, not on individual file content // We'll trigger index generation when processing any file in the project diff --git a/src/core/formatters/rules/spacing/BlankLineBetweenDeclarationsRule.ts b/src/core/formatters/rules/spacing/BlankLineBetweenDeclarationsRule.ts index 3bb46c7..4976fbe 100644 --- a/src/core/formatters/rules/spacing/BlankLineBetweenDeclarationsRule.ts +++ b/src/core/formatters/rules/spacing/BlankLineBetweenDeclarationsRule.ts @@ -110,7 +110,7 @@ export class BlankLineBetweenDeclarationsRule extends BaseFormattingRule { result[result.length - 1].trim() !== "") { result.push(""); lastNonBlankLineWasDeclarationEnd = false; - } + } // Add blank line before declaration starts ONLY if the keyword is different else if (isDeclarationStart && diff --git a/src/core/formatters/rules/style/StructuralIndentationRule.ts b/src/core/formatters/rules/style/StructuralIndentationRule.ts index 7e77d12..df616f1 100644 --- a/src/core/formatters/rules/style/StructuralIndentationRule.ts +++ b/src/core/formatters/rules/style/StructuralIndentationRule.ts @@ -5,6 +5,7 @@ import { BaseFormattingRule } from "../../BaseFormattingRule"; + /** A fix to apply to a closing bracket */ interface BracketFix { position: number; @@ -32,13 +33,21 @@ interface BracketInfo { export class StructuralIndentationRule extends BaseFormattingRule { readonly name = "StructuralIndentationRule"; -private skipString(source: string, start: number, quote: string): number { +private skipString(source: string, start: number, quote: string): { pos: number; newlines: number } { let i = start + 1; + let newlines = 0; const isTemplate = quote === "`"; while (i < source.length) { const char = source[i]; + // Count newlines + if (char === "\n") { + newlines++; + i++; + continue; + } + // Handle escape sequences if (char === "\\") { i += 2; @@ -50,10 +59,16 @@ private skipString(source: string, start: number, quote: string): number { i += 2; let braceCount = 1; while (i < source.length && braceCount > 0) { - if (source[i] === "{") braceCount++; - else if (source[i] === "}") braceCount--; - else if (source[i] === '"' || source[i] === "'" || source[i] === "`") { - i = this.skipString(source, i, source[i]); + if (source[i] === "\n") { + newlines++; + } else if (source[i] === "{") { + braceCount++; + } else if (source[i] === "}") { + braceCount--; + } else if (source[i] === '"' || source[i] === "'" || source[i] === "`") { + const result = this.skipString(source, i, source[i]); + i = result.pos; + newlines += result.newlines; continue; } i++; @@ -63,14 +78,14 @@ private skipString(source: string, start: number, quote: string): number { // End of string if (char === quote) { - return i + 1; + return { pos: i + 1, newlines }; } i++; } - return i; - } + return { pos: i, newlines }; +} private isRegexStart(source: string, index: number): boolean { // Look backwards to determine if this / starts a regex @@ -98,7 +113,7 @@ private isRegexStart(source: string, index: number): boolean { } return false; - } +} private skipRegex(source: string, start: number): number { let i = start + 1; @@ -132,7 +147,7 @@ private skipRegex(source: string, start: number): number { } return i; - } +} private getLineIndentLevel(line: string, indentWidth: number): number { const leadingWhitespace = line.match(/^[\t ]*/)?.[0] || ""; @@ -141,7 +156,7 @@ private getLineIndentLevel(line: string, indentWidth: number): number { const spaceCount = (leadingWhitespace.match(/ /g) || []).length; return tabCount + Math.floor(spaceCount / indentWidth); - } +} private startsWithClosingBracket(trimmedLine: string): boolean { return /^[}\])]/.test(trimmedLine); @@ -184,7 +199,17 @@ private findBracketFixes(source: string, lines: string[], indentWidth: number): // Skip string literals if (char === '"' || char === "'" || char === "`") { - i = this.skipString(source, i, char); + const result = this.skipString(source, i, char); + i = result.pos; + line += result.newlines; + if (result.newlines > 0) { + // Find the last newline position to update lineStart + let lastNewline = i - 1; + while (lastNewline >= 0 && source[lastNewline] !== "\n") { + lastNewline--; + } + lineStart = lastNewline + 1; + } column = i - lineStart; continue; } @@ -279,7 +304,7 @@ private findBracketFixes(source: string, lines: string[], indentWidth: number): } return fixes; - } +} apply(source: string, filePath?: string): string { const config = this.getCodeStyleConfig(); @@ -324,5 +349,5 @@ apply(source: string, filePath?: string): string { } return result.join("\n"); - } +} } diff --git a/src/core/formatters/rules/style/__tests__/StructuralIndentationRule.test.ts b/src/core/formatters/rules/style/__tests__/StructuralIndentationRule.test.ts index 280e666..86c0e16 100644 --- a/src/core/formatters/rules/style/__tests__/StructuralIndentationRule.test.ts +++ b/src/core/formatters/rules/style/__tests__/StructuralIndentationRule.test.ts @@ -3,6 +3,8 @@ * All Rights Reserved. */ +// tsfmt-ignore + import { CoreConfig } from "../../../../config"; import { Container } from "../../../../di"; import { StructuralIndentationRule } from "../StructuralIndentationRule"; @@ -28,260 +30,269 @@ describe("StructuralIndentationRule", () => { describe("apply", () => { it("should fix a single misaligned closing brace", () => { - const input = `function test() { - const obj = { - a: 1 -}; - return obj; -}`; - - const expected = `function test() { - const obj = { - a: 1 - }; - return obj; -}`; + // Input has }; at column 0, should be at 4 spaces + const input = [ + "function test() {", + " const obj = {", + " a: 1", + "};", // Wrong: at column 0 + " return obj;", + "}" + ].join("\n"); + + const expected = [ + "function test() {", + " const obj = {", + " a: 1", + " };", // Correct: at 4 spaces + " return obj;", + "}" + ].join("\n"); const result = rule.apply(input); expect(result).toBe(expected); }); it("should fix nested misaligned closing braces", () => { - const input = `function test() { - const outer = { - inner: { - value: 1 -} -}; - return outer; -}`; - - const expected = `function test() { - const outer = { - inner: { - value: 1 - } - }; - return outer; -}`; + const input = [ + "function test() {", + " const outer = {", + " inner: {", + " value: 1", + "}", // Wrong + "};", // Wrong + " return outer;", + "}" + ].join("\n"); + + const expected = [ + "function test() {", + " const outer = {", + " inner: {", + " value: 1", + " }", // Fixed to 8 spaces + " };", // Fixed to 4 spaces + " return outer;", + "}" + ].join("\n"); const result = rule.apply(input); expect(result).toBe(expected); }); it("should fix deeply nested structures", () => { - const input = `const obj = { - a: { - b: { - c: { - d: 1 -} -} -} -};`; - - const expected = `const obj = { - a: { - b: { - c: { - d: 1 - } - } - } -};`; + const input = [ + "const obj = {", + " a: {", + " b: {", + " c: {", + " d: 1", + "}", // Wrong + "}", // Wrong + "}", // Wrong + "};" + ].join("\n"); + + const expected = [ + "const obj = {", + " a: {", + " b: {", + " c: {", + " d: 1", + " }", // 12 spaces + " }", // 8 spaces + " }", // 4 spaces + "};" + ].join("\n"); const result = rule.apply(input); expect(result).toBe(expected); }); it("should fix misaligned array brackets", () => { - const input = `const arr = [ - 1, - 2, - 3 -];`; - - const expected = `const arr = [ - 1, - 2, - 3 -];`; - + const input = [ + "const arr = [", + " 1,", + " 2,", + " 3", + "];" // Already correct at column 0 + ].join("\n"); + + // No change expected - array at top level, ] at column 0 is correct const result = rule.apply(input); - expect(result).toBe(expected); + expect(result).toBe(input); }); it("should fix nested array brackets", () => { - const input = `const matrix = [ - [1, 2], - [3, 4] -];`; - - const expected = `const matrix = [ - [1, 2], - [3, 4] -];`; + const input = [ + "const matrix = [", + " [1, 2],", + " [3, 4]", + "];" // Already correct + ].join("\n"); const result = rule.apply(input); - expect(result).toBe(expected); + expect(result).toBe(input); }); it("should fix mixed bracket types", () => { - const input = `function test() { - const obj = { - arr: [ - 1, - 2 -] -}; - return obj; -}`; - - const expected = `function test() { - const obj = { - arr: [ - 1, - 2 - ] - }; - return obj; -}`; + const input = [ + "function test() {", + " const obj = {", + " arr: [", + " 1,", + " 2", + "]", // Wrong + "};", // Wrong + " return obj;", + "}" + ].join("\n"); + + const expected = [ + "function test() {", + " const obj = {", + " arr: [", + " 1,", + " 2", + " ]", // 8 spaces + " };", // 4 spaces + " return obj;", + "}" + ].join("\n"); const result = rule.apply(input); expect(result).toBe(expected); }); it("should not modify correctly indented code", () => { - const input = `function test() { - const obj = { - a: 1, - b: 2 - }; - return obj; -}`; + const input = [ + "function test() {", + " const obj = {", + " a: 1,", + " b: 2", + " };", + " return obj;", + "}" + ].join("\n"); const result = rule.apply(input); expect(result).toBe(input); }); it("should ignore braces inside string literals", () => { - const input = `const str = "{ not a real brace }"; -const obj = { - value: 1 -};`; - - const expected = `const str = "{ not a real brace }"; -const obj = { - value: 1 -};`; + const input = [ + 'const str = "{ not a real brace }";', + "const obj = {", + " value: 1", + "};" + ].join("\n"); const result = rule.apply(input); - expect(result).toBe(expected); + expect(result).toBe(input); }); it("should ignore braces inside template literals", () => { - const input = `const template = \`{ - fake brace -}\`; -const obj = { - value: 1 -};`; - - const expected = `const template = \`{ - fake brace -}\`; -const obj = { - value: 1 -};`; + const input = [ + "const template = `{", + " fake brace", + "}`;", + "const obj = {", + " value: 1", + "};" + ].join("\n"); const result = rule.apply(input); - expect(result).toBe(expected); + expect(result).toBe(input); }); it("should ignore braces inside single-line comments", () => { - const input = `// { comment brace } -const obj = { - value: 1 -};`; - - const expected = `// { comment brace } -const obj = { - value: 1 -};`; + const input = [ + "// { comment brace }", + "const obj = {", + " value: 1", + "};" + ].join("\n"); const result = rule.apply(input); - expect(result).toBe(expected); + expect(result).toBe(input); }); it("should ignore braces inside multi-line comments", () => { - const input = `/* - * { comment brace } - */ -const obj = { - value: 1 -};`; - - const expected = `/* - * { comment brace } - */ -const obj = { - value: 1 -};`; + const input = [ + "/*", + " * { comment brace }", + " */", + "const obj = {", + " value: 1", + "};" + ].join("\n"); const result = rule.apply(input); - expect(result).toBe(expected); + expect(result).toBe(input); }); it("should handle class declarations", () => { - const input = `class Example { - method() { - return { - a: 1 -}; -} -}`; - - const expected = `class Example { - method() { - return { - a: 1 - }; - } -}`; + const input = [ + "class Example {", + " method() {", + " return {", + " a: 1", + "};", // Wrong: should be 8 spaces + "}", // Wrong: should be 4 spaces + "}" + ].join("\n"); + + const expected = [ + "class Example {", + " method() {", + " return {", + " a: 1", + " };", // 8 spaces + " }", // 4 spaces + "}" + ].join("\n"); const result = rule.apply(input); expect(result).toBe(expected); }); it("should handle if statements", () => { - const input = `function test() { - if (condition) { - doSomething(); -} -}`; - - const expected = `function test() { - if (condition) { - doSomething(); - } -}`; + const input = [ + "function test() {", + " if (condition) {", + " doSomething();", + "}", // Wrong + "}" + ].join("\n"); + + const expected = [ + "function test() {", + " if (condition) {", + " doSomething();", + " }", // 4 spaces + "}" + ].join("\n"); const result = rule.apply(input); expect(result).toBe(expected); }); it("should handle arrow functions", () => { - const input = `const fn = () => { - return { - value: 1 -}; -};`; - - const expected = `const fn = () => { - return { - value: 1 - }; -};`; + const input = [ + "const fn = () => {", + " return {", + " value: 1", + "};", // Wrong + "};" + ].join("\n"); + + const expected = [ + "const fn = () => {", + " return {", + " value: 1", + " };", // 4 spaces + "};" + ].join("\n"); const result = rule.apply(input); expect(result).toBe(expected); @@ -293,81 +304,121 @@ const obj = { }); it("should handle source code without braces", () => { - const input = `const x = 1; -const y = 2;`; + const input = [ + "const x = 1;", + "const y = 2;" + ].join("\n"); const result = rule.apply(input); expect(result).toBe(input); }); it("should handle single-line objects (no change needed)", () => { - const input = `const obj = { a: 1, b: 2 };`; + const input = "const obj = { a: 1, b: 2 };"; const result = rule.apply(input); expect(result).toBe(input); }); it("should handle multiple closing brackets on same line at column 0", () => { - const input = `const obj = { - nested: { - value: 1 -}};`; - - const expected = `const obj = { - nested: { - value: 1 - }};`; + const input = [ + "const obj = {", + " nested: {", + " value: 1", + "}};", // Two brackets at wrong position + ].join("\n"); + + const expected = [ + "const obj = {", + " nested: {", + " value: 1", + " }};", // Fixed: leftmost bracket determines indent (4 spaces) + ].join("\n"); const result = rule.apply(input); expect(result).toBe(expected); }); it("should preserve trailing content after closing braces", () => { - const input = `const obj = { - a: 1 -}; // trailing comment`; - - const expected = `const obj = { - a: 1 -}; // trailing comment`; + const input = [ + "const obj = {", + " a: 1", + "}; // trailing comment" + ].join("\n"); const result = rule.apply(input); - expect(result).toBe(expected); + expect(result).toBe(input); }); it("should handle interface declarations", () => { - const input = `interface Example { - prop: { - nested: string; -}; -}`; - - const expected = `interface Example { - prop: { - nested: string; - }; -}`; + const input = [ + "interface Example {", + " prop: {", + " nested: string;", + "};", // Wrong + "}" + ].join("\n"); + + const expected = [ + "interface Example {", + " prop: {", + " nested: string;", + " };", // 4 spaces + "}" + ].join("\n"); const result = rule.apply(input); expect(result).toBe(expected); }); it("should handle try-catch blocks", () => { - const input = `function test() { - try { - doSomething(); -} catch (e) { - handleError(); -} -}`; - - const expected = `function test() { - try { - doSomething(); - } catch (e) { - handleError(); - } -}`; + const input = [ + "function test() {", + " try {", + " doSomething();", + "} catch (e) {", // Wrong: } should be at 4 spaces + " handleError();", + "}", // Wrong + "}" + ].join("\n"); + + const expected = [ + "function test() {", + " try {", + " doSomething();", + " } catch (e) {", // 4 spaces (matches try) + " handleError();", + " }", // 4 spaces (matches catch) + "}" + ].join("\n"); + + const result = rule.apply(input); + expect(result).toBe(expected); + }); + + it("should handle multi-line template literals correctly", () => { + // This tests that the rule handles newlines inside template literals + const input = [ + "function log() {", + " const msg = `Line 1", + "Line 2", + "Line 3`;", + " const obj = {", + " a: 1", + "};", // Wrong + "}" + ].join("\n"); + + const expected = [ + "function log() {", + " const msg = `Line 1", + "Line 2", + "Line 3`;", + " const obj = {", + " a: 1", + " };", // 4 spaces + "}" + ].join("\n"); const result = rule.apply(input); expect(result).toBe(expected); @@ -389,19 +440,23 @@ const y = 2;`; }); it("should fix misaligned braces using tabs", () => { - const input = `function test() { -\tconst obj = { -\t\ta: 1 -}; -\treturn obj; -}`; - - const expected = `function test() { -\tconst obj = { -\t\ta: 1 -\t}; -\treturn obj; -}`; + const input = [ + "function test() {", + "\tconst obj = {", + "\t\ta: 1", + "};", // Wrong + "\treturn obj;", + "}" + ].join("\n"); + + const expected = [ + "function test() {", + "\tconst obj = {", + "\t\ta: 1", + "\t};", // 1 tab + "\treturn obj;", + "}" + ].join("\n"); const result = rule.apply(input); expect(result).toBe(expected); @@ -420,9 +475,11 @@ const y = 2;`; container.singleton(config); rule = new StructuralIndentationRule(container); - const input = `const obj = { - a: 1 -};`; + const input = [ + "const obj = {", + " a: 1", + "};" + ].join("\n"); const result = rule.apply(input); expect(result).toBe(input); @@ -439,9 +496,11 @@ const y = 2;`; container.singleton(config); rule = new StructuralIndentationRule(container); - const input = `const obj = { - a: 1 -};`; + const input = [ + "const obj = {", + " a: 1", + "};" + ].join("\n"); const result = rule.apply(input); expect(result).toBe(input); diff --git a/src/core/pipeline/FormatterPipeline.ts b/src/core/pipeline/FormatterPipeline.ts index 7fec2c1..5864e8c 100644 --- a/src/core/pipeline/FormatterPipeline.ts +++ b/src/core/pipeline/FormatterPipeline.ts @@ -160,6 +160,20 @@ export class FormatterPipeline { return files; } + /** Check if source code contains a tsfmt-ignore directive */ + private shouldIgnoreFile(source: string): boolean { + // Check the first 1000 characters for the ignore directive + // This covers file headers, copyright notices, and initial comments + const header = source.slice(0, 1000); + + // Match tsfmt-ignore in various comment formats: + // - // tsfmt-ignore (single-line comment) + // - /* tsfmt-ignore */ (inline block comment) + // - * tsfmt-ignore (inside multi-line block comment) + // - tsfmt-ignore on its own line in a block comment + return /(?:\/\/|\/\*|\*)\s*tsfmt-ignore|^\s*tsfmt-ignore\s*$/m.test(header); + } + /** * Format a file using the configured formatters in sequence * @param filePath - Absolute path to the file to format @@ -171,6 +185,18 @@ export class FormatterPipeline { // Read original source const originalSource = await fs.readFile(filePath, "utf-8"); + // Check for tsfmt-ignore directive + if (this.shouldIgnoreFile(originalSource)) { + return { + filePath, + originalSource, + currentSource: originalSource, + executions: [], + changed: false, + dryRun, + }; + } + // Initialize pipeline context const context: PipelineContext = { filePath, diff --git a/src/core/pipeline/__tests__/FormatterPipeline.test.ts b/src/core/pipeline/__tests__/FormatterPipeline.test.ts index b6f6273..35d0105 100644 --- a/src/core/pipeline/__tests__/FormatterPipeline.test.ts +++ b/src/core/pipeline/__tests__/FormatterPipeline.test.ts @@ -255,6 +255,82 @@ describe("FormatterPipeline", () => { expect(context.changed).toBe(false); expect(context.executions[0].changed).toBe(false); }); + it("should skip formatting files with // tsfmt-ignore comment", async () => { + const source = "// tsfmt-ignore\nconst foo = 'single quotes';"; + + await fs.writeFile(testFilePath, source, "utf-8"); + + const config: CoreConfig = { + ...ConfigDefaults.getDefaultConfig(), + indexGeneration: {enabled: false}, + codeStyle: {enabled: true, quoteStyle: "double"}, + sorting: {enabled: false}, + imports: {enabled: false}, + spacing: {enabled: false}, + }; + + const container = new Container(); + ServiceRegistration.registerServices(container, config); + const pipeline = new FormatterPipeline(config, container); + const context = await pipeline.formatFile(testFilePath, false); + + expect(context.changed).toBe(false); + expect(context.executions).toHaveLength(0); + expect(context.currentSource).toBe(source); + + const fileContent = await fs.readFile(testFilePath, "utf-8"); + expect(fileContent).toBe(source); + }); + it("should skip formatting files with /* tsfmt-ignore */ comment", async () => { + const source = "/* tsfmt-ignore */\nconst foo = 'single quotes';"; + + await fs.writeFile(testFilePath, source, "utf-8"); + + const config: CoreConfig = { + ...ConfigDefaults.getDefaultConfig(), + indexGeneration: {enabled: false}, + codeStyle: {enabled: true, quoteStyle: "double"}, + sorting: {enabled: false}, + imports: {enabled: false}, + spacing: {enabled: false}, + }; + + const container = new Container(); + ServiceRegistration.registerServices(container, config); + const pipeline = new FormatterPipeline(config, container); + const context = await pipeline.formatFile(testFilePath, false); + + expect(context.changed).toBe(false); + expect(context.executions).toHaveLength(0); + }); + it("should skip formatting files with tsfmt-ignore in header comment", async () => { + const source = [ + "/*", + "* Copyright notice", + "* tsfmt-ignore", + "*/", + "const foo = 'single quotes';" + ].join("\n"); + + await fs.writeFile(testFilePath, source, "utf-8"); + + const config: CoreConfig = { + ...ConfigDefaults.getDefaultConfig(), + indexGeneration: {enabled: false}, + codeStyle: {enabled: true, quoteStyle: "double"}, + sorting: {enabled: false}, + imports: {enabled: false}, + spacing: {enabled: false}, + }; + + const container = new Container(); + ServiceRegistration.registerServices(container, config); + const pipeline = new FormatterPipeline(config, container); + const context = await pipeline.formatFile(testFilePath, false); + + expect(context.changed).toBe(false); + expect(context.executions).toHaveLength(0); + }); }); describe("formatFiles", () => { it("should format multiple files", async () => { From a267bbd4fa9587e9035514f854ef1add2758c8ed Mon Sep 17 00:00:00 2001 From: EncoreBot Date: Fri, 23 Jan 2026 17:29:52 +0000 Subject: [PATCH 8/8] Apply Formatting and Build --- src/build-plugins/index.ts | 3 ++- src/core/config/ConfigLoader.ts | 10 +++++----- src/core/config/__tests__/ConfigLoader.test.ts | 8 ++++---- .../rules/style/StructuralIndentationRule.ts | 4 ++-- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/build-plugins/index.ts b/src/build-plugins/index.ts index 43e4b1c..f6daf44 100644 --- a/src/build-plugins/index.ts +++ b/src/build-plugins/index.ts @@ -1,4 +1,5 @@ + // Auto-generated exports - do not edit manually // Run tsfmt to regenerate -export * from "./transformGenericsPlugin"; +export * from "./transformGenericsPlugin" diff --git a/src/core/config/ConfigLoader.ts b/src/core/config/ConfigLoader.ts index a2652b5..67929e3 100644 --- a/src/core/config/ConfigLoader.ts +++ b/src/core/config/ConfigLoader.ts @@ -114,7 +114,7 @@ export default config; return { size: this.configCache.size, keys: Array.from(this.configCache.keys()) -}; + }; } /** @@ -128,7 +128,7 @@ export default config; } catch { return 0; } - } + } /** * Checks if a tsfmt.config.ts file exists in the project @@ -152,8 +152,8 @@ export default config; target: ts.ScriptTarget.ES2015, esModuleInterop: true, allowSyntheticDefaultImports: true, -}, -}); + }, + }); return result.outputText; } @@ -198,7 +198,7 @@ export default config; } return config; - } catch (error) { + } catch (error) { throw new Error(`Failed to load ${this.CONFIG_FILE_NAME}: ${error instanceof Error ? error.message : String(error)}`); } } diff --git a/src/core/config/__tests__/ConfigLoader.test.ts b/src/core/config/__tests__/ConfigLoader.test.ts index c8489ff..e7ebda9 100644 --- a/src/core/config/__tests__/ConfigLoader.test.ts +++ b/src/core/config/__tests__/ConfigLoader.test.ts @@ -174,7 +174,7 @@ describe("ConfigLoader", () => { const result = ConfigLoader.loadConfig(tempDir, false); expect(result.codeStyle?.quoteStyle).toBe("invalid"); -}); + }); }); describe("loadConfigWithoutValidation", () => { @@ -230,8 +230,8 @@ describe("ConfigLoader", () => { const stats = ConfigLoader.getCacheStats(); expect(stats.size).toBe(0); expect(stats.keys).toHaveLength(0); -}); - }); + }); + }); describe("getCacheStats", () => { it("should return cache statistics", () => { @@ -263,7 +263,7 @@ describe("ConfigLoader", () => { expect(() => { ConfigLoader.createSampleConfig(tempDir); }).toThrow("Configuration file already exists"); -}); + }); it("should overwrite existing file when overwrite is true", () => { mockedFs.existsSync.mockReturnValue(true); diff --git a/src/core/formatters/rules/style/StructuralIndentationRule.ts b/src/core/formatters/rules/style/StructuralIndentationRule.ts index df616f1..b6e1816 100644 --- a/src/core/formatters/rules/style/StructuralIndentationRule.ts +++ b/src/core/formatters/rules/style/StructuralIndentationRule.ts @@ -78,13 +78,13 @@ private skipString(source: string, start: number, quote: string): { pos: number; // End of string if (char === quote) { - return { pos: i + 1, newlines }; + return {pos: i + 1, newlines}; } i++; } - return { pos: i, newlines }; + return {pos: i, newlines}; } private isRegexStart(source: string, index: number): boolean {