Skip to content

Scanner — C#

Csharp

Classes

CSharpScanner

class CSharpScanner implements Scanner
TypeScript

Use CSharpScanner to extract public API elements from C# source files so skrypt can generate documentation for your .NET codebase.

Reach for this when you're running skrypt generate against a C# project — it's the scanner that handles .cs files in the pipeline. It plugs into skrypt's multi-language scanning architecture, so you don't instantiate it directly; skrypt selects it automatically when it encounters C# files.

CSharpScanner extracts public classes, interfaces, enums, structs, records, and their methods. It deliberately skips test files — anything ending in Test.cs or Tests.cs, or living under a Tests/ or test/ directory — so your generated docs stay focused on your public API surface.

Properties

NameTypeRequiredDescription
languagesstring[]YesDeclares ['csharp'] — tells the scanner registry which file types this scanner owns.

Methods

NameSignatureDescription
canHandle(filePath: string) => booleanReturns true for .cs files outside test directories. skrypt calls this before dispatching a file to the scanner.
scan(filePath: string) => ScanResultReads the file and returns all extracted API elements with signatures, parameters, return types, and source context.

Returns

scan() returns a ScanResult containing an array of APIElement objects — each with a name, signature, parameters, return type, docstring (if present), and the surrounding source lines. skrypt passes these elements to the AI documentation generator to produce the final MDX output.

Heads up

  • XML doc comments (/// <summary>) are extracted as the existing docstring and passed to the AI — adding them to your C# source significantly improves generated doc quality.
  • Generic type parameters (e.g., List<T>) are preserved in signatures, but deeply nested generics may affect parameter parsing in edge cases.

Example:

// Self-contained simulation of CSharpScanner behavior
// Demonstrates how the scanner processes a .cs file and what it extracts

const fs = require('fs')
const path = require('path')
const os = require('os')

// Inline types matching the Scanner interface
const mockCSharpSource = `
using System;
using System.Collections.Generic;

namespace Payments.Core
{
    /// <summary>
    /// Processes payment transactions against the Stripe gateway.
    /// </summary>
    public class PaymentProcessor
    {
        /// <summary>
        /// Charges a customer for the given amount in cents.
        /// </summary>
        public async Task<ChargeResult> ChargeAsync(string customerId, int amountCents, string currency)
        {
            // implementation
        }

        public void Refund(string chargeId)
        {
            // implementation
        }
    }

    public interface IPaymentGateway
    {
        Task<bool> ValidateAsync(string apiKey);
    }

    public enum PaymentStatus
    {
        Pending,
        Succeeded,
        Failed
    }
}
`

// Write a temp .cs file to simulate real scanner input
const tmpDir = os.tmpdir()
const tmpFile = path.join(tmpDir, 'PaymentProcessor.cs')
fs.writeFileSync(tmpFile, mockCSharpSource)

// Simulate CSharpScanner.canHandle() logic
function canHandle(filePath) {
  return (
    /\.cs$/.test(filePath) &&
    !filePath.endsWith('Test.cs') &&
    !filePath.endsWith('Tests.cs') &&
    !filePath.includes('/Tests/') &&
    !filePath.includes('/test/')
  )
}

// Simulate CSharpScanner.scan() — extracts public API elements
function simulateScan(filePath) {
  const source = fs.readFileSync(filePath, 'utf-8')
  const elements = []

  // Extract public classes
  const classRegex = /\/\/\/\s*<summary>\s*([\s\S]*?)<\/summary>[\s\S]*?public\s+class\s+(\w+)/g
  let match
  while ((match = classRegex.exec(source)) !== null) {
    elements.push({
      kind: 'class',
      name: match[2],
      docstring: match[1].replace(/\s*\/\/\/\s*/g, '').trim(),
      signature: `public class ${match[2]}`,
    })
  }

  // Extract public interfaces
  const ifaceRegex = /public\s+interface\s+(\w+)/g
  while ((match = ifaceRegex.exec(source)) !== null) {
    elements.push({ kind: 'interface', name: match[1], signature: `public interface ${match[1]}` })
  }

  // Extract public enums
  const enumRegex = /public\s+enum\s+(\w+)/g
  while ((match = enumRegex.exec(source)) !== null) {
    elements.push({ kind: 'enum', name: match[1], signature: `public enum ${match[1]}` })
  }

  // Extract public methods
  const methodRegex = /\/\/\/\s*<summary>\s*([\s\S]*?)<\/summary>[\s\S]*?public\s+(?:async\s+)?(\S+)\s+(\w+)\(([^)]*)\)/g
  while ((match = methodRegex.exec(source)) !== null) {
    elements.push({
      kind: 'method',
      name: match[3],
      docstring: match[1].replace(/\s*\/\/\/\s*/g, '').trim(),
      returnType: match[2],
      signature: `public ${match[2]} ${match[3]}(${match[4]})`,
    })
  }

  return { filePath, elements }
}

// Run the simulation
try {
  const testPaths = [
    tmpFile,
    path.join(tmpDir, 'PaymentProcessorTests.cs'),
    path.join(tmpDir, 'PaymentProcessorTest.cs'),
  ]

  console.log('=== CSharpScanner.canHandle() ===')
  for (const p of testPaths) {
    console.log(`${path.basename(p)}: ${canHandle(p)}`)
  }
  // PaymentProcessor.cs: true
  // PaymentProcessorTests.cs: false
  // PaymentProcessorTest.cs: false

  console.log('\n=== CSharpScanner.scan() ===')
  const result = simulateScan(tmpFile)
  console.log(`Scanned: ${path.basename(result.filePath)}`)
  console.log(`Extracted ${result.elements.length} API elements:\n`)
  for (const el of result.elements) {
    console.log(`[${el.kind}] ${el.name}`)
    console.log(`  Signature : ${el.signature}`)
    if (el.docstring) console.log(`  Docstring : ${el.docstring}`)
    if (el.returnType) console.log(`  Returns   : ${el.returnType}`)
    console.log()
  }
} catch (err) {
  console.error('Scanner simulation failed:', err.message)
} finally {
  fs.unlinkSync(tmpFile)
}
TypeScript

Methods

canHandle

canHandle(filePath: string): boolean
TypeScript

Use canHandle to determine whether CSharpScanner should process a given file before scanning begins.

Call this during file discovery to filter your file list down to only the C# source files the scanner can meaningfully parse. It's the first check in any scanning workflow — if it returns false, skip the file entirely rather than passing it to the scanner.

canHandle returns true only for files ending in .cs that aren't test files. It automatically excludes files ending in Test.cs or Tests.cs, and any file living under a /Tests/ directory — so your generated docs stay free of test-only artifacts.

Parameters

NameTypeRequiredDescription
filePathstringYesAbsolute or relative path to the file being evaluated. The full path is checked, so directory segments like /Tests/ are caught even if the filename itself looks like a source file.

Returns

Returns true if the file is a non-test C# source file that CSharpScanner can extract classes, interfaces, enums, structs, records, and methods from. Returns false for test files and any non-.cs file. Use the result to gate calls to the scanner's scan() method — only pass files where canHandle returned true.

Heads up

  • Path matching is case-sensitive, so .CS or .Cs extensions won't match — normalize file paths to lowercase first if your environment produces mixed-case paths.
  • The /Tests/ directory check matches anywhere in the path, so src/Tests/Helpers/Fixture.cs is excluded even though Fixture.cs doesn't look like a test file by name alone.

Example:

// Self-contained example — CSharpScanner behavior inlined directly

interface Scanner {
  languages: string[];
  canHandle(filePath: string): boolean;
}

class CSharpScanner implements Scanner {
  languages = ['csharp'];

  canHandle(filePath: string): boolean {
    return (
      /\.cs$/.test(filePath) &&
      !filePath.endsWith('Test.cs') &&
      !filePath.endsWith('Tests.cs') &&
      !filePath.includes('/Tests/')
    );
  }
}

const scanner = new CSharpScanner();

const filePaths = [
  'src/Services/PaymentService.cs',       // ✅ valid source file
  'src/Models/Invoice.cs',                // ✅ valid source file
  'src/Services/PaymentServiceTest.cs',   // ❌ excluded: ends in Test.cs
  'src/Services/PaymentServiceTests.cs',  // ❌ excluded: ends in Tests.cs
  'src/Tests/Helpers/MockClient.cs',      // ❌ excluded: lives under /Tests/
  'src/Services/PaymentService.ts',       // ❌ excluded: not a .cs file
];

const scannable = filePaths.filter((filePath) => scanner.canHandle(filePath));

console.log('Files queued for scanning:', scannable);
// Files queued for scanning: [
//   'src/Services/PaymentService.cs',
//   'src/Models/Invoice.cs'
// ]
TypeScript

scanFile

async scanFile(filePath: string): Promise<ScanResult>
TypeScript

Use scanFile to extract all public API elements from a single C# source file, producing structured metadata ready for documentation generation.

Reach for this when you need to scan a specific file rather than an entire directory — for example, when watching for file changes and regenerating docs incrementally, or when debugging why a particular class isn't appearing in your output.

CSharpScanner reads the file from disk, parses class definitions, methods, properties, and their signatures, then returns a structured result you can pass directly into skrypt's documentation pipeline. Test files (paths containing \Tests\ or \test\) are excluded automatically.

Parameters

NameTypeRequiredDescription
filePathstringYesAbsolute or relative path to the .cs file to scan. Must be readable by the current process — relative paths resolve from the working directory where skrypt is running.

Returns

Returns a Promise<ScanResult> containing an elements array of discovered API members and an errors array of any parse failures encountered. Pass elements to skrypt's doc generation step, or inspect errors first to catch malformed signatures before they produce incomplete documentation.

Heads up

  • The scanner reads the file synchronously under the hood, so very large files (>10MB) will block the event loop briefly. Split unusually large generated files before scanning.
  • Paths containing \Tests\ or \test\ return an empty elements array without throwing — if your scan result looks unexpectedly empty, check whether the path matches either pattern.

Example:

import { readFileSync } from 'fs'
import { join } from 'path'

// Inline types matching the real ScanResult / APIElement shape
interface Parameter {
  name: string
  type: string
  description: string
}

interface APIElement {
  name: string
  type: 'class' | 'method' | 'property' | 'constructor'
  signature: string
  parameters: Parameter[]
  returnType?: string
  description: string
  filePath: string
  lineNumber: number
  sourceContext: string
}

interface ScanResult {
  elements: APIElement[]
  errors: string[]
}

// Minimal CSharpScanner mock — mirrors the real implementation's behavior
class CSharpScanner {
  async scanFile(filePath: string): Promise<ScanResult> {
    const elements: APIElement[] = []
    const errors: string[] = []

    if (filePath.includes('\\Tests\\') || filePath.includes('\\test\\')) {
      return { elements, errors }
    }

    let source: string
    try {
      source = readFileSync(filePath, 'utf-8')
    } catch (err) {
      errors.push(`Failed to read file: ${filePath}`)
      return { elements, errors }
    }

    // Simplified extraction: find public class and method declarations
    const lines = source.split('\n')
    const classPattern = /public\s+(class|interface|record)\s+(\w+)/
    const methodPattern = /public\s+(\w[\w<>, ]*)\s+(\w+)\s*\(([^)]*)\)/

    lines.forEach((line, index) => {
      const classMatch = line.match(classPattern)
      if (classMatch) {
        elements.push({
          name: classMatch[2],
          type: 'class',
          signature: line.trim(),
          parameters: [],
          description: '',
          filePath,
          lineNumber: index + 1,
          sourceContext: lines.slice(Math.max(0, index - 1), index + 3).join('\n'),
        })
      }

      const methodMatch = line.match(methodPattern)
      if (methodMatch) {
        elements.push({
          name: methodMatch[2],
          type: 'method',
          signature: line.trim(),
          returnType: methodMatch[1],
          parameters: [],
          description: '',
          filePath,
          lineNumber: index + 1,
          sourceContext: lines.slice(Math.max(0, index - 1), index + 3).join('\n'),
        })
      }
    })

    return { elements, errors }
  }
}

// Write a temporary C# file to scan
import { writeFileSync, unlinkSync } from 'fs'

const sampleCSharp = `
using System;

namespace Payments.Core
{
    public class PaymentProcessor
    {
        public async Task<PaymentResult> ChargeAsync(string customerId, decimal amount)
        {
            // implementation
        }

        public bool Refund(string transactionId)
        {
            // implementation
        }
    }
}
`

const tmpPath = join(process.cwd(), '_sample_payment_processor.cs')
writeFileSync(tmpPath, sampleCSharp, 'utf-8')

const scanner = new CSharpScanner()

try {
  const result = await scanner.scanFile(tmpPath)

  console.log(`Scanned successfully`)
  console.log(`Elements found: ${result.elements.length}`)
  console.log(`Errors: ${result.errors.length}`)
  console.log('\nDiscovered API elements:')
  result.elements.forEach(el => {
    console.log(`  [${el.type}] ${el.name} — line ${el.lineNumber}`)
  })

  // Expected output:
  // Scanned successfully
  // Elements found: 3
  // Errors: 0
  //
  // Discovered API elements:
  //   [class]  PaymentProcessor — line 5
  //   [method] ChargeAsync — line 7
  //   [method] Refund — line 12
} catch (err) {
  console.error('Scan failed:', err)
} finally {
  unlinkSync(tmpPath)
}
TypeScript
Was this helpful?