@webpieces/code-rules 0.3.337 → 0.3.339

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/code-rules",
3
- "version": "0.3.337",
3
+ "version": "0.3.339",
4
4
  "description": "Standalone code validation rules extracted from architecture-validators, no Nx dependency required",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -20,7 +20,7 @@
20
20
  "directory": "packages/tooling/code-rules"
21
21
  },
22
22
  "dependencies": {
23
- "@webpieces/rules-config": "0.3.337"
23
+ "@webpieces/rules-config": "0.3.339"
24
24
  },
25
25
  "publishConfig": {
26
26
  "access": "public"
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * Validate No Function Outside Class
3
3
  *
4
- * Flags a function CREATED OUTSIDE A CLASS at module scope:
5
- * - a top-level `function foo()` / `export function foo()` declaration, and
6
- * - a top-level `const foo = () => {}` / `const foo = function(){}`.
4
+ * Flags a function CREATED OUTSIDE A CLASS at module scope, or a STATIC class member:
5
+ * - a top-level `function foo()` / `export function foo()` declaration,
6
+ * - a top-level `const foo = () => {}` / `const foo = function(){}`, and
7
+ * - a `static foo() {}` method or `static foo = () => {}` field-function (a static member
8
+ * belongs to the class object, not an instance, so it can't be injected either).
7
9
  *
8
10
  * WHY: webpieces DI + @DocumentDesign only work when behavior lives in injectable classes that can be
9
11
  * wired into each other. A module-scope function is a dead-end the DI graph can't reach — move the
@@ -2,9 +2,11 @@
2
2
  /**
3
3
  * Validate No Function Outside Class
4
4
  *
5
- * Flags a function CREATED OUTSIDE A CLASS at module scope:
6
- * - a top-level `function foo()` / `export function foo()` declaration, and
7
- * - a top-level `const foo = () => {}` / `const foo = function(){}`.
5
+ * Flags a function CREATED OUTSIDE A CLASS at module scope, or a STATIC class member:
6
+ * - a top-level `function foo()` / `export function foo()` declaration,
7
+ * - a top-level `const foo = () => {}` / `const foo = function(){}`, and
8
+ * - a `static foo() {}` method or `static foo = () => {}` field-function (a static member
9
+ * belongs to the class object, not an instance, so it can't be injected either).
8
10
  *
9
11
  * WHY: webpieces DI + @DocumentDesign only work when behavior lives in injectable classes that can be
10
12
  * wired into each other. A module-scope function is a dead-end the DI graph can't reach — move the
@@ -40,14 +42,16 @@ const ts = tslib_1.__importStar(require("typescript"));
40
42
  const rules_config_1 = require("@webpieces/rules-config");
41
43
  const code_validator_1 = require("./code-validator");
42
44
  const resolve_mode_1 = require("./resolve-mode");
43
- const SHARED_MESSAGE = `Functions must live inside a class — a function created at module scope can't be injected, so
44
- webpieces DI + @DocumentDesign can't wire it into anything.
45
+ const SHARED_MESSAGE = `Functions must live inside a class as INSTANCE methods — a function created at module scope, OR a
46
+ static method, can't be injected, so webpieces DI + @DocumentDesign can't wire it into anything. A
47
+ static method is just a module-scope function wearing a class as a namespace.
45
48
  BAD: export function computeTotal(cart: Cart): number { ... }
49
+ BAD: export class CartMath { static computeTotal(cart: Cart): number { ... } } // static = not injectable
46
50
  GOOD: @provideSingleton()
47
51
  export class CartService { computeTotal(cart: Cart): number { ... } }
48
52
  Then inject CartService where you need it. Inline callbacks (arr.map(x => x)), promise handlers, and
49
- functions nested inside a method are fine — only MODULE-SCOPE function declarations and top-level
50
- const-assigned functions are flagged.`;
53
+ functions nested inside a method are fine — only MODULE-SCOPE function declarations, top-level
54
+ const-assigned functions, and STATIC methods/field-functions are flagged.`;
51
55
  function isTestFile(filePath) {
52
56
  return filePath.includes('.spec.ts') || filePath.includes('.test.ts') || filePath.includes('__tests__/');
53
57
  }
@@ -61,6 +65,32 @@ function hasDeclareModifier(node) {
61
65
  const mods = ts.getModifiers(node);
62
66
  return mods?.some((m) => m.kind === ts.SyntaxKind.DeclareKeyword) ?? false;
63
67
  }
68
+ // True for a class member carrying the `static` modifier. A static method/field-function belongs
69
+ // to the class object, not an instance, so it can't be injected — it is procedural code wearing a
70
+ // class as a namespace, and is flagged the same as a module-scope function.
71
+ // webpieces-disable no-function-outside-class -- the rule engine is inherently functional; validators can't be class members
72
+ function hasStaticModifier(node) {
73
+ if (!ts.canHaveModifiers(node))
74
+ return false;
75
+ const mods = ts.getModifiers(node);
76
+ return mods?.some((m) => m.kind === ts.SyntaxKind.StaticKeyword) ?? false;
77
+ }
78
+ // A `static` method, or a `static` field initialized to an arrow/function-expression, on a class.
79
+ // webpieces-disable no-function-outside-class -- the rule engine is inherently functional; validators can't be class members
80
+ function isStaticFunctionMember(node) {
81
+ if (ts.isMethodDeclaration(node))
82
+ return hasStaticModifier(node);
83
+ if (ts.isPropertyDeclaration(node) && hasStaticModifier(node)) {
84
+ const init = node.initializer;
85
+ return init !== undefined && (ts.isArrowFunction(init) || ts.isFunctionExpression(init));
86
+ }
87
+ return false;
88
+ }
89
+ // Display name of a static member for the violation message.
90
+ // webpieces-disable no-function-outside-class -- the rule engine is inherently functional; validators can't be class members
91
+ function staticMemberName(node) {
92
+ return ts.isIdentifier(node.name) ? node.name.text : '<static>';
93
+ }
64
94
  // The violation line (or the line just above it) carries the disable comment.
65
95
  function hasDisableOnLine(fileLines, lineNumber) {
66
96
  const current = fileLines[lineNumber - 1] ?? '';
@@ -108,6 +138,10 @@ function findFunctionsOutsideClassInSource(content, filePath, disableAllowed) {
108
138
  if (ts.isVariableStatement(node) && ts.isSourceFile(node.parent) && !hasDeclareModifier(node)) {
109
139
  checkTopLevelConst(node, fileLines, sourceFile, violations, disableAllowed);
110
140
  }
141
+ if (isStaticFunctionMember(node) && !hasDeclareModifier(node)) {
142
+ const name = staticMemberName(node);
143
+ recordViolation(node, `static ${name}() — a static method can't be injected; make it an instance method and inject the class`, fileLines, sourceFile, violations, disableAllowed);
144
+ }
111
145
  ts.forEachChild(node, visit);
112
146
  }
113
147
  visit(sourceFile);
@@ -1 +1 @@
1
- {"version":3,"file":"validate-no-function-outside-class.js","sourceRoot":"","sources":["../../../../../packages/tooling/code-rules/src/validate-no-function-outside-class.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Validate No Function Outside Class\n *\n * Flags a function CREATED OUTSIDE A CLASS at module scope:\n * - a top-level `function foo()` / `export function foo()` declaration, and\n * - a top-level `const foo = () => {}` / `const foo = function(){}`.\n *\n * WHY: webpieces DI + @DocumentDesign only work when behavior lives in injectable classes that can be\n * wired into each other. A module-scope function is a dead-end the DI graph can't reach — move the\n * logic onto a `@provideSingleton()` class and inject it instead.\n *\n * Uses the TypeScript AST so class-membership is checked precisely (not a regex heuristic): a node is a\n * violation only when its parent is the SourceFile (module scope). That automatically EXEMPTS inline\n * callbacks (`arr.map(x => x)`), promise handlers, nested functions inside methods, and functions\n * nested in other functions — their parent is a Block/CallExpression, never the SourceFile.\n *\n * ALLOWED (skip — NOT violations)\n * - Any function/arrow nested inside a class method, another function, or a callback.\n * - Non-function top-level consts: `const SCHEMA = z.object({})`, `const MAX = 5`, object/array literals.\n * - Ambient declarations (`declare function ...`) and whole `*.d.ts` files.\n * - Test files (*.test.ts, *.spec.ts, __tests__/**).\n * - Files under a configured `allowedPaths` glob (e.g. React component/hook trees that legitimately use\n * module-scope functions). Matched with the shared `isPathExcluded` glob/prefix/segment semantics.\n * - Lines with `// webpieces-disable no-function-outside-class -- <reason>` (when disableAllowed).\n *\n * MODES (AST + LINE-SCOPED)\n * - OFF: Skip.\n * - NEW_AND_MODIFIED_CODE: Flag only on changed lines (diff hunks).\n * - NEW_AND_MODIFIED_FILES: Flag every occurrence in any modified file.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport * as ts from 'typescript';\nimport { hasDisable, RULE_NAMES, NoFunctionOutsideClassConfig, ModifiedCodeMode, detectBase, getChangedFiles, getFileDiff, getChangedLineNumbers, isPathExcluded } from '@webpieces/rules-config';\nimport { CodeValidator, ExecutorResult } from './code-validator';\nimport { shouldSkipRule } from './resolve-mode';\n\nconst SHARED_MESSAGE = `Functions must live inside a class — a function created at module scope can't be injected, so\nwebpieces DI + @DocumentDesign can't wire it into anything.\n BAD: export function computeTotal(cart: Cart): number { ... }\n GOOD: @provideSingleton()\n export class CartService { computeTotal(cart: Cart): number { ... } }\nThen inject CartService where you need it. Inline callbacks (arr.map(x => x)), promise handlers, and\nfunctions nested inside a method are fine — only MODULE-SCOPE function declarations and top-level\nconst-assigned functions are flagged.`;\n\ninterface FnViolation {\n file: string;\n line: number;\n column: number;\n context: string;\n}\n\ninterface FnViolationInfo {\n line: number;\n column: number;\n context: string;\n hasDisableComment: boolean;\n}\n\nfunction isTestFile(filePath: string): boolean {\n return filePath.includes('.spec.ts') || filePath.includes('.test.ts') || filePath.includes('__tests__/');\n}\n\nfunction isDeclarationFile(filePath: string): boolean {\n return filePath.endsWith('.d.ts');\n}\n\n// True for a node carrying a `declare` modifier (ambient signature — not a real definition).\nfunction hasDeclareModifier(node: ts.Node): boolean {\n if (!ts.canHaveModifiers(node)) return false;\n const mods = ts.getModifiers(node);\n return mods?.some((m: ts.Modifier): boolean => m.kind === ts.SyntaxKind.DeclareKeyword) ?? false;\n}\n\n// The violation line (or the line just above it) carries the disable comment.\nfunction hasDisableOnLine(fileLines: string[], lineNumber: number): boolean {\n const current = fileLines[lineNumber - 1] ?? '';\n const previous = lineNumber >= 2 ? (fileLines[lineNumber - 2] ?? '') : '';\n return hasDisable(current, RULE_NAMES.NO_FUNCTION_OUTSIDE_CLASS)\n || hasDisable(previous, RULE_NAMES.NO_FUNCTION_OUTSIDE_CLASS);\n}\n\nfunction recordViolation(\n node: ts.Node,\n context: string,\n fileLines: string[],\n sourceFile: ts.SourceFile,\n violations: FnViolationInfo[],\n disableAllowed: boolean,\n): void {\n const startPos = node.getStart(sourceFile);\n if (startPos < 0) return;\n const pos = sourceFile.getLineAndCharacterOfPosition(startPos);\n const line = pos.line + 1;\n const column = pos.character + 1;\n const disabled = hasDisableOnLine(fileLines, line);\n // When disableAllowed is false, a disable comment does NOT clear the violation.\n const effectiveDisabled = disableAllowed ? disabled : false;\n violations.push({ line, column, context, hasDisableComment: effectiveDisabled });\n}\n\n// Report a module-scope const whose initializer is an arrow / function-expression (a function\n// assigned to a top-level const). Non-function consts are left alone.\nfunction checkTopLevelConst(\n node: ts.VariableStatement,\n fileLines: string[],\n sourceFile: ts.SourceFile,\n violations: FnViolationInfo[],\n disableAllowed: boolean,\n): void {\n for (const decl of node.declarationList.declarations) {\n const init = decl.initializer;\n if (!init) continue;\n if (!ts.isArrowFunction(init) && !ts.isFunctionExpression(init)) continue;\n const name = ts.isIdentifier(decl.name) ? decl.name.text : '<fn>';\n recordViolation(decl, `const ${name} = <function> at module scope (outside any class)`, fileLines, sourceFile, violations, disableAllowed);\n }\n}\n\n// AST scan: a node is a violation only when its parent is the SourceFile (module scope).\nexport function findFunctionsOutsideClassInSource(content: string, filePath: string, disableAllowed: boolean): FnViolationInfo[] {\n if (isDeclarationFile(filePath)) return [];\n const fileLines = content.split('\\n');\n const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);\n const violations: FnViolationInfo[] = [];\n\n function visit(node: ts.Node): void {\n if (ts.isFunctionDeclaration(node) && ts.isSourceFile(node.parent) && !hasDeclareModifier(node)) {\n const name = node.name?.text ?? '<anonymous>';\n recordViolation(node, `function ${name}() declared at module scope (outside any class)`, fileLines, sourceFile, violations, disableAllowed);\n }\n if (ts.isVariableStatement(node) && ts.isSourceFile(node.parent) && !hasDeclareModifier(node)) {\n checkTopLevelConst(node, fileLines, sourceFile, violations, disableAllowed);\n }\n ts.forEachChild(node, visit);\n }\n\n visit(sourceFile);\n return violations;\n}\n\n// webpieces-disable no-function-outside-class -- the rule engine is inherently functional; validators can't be class members\nexport function findFunctionsOutsideClassInFile(filePath: string, workspaceRoot: string, disableAllowed: boolean, allowedPaths: string[]): FnViolationInfo[] {\n if (isTestFile(filePath)) return [];\n if (isPathExcluded(filePath, allowedPaths)) return [];\n const fullPath = path.join(workspaceRoot, filePath);\n if (!fs.existsSync(fullPath)) return [];\n const content = fs.readFileSync(fullPath, 'utf-8');\n return findFunctionsOutsideClassInSource(content, filePath, disableAllowed);\n}\n\n// webpieces-disable no-function-outside-class -- the rule engine is inherently functional; validators can't be class members\nfunction findViolationsForModifiedCode(workspaceRoot: string, changedFiles: string[], base: string, head: string | undefined, disableAllowed: boolean, allowedPaths: string[]): FnViolation[] {\n const violations: FnViolation[] = [];\n for (const file of changedFiles) {\n const changedLines = getChangedLineNumbers(getFileDiff(workspaceRoot, file, base, head));\n if (changedLines.size === 0) continue;\n for (const v of findFunctionsOutsideClassInFile(file, workspaceRoot, disableAllowed, allowedPaths)) {\n if (disableAllowed && v.hasDisableComment) continue;\n if (!changedLines.has(v.line)) continue;\n violations.push({ file, line: v.line, column: v.column, context: v.context });\n }\n }\n return violations;\n}\n\n// webpieces-disable no-function-outside-class -- the rule engine is inherently functional; validators can't be class members\nfunction findViolationsForModifiedFiles(workspaceRoot: string, changedFiles: string[], disableAllowed: boolean, allowedPaths: string[]): FnViolation[] {\n const violations: FnViolation[] = [];\n for (const file of changedFiles) {\n for (const v of findFunctionsOutsideClassInFile(file, workspaceRoot, disableAllowed, allowedPaths)) {\n if (disableAllowed && v.hasDisableComment) continue;\n violations.push({ file, line: v.line, column: v.column, context: v.context });\n }\n }\n return violations;\n}\n\nfunction reportViolations(violations: FnViolation[], mode: ModifiedCodeMode, disableAllowed: boolean): void {\n console.error('');\n console.error('❌ Function(s) created outside a class!');\n console.error('');\n console.error(SHARED_MESSAGE);\n console.error('');\n for (const v of violations) {\n console.error(` ❌ ${v.file}:${v.line}:${v.column}`);\n console.error(` ${v.context}`);\n }\n console.error('');\n console.error(disableAllowed\n ? ' Escape hatch (use sparingly): // webpieces-disable no-function-outside-class -- <reason>'\n : ' Escape hatch: DISABLED (disableAllowed: false)');\n console.error(' Whole-tree exemption (e.g. React): add a glob to no-function-outside-class.allowedPaths in webpieces.config.json');\n console.error(`\\n Current mode: ${mode}\\n`);\n}\n\nfunction resolveMode(normalMode: ModifiedCodeMode, epoch: number | undefined, branchPattern: string | undefined): ModifiedCodeMode {\n if (normalMode === 'OFF') return normalMode;\n const skip = shouldSkipRule(epoch, branchPattern);\n if (skip.skip) {\n console.log(`\\n⏭️ Skipping no-function-outside-class validation (${skip.reason})\\n`);\n return 'OFF';\n }\n return normalMode;\n}\n\nasync function runValidatorImpl(options: NoFunctionOutsideClassConfig, workspaceRoot: string): Promise<ExecutorResult> {\n const mode: ModifiedCodeMode = resolveMode(options.mode ?? 'OFF', options.ignoreModifiedUntilEpoch, options.ignoreRuleWhileOnBranch);\n const disableAllowed = options.disableAllowed ?? true;\n const allowedPaths = options.allowedPaths ?? [];\n\n if (mode === 'OFF') {\n console.log('\\n⏭️ Skipping no-function-outside-class validation (mode: OFF)\\n');\n return { success: true };\n }\n\n console.log('\\n📏 Validating No Function Outside Class\\n');\n console.log(` Mode: ${mode}`);\n\n let base = process.env['NX_BASE'];\n const head = process.env['NX_HEAD'];\n if (!base) {\n base = detectBase(workspaceRoot) ?? undefined;\n if (!base) {\n console.log('\\n⏭️ Skipping no-function-outside-class validation (could not detect base branch)\\n');\n return { success: true };\n }\n }\n\n console.log(` Base: ${base}`);\n console.log(` Head: ${head ?? 'working tree (includes uncommitted changes)'}\\n`);\n\n const changedFiles = getChangedFiles(workspaceRoot, base, head);\n if (changedFiles.length === 0) {\n console.log('✅ No TypeScript files changed');\n return { success: true };\n }\n\n console.log(`📂 Checking ${changedFiles.length} changed file(s)...`);\n\n let violations: FnViolation[] = [];\n if (mode === 'NEW_AND_MODIFIED_CODE') {\n violations = findViolationsForModifiedCode(workspaceRoot, changedFiles, base, head, disableAllowed, allowedPaths);\n } else if (mode === 'NEW_AND_MODIFIED_FILES') {\n violations = findViolationsForModifiedFiles(workspaceRoot, changedFiles, disableAllowed, allowedPaths);\n }\n\n if (violations.length === 0) {\n console.log('✅ No function-outside-class violations found');\n return { success: true };\n }\n\n reportViolations(violations, mode, disableAllowed);\n return { success: false };\n}\n\nexport class NoFunctionOutsideClassValidator extends CodeValidator<NoFunctionOutsideClassConfig> {\n constructor(config: NoFunctionOutsideClassConfig) {\n super(config, 'no-function-outside-class');\n }\n\n async run(workspaceRoot: string): Promise<ExecutorResult> {\n return runValidatorImpl(this.config, workspaceRoot);\n }\n}\n"]}
1
+ {"version":3,"file":"validate-no-function-outside-class.js","sourceRoot":"","sources":["../../../../../packages/tooling/code-rules/src/validate-no-function-outside-class.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Validate No Function Outside Class\n *\n * Flags a function CREATED OUTSIDE A CLASS at module scope, or a STATIC class member:\n * - a top-level `function foo()` / `export function foo()` declaration,\n * - a top-level `const foo = () => {}` / `const foo = function(){}`, and\n * - a `static foo() {}` method or `static foo = () => {}` field-function (a static member\n * belongs to the class object, not an instance, so it can't be injected either).\n *\n * WHY: webpieces DI + @DocumentDesign only work when behavior lives in injectable classes that can be\n * wired into each other. A module-scope function is a dead-end the DI graph can't reach — move the\n * logic onto a `@provideSingleton()` class and inject it instead.\n *\n * Uses the TypeScript AST so class-membership is checked precisely (not a regex heuristic): a node is a\n * violation only when its parent is the SourceFile (module scope). That automatically EXEMPTS inline\n * callbacks (`arr.map(x => x)`), promise handlers, nested functions inside methods, and functions\n * nested in other functions — their parent is a Block/CallExpression, never the SourceFile.\n *\n * ALLOWED (skip — NOT violations)\n * - Any function/arrow nested inside a class method, another function, or a callback.\n * - Non-function top-level consts: `const SCHEMA = z.object({})`, `const MAX = 5`, object/array literals.\n * - Ambient declarations (`declare function ...`) and whole `*.d.ts` files.\n * - Test files (*.test.ts, *.spec.ts, __tests__/**).\n * - Files under a configured `allowedPaths` glob (e.g. React component/hook trees that legitimately use\n * module-scope functions). Matched with the shared `isPathExcluded` glob/prefix/segment semantics.\n * - Lines with `// webpieces-disable no-function-outside-class -- <reason>` (when disableAllowed).\n *\n * MODES (AST + LINE-SCOPED)\n * - OFF: Skip.\n * - NEW_AND_MODIFIED_CODE: Flag only on changed lines (diff hunks).\n * - NEW_AND_MODIFIED_FILES: Flag every occurrence in any modified file.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport * as ts from 'typescript';\nimport { hasDisable, RULE_NAMES, NoFunctionOutsideClassConfig, ModifiedCodeMode, detectBase, getChangedFiles, getFileDiff, getChangedLineNumbers, isPathExcluded } from '@webpieces/rules-config';\nimport { CodeValidator, ExecutorResult } from './code-validator';\nimport { shouldSkipRule } from './resolve-mode';\n\nconst SHARED_MESSAGE = `Functions must live inside a class as INSTANCE methods — a function created at module scope, OR a\nstatic method, can't be injected, so webpieces DI + @DocumentDesign can't wire it into anything. A\nstatic method is just a module-scope function wearing a class as a namespace.\n BAD: export function computeTotal(cart: Cart): number { ... }\n BAD: export class CartMath { static computeTotal(cart: Cart): number { ... } } // static = not injectable\n GOOD: @provideSingleton()\n export class CartService { computeTotal(cart: Cart): number { ... } }\nThen inject CartService where you need it. Inline callbacks (arr.map(x => x)), promise handlers, and\nfunctions nested inside a method are fine — only MODULE-SCOPE function declarations, top-level\nconst-assigned functions, and STATIC methods/field-functions are flagged.`;\n\ninterface FnViolation {\n file: string;\n line: number;\n column: number;\n context: string;\n}\n\ninterface FnViolationInfo {\n line: number;\n column: number;\n context: string;\n hasDisableComment: boolean;\n}\n\nfunction isTestFile(filePath: string): boolean {\n return filePath.includes('.spec.ts') || filePath.includes('.test.ts') || filePath.includes('__tests__/');\n}\n\nfunction isDeclarationFile(filePath: string): boolean {\n return filePath.endsWith('.d.ts');\n}\n\n// True for a node carrying a `declare` modifier (ambient signature — not a real definition).\nfunction hasDeclareModifier(node: ts.Node): boolean {\n if (!ts.canHaveModifiers(node)) return false;\n const mods = ts.getModifiers(node);\n return mods?.some((m: ts.Modifier): boolean => m.kind === ts.SyntaxKind.DeclareKeyword) ?? false;\n}\n\n// True for a class member carrying the `static` modifier. A static method/field-function belongs\n// to the class object, not an instance, so it can't be injected — it is procedural code wearing a\n// class as a namespace, and is flagged the same as a module-scope function.\n// webpieces-disable no-function-outside-class -- the rule engine is inherently functional; validators can't be class members\nfunction hasStaticModifier(node: ts.Node): boolean {\n if (!ts.canHaveModifiers(node)) return false;\n const mods = ts.getModifiers(node);\n return mods?.some((m: ts.Modifier): boolean => m.kind === ts.SyntaxKind.StaticKeyword) ?? false;\n}\n\n// A `static` method, or a `static` field initialized to an arrow/function-expression, on a class.\n// webpieces-disable no-function-outside-class -- the rule engine is inherently functional; validators can't be class members\nfunction isStaticFunctionMember(node: ts.Node): boolean {\n if (ts.isMethodDeclaration(node)) return hasStaticModifier(node);\n if (ts.isPropertyDeclaration(node) && hasStaticModifier(node)) {\n const init = node.initializer;\n return init !== undefined && (ts.isArrowFunction(init) || ts.isFunctionExpression(init));\n }\n return false;\n}\n\n// Display name of a static member for the violation message.\n// webpieces-disable no-function-outside-class -- the rule engine is inherently functional; validators can't be class members\nfunction staticMemberName(node: ts.MethodDeclaration | ts.PropertyDeclaration): string {\n return ts.isIdentifier(node.name) ? node.name.text : '<static>';\n}\n\n// The violation line (or the line just above it) carries the disable comment.\nfunction hasDisableOnLine(fileLines: string[], lineNumber: number): boolean {\n const current = fileLines[lineNumber - 1] ?? '';\n const previous = lineNumber >= 2 ? (fileLines[lineNumber - 2] ?? '') : '';\n return hasDisable(current, RULE_NAMES.NO_FUNCTION_OUTSIDE_CLASS)\n || hasDisable(previous, RULE_NAMES.NO_FUNCTION_OUTSIDE_CLASS);\n}\n\nfunction recordViolation(\n node: ts.Node,\n context: string,\n fileLines: string[],\n sourceFile: ts.SourceFile,\n violations: FnViolationInfo[],\n disableAllowed: boolean,\n): void {\n const startPos = node.getStart(sourceFile);\n if (startPos < 0) return;\n const pos = sourceFile.getLineAndCharacterOfPosition(startPos);\n const line = pos.line + 1;\n const column = pos.character + 1;\n const disabled = hasDisableOnLine(fileLines, line);\n // When disableAllowed is false, a disable comment does NOT clear the violation.\n const effectiveDisabled = disableAllowed ? disabled : false;\n violations.push({ line, column, context, hasDisableComment: effectiveDisabled });\n}\n\n// Report a module-scope const whose initializer is an arrow / function-expression (a function\n// assigned to a top-level const). Non-function consts are left alone.\nfunction checkTopLevelConst(\n node: ts.VariableStatement,\n fileLines: string[],\n sourceFile: ts.SourceFile,\n violations: FnViolationInfo[],\n disableAllowed: boolean,\n): void {\n for (const decl of node.declarationList.declarations) {\n const init = decl.initializer;\n if (!init) continue;\n if (!ts.isArrowFunction(init) && !ts.isFunctionExpression(init)) continue;\n const name = ts.isIdentifier(decl.name) ? decl.name.text : '<fn>';\n recordViolation(decl, `const ${name} = <function> at module scope (outside any class)`, fileLines, sourceFile, violations, disableAllowed);\n }\n}\n\n// AST scan: a node is a violation only when its parent is the SourceFile (module scope).\nexport function findFunctionsOutsideClassInSource(content: string, filePath: string, disableAllowed: boolean): FnViolationInfo[] {\n if (isDeclarationFile(filePath)) return [];\n const fileLines = content.split('\\n');\n const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);\n const violations: FnViolationInfo[] = [];\n\n function visit(node: ts.Node): void {\n if (ts.isFunctionDeclaration(node) && ts.isSourceFile(node.parent) && !hasDeclareModifier(node)) {\n const name = node.name?.text ?? '<anonymous>';\n recordViolation(node, `function ${name}() declared at module scope (outside any class)`, fileLines, sourceFile, violations, disableAllowed);\n }\n if (ts.isVariableStatement(node) && ts.isSourceFile(node.parent) && !hasDeclareModifier(node)) {\n checkTopLevelConst(node, fileLines, sourceFile, violations, disableAllowed);\n }\n if (isStaticFunctionMember(node) && !hasDeclareModifier(node)) {\n const name = staticMemberName(node as ts.MethodDeclaration | ts.PropertyDeclaration);\n recordViolation(node, `static ${name}() — a static method can't be injected; make it an instance method and inject the class`, fileLines, sourceFile, violations, disableAllowed);\n }\n ts.forEachChild(node, visit);\n }\n\n visit(sourceFile);\n return violations;\n}\n\n// webpieces-disable no-function-outside-class -- the rule engine is inherently functional; validators can't be class members\nexport function findFunctionsOutsideClassInFile(filePath: string, workspaceRoot: string, disableAllowed: boolean, allowedPaths: string[]): FnViolationInfo[] {\n if (isTestFile(filePath)) return [];\n if (isPathExcluded(filePath, allowedPaths)) return [];\n const fullPath = path.join(workspaceRoot, filePath);\n if (!fs.existsSync(fullPath)) return [];\n const content = fs.readFileSync(fullPath, 'utf-8');\n return findFunctionsOutsideClassInSource(content, filePath, disableAllowed);\n}\n\n// webpieces-disable no-function-outside-class -- the rule engine is inherently functional; validators can't be class members\nfunction findViolationsForModifiedCode(workspaceRoot: string, changedFiles: string[], base: string, head: string | undefined, disableAllowed: boolean, allowedPaths: string[]): FnViolation[] {\n const violations: FnViolation[] = [];\n for (const file of changedFiles) {\n const changedLines = getChangedLineNumbers(getFileDiff(workspaceRoot, file, base, head));\n if (changedLines.size === 0) continue;\n for (const v of findFunctionsOutsideClassInFile(file, workspaceRoot, disableAllowed, allowedPaths)) {\n if (disableAllowed && v.hasDisableComment) continue;\n if (!changedLines.has(v.line)) continue;\n violations.push({ file, line: v.line, column: v.column, context: v.context });\n }\n }\n return violations;\n}\n\n// webpieces-disable no-function-outside-class -- the rule engine is inherently functional; validators can't be class members\nfunction findViolationsForModifiedFiles(workspaceRoot: string, changedFiles: string[], disableAllowed: boolean, allowedPaths: string[]): FnViolation[] {\n const violations: FnViolation[] = [];\n for (const file of changedFiles) {\n for (const v of findFunctionsOutsideClassInFile(file, workspaceRoot, disableAllowed, allowedPaths)) {\n if (disableAllowed && v.hasDisableComment) continue;\n violations.push({ file, line: v.line, column: v.column, context: v.context });\n }\n }\n return violations;\n}\n\nfunction reportViolations(violations: FnViolation[], mode: ModifiedCodeMode, disableAllowed: boolean): void {\n console.error('');\n console.error('❌ Function(s) created outside a class!');\n console.error('');\n console.error(SHARED_MESSAGE);\n console.error('');\n for (const v of violations) {\n console.error(` ❌ ${v.file}:${v.line}:${v.column}`);\n console.error(` ${v.context}`);\n }\n console.error('');\n console.error(disableAllowed\n ? ' Escape hatch (use sparingly): // webpieces-disable no-function-outside-class -- <reason>'\n : ' Escape hatch: DISABLED (disableAllowed: false)');\n console.error(' Whole-tree exemption (e.g. React): add a glob to no-function-outside-class.allowedPaths in webpieces.config.json');\n console.error(`\\n Current mode: ${mode}\\n`);\n}\n\nfunction resolveMode(normalMode: ModifiedCodeMode, epoch: number | undefined, branchPattern: string | undefined): ModifiedCodeMode {\n if (normalMode === 'OFF') return normalMode;\n const skip = shouldSkipRule(epoch, branchPattern);\n if (skip.skip) {\n console.log(`\\n⏭️ Skipping no-function-outside-class validation (${skip.reason})\\n`);\n return 'OFF';\n }\n return normalMode;\n}\n\nasync function runValidatorImpl(options: NoFunctionOutsideClassConfig, workspaceRoot: string): Promise<ExecutorResult> {\n const mode: ModifiedCodeMode = resolveMode(options.mode ?? 'OFF', options.ignoreModifiedUntilEpoch, options.ignoreRuleWhileOnBranch);\n const disableAllowed = options.disableAllowed ?? true;\n const allowedPaths = options.allowedPaths ?? [];\n\n if (mode === 'OFF') {\n console.log('\\n⏭️ Skipping no-function-outside-class validation (mode: OFF)\\n');\n return { success: true };\n }\n\n console.log('\\n📏 Validating No Function Outside Class\\n');\n console.log(` Mode: ${mode}`);\n\n let base = process.env['NX_BASE'];\n const head = process.env['NX_HEAD'];\n if (!base) {\n base = detectBase(workspaceRoot) ?? undefined;\n if (!base) {\n console.log('\\n⏭️ Skipping no-function-outside-class validation (could not detect base branch)\\n');\n return { success: true };\n }\n }\n\n console.log(` Base: ${base}`);\n console.log(` Head: ${head ?? 'working tree (includes uncommitted changes)'}\\n`);\n\n const changedFiles = getChangedFiles(workspaceRoot, base, head);\n if (changedFiles.length === 0) {\n console.log('✅ No TypeScript files changed');\n return { success: true };\n }\n\n console.log(`📂 Checking ${changedFiles.length} changed file(s)...`);\n\n let violations: FnViolation[] = [];\n if (mode === 'NEW_AND_MODIFIED_CODE') {\n violations = findViolationsForModifiedCode(workspaceRoot, changedFiles, base, head, disableAllowed, allowedPaths);\n } else if (mode === 'NEW_AND_MODIFIED_FILES') {\n violations = findViolationsForModifiedFiles(workspaceRoot, changedFiles, disableAllowed, allowedPaths);\n }\n\n if (violations.length === 0) {\n console.log('✅ No function-outside-class violations found');\n return { success: true };\n }\n\n reportViolations(violations, mode, disableAllowed);\n return { success: false };\n}\n\nexport class NoFunctionOutsideClassValidator extends CodeValidator<NoFunctionOutsideClassConfig> {\n constructor(config: NoFunctionOutsideClassConfig) {\n super(config, 'no-function-outside-class');\n }\n\n async run(workspaceRoot: string): Promise<ExecutorResult> {\n return runValidatorImpl(this.config, workspaceRoot);\n }\n}\n"]}