From d152f822b35e3bd3663656b0993b9b1952a9eac5 Mon Sep 17 00:00:00 2001 From: JohnDoe6345789 Date: Sat, 27 Dec 2025 19:15:25 +0000 Subject: [PATCH] feat: add class and function detectors for TypeScript/TSX source files --- detection/detectors/class-detector.ts | 64 +++++++++++++++++++ detection/detectors/function-detector.ts | 78 ++++++++++++++++++++++++ detection/index.ts | 45 ++++++++++++++ 3 files changed, 187 insertions(+) create mode 100644 detection/detectors/class-detector.ts create mode 100644 detection/detectors/function-detector.ts create mode 100644 detection/index.ts diff --git a/detection/detectors/class-detector.ts b/detection/detectors/class-detector.ts new file mode 100644 index 000000000..2fb08b909 --- /dev/null +++ b/detection/detectors/class-detector.ts @@ -0,0 +1,64 @@ +import * as ts from 'typescript' +import { Detector, DetectionFinding, DetectorContext } from '..' + +const getLocation = (sourceFile: ts.SourceFile, node: ts.Node) => { + const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart()) + + return { + line: line + 1, + column: character + 1 + } +} + +const getClassName = ( + node: ts.ClassDeclaration | ts.ClassExpression, + sourceFile: ts.SourceFile +): string => { + if (node.name) { + return node.name.getText(sourceFile) + } + + const parent = node.parent + + if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) { + return parent.name.text + } + + return 'anonymous' +} + +const collectClasses = (context: DetectorContext): DetectionFinding[] => { + const sourceFile = ts.createSourceFile( + context.filePath, + context.source, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.TSX + ) + + const findings: DetectionFinding[] = [] + + const visit = (node: ts.Node) => { + if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) { + const name = getClassName(node, sourceFile) + findings.push({ + detectorId: 'class-detector', + name, + message: `Class detected: ${name}`, + location: getLocation(sourceFile, node) + }) + } + + ts.forEachChild(node, visit) + } + + visit(sourceFile) + + return findings +} + +export const classDetector: Detector = { + id: 'class-detector', + description: 'Detects class declarations and expressions within a TypeScript/TSX source file.', + detect: collectClasses +} diff --git a/detection/detectors/function-detector.ts b/detection/detectors/function-detector.ts new file mode 100644 index 000000000..7bfffafd1 --- /dev/null +++ b/detection/detectors/function-detector.ts @@ -0,0 +1,78 @@ +import * as ts from 'typescript' +import { Detector, DetectionFinding, DetectorContext } from '..' + +type FunctionLike = + | ts.FunctionDeclaration + | ts.FunctionExpression + | ts.ArrowFunction + | ts.MethodDeclaration + | ts.ConstructorDeclaration + +const getLocation = (sourceFile: ts.SourceFile, node: ts.Node) => { + const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart()) + + return { + line: line + 1, + column: character + 1 + } +} + +const getFunctionName = (node: FunctionLike, sourceFile: ts.SourceFile): string => { + if ('name' in node && node.name) { + return node.name.getText(sourceFile) + } + + const parent = node.parent + + if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) { + return parent.name.text + } + + if (ts.isPropertyAssignment(parent) && ts.isIdentifier(parent.name)) { + return parent.name.text + } + + return 'anonymous' +} + +const collectFunctions = (context: DetectorContext): DetectionFinding[] => { + const sourceFile = ts.createSourceFile( + context.filePath, + context.source, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.TSX + ) + + const findings: DetectionFinding[] = [] + + const visit = (node: ts.Node) => { + if ( + ts.isFunctionDeclaration(node) || + ts.isFunctionExpression(node) || + ts.isArrowFunction(node) || + ts.isMethodDeclaration(node) || + ts.isConstructorDeclaration(node) + ) { + const name = getFunctionName(node, sourceFile) + findings.push({ + detectorId: 'function-detector', + name, + message: `Function detected: ${name}`, + location: getLocation(sourceFile, node) + }) + } + + ts.forEachChild(node, visit) + } + + visit(sourceFile) + + return findings +} + +export const functionDetector: Detector = { + id: 'function-detector', + description: 'Detects functions and methods within a TypeScript/TSX source file.', + detect: collectFunctions +} diff --git a/detection/index.ts b/detection/index.ts new file mode 100644 index 000000000..7f5eb21f4 --- /dev/null +++ b/detection/index.ts @@ -0,0 +1,45 @@ +import { classDetector } from './detectors/class-detector' +import { functionDetector } from './detectors/function-detector' + +export type DetectorContext = { + filePath: string + source: string +} + +export type DetectionFinding = { + detectorId: string + name: string + message: string + location?: { + line: number + column: number + } +} + +export interface Detector { + id: string + description: string + detect: (context: DetectorContext) => DetectionFinding[] +} + +export class DetectorRegistry { + private readonly detectors: Detector[] = [] + + register(detector: Detector): void { + this.detectors.push(detector) + } + + list(): Detector[] { + return [...this.detectors] + } + + run(context: DetectorContext): DetectionFinding[] { + return this.detectors.flatMap((detector) => detector.detect(context)) + } +} + +export const registry = new DetectorRegistry() + +const builtInDetectors: Detector[] = [functionDetector, classDetector] + +builtInDetectors.forEach((detector) => registry.register(detector))