OverviewFilesCommitsIssuesDocsReleasesPeers

codetree / lib / parsers / cpp.js

462 lines · javascript ref:
import { parser } from '../../vendor/lezer-cpp/lezer-cpp.mjs'

/**
 * C++ parser — walks the Lezer concrete syntax tree to extract:
 *  - #include "local.h" → imports (quoted, not <angle-bracket>)
 *  - class / struct definitions → classes with methods, inheritance
 *  - free functions → functions
 *  - namespaces → namespaces array
 *  - enums → enums array
 *  - typedef / using aliases → constants
 *  - #define macros → constants
 */

export function parseFile (source, filePath) {
  let tree
  try { tree = parser.parse(source) }
  catch (err) { return { error: err.message, filePath } }

  const result = {
    filePath,
    imports: [],
    exports: [],
    classes: [],
    functions: [],
    constants: [],
    namespaces: [],
    enums: []
  }

  // Out-of-line definitions (Class::method) may appear before their class; track them
  // and merge into the class if we encounter it.
  const outOfLineMethods = {}   // className -> [method,...]

  const cursor = tree.cursor()
  if (cursor.firstChild()) {
    do {
      visitTopLevel(cursor, source, result, outOfLineMethods)
    } while (cursor.nextSibling())
  }

  // Attach any out-of-line methods to their classes (class may have been visited earlier)
  for (const cls of result.classes) {
    const extra = outOfLineMethods[cls.name]
    if (extra) {
      for (const m of extra) cls.methods.push(m)
    }
  }

  return result
}

function visitTopLevel (cursor, source, result, outOfLineMethods) {
  switch (cursor.name) {
    case 'PreprocDirective':
      handlePreproc(cursor, source, result)
      return

    case 'NamespaceDefinition':
      handleNamespace(cursor, source, result, outOfLineMethods)
      return

    case 'ClassSpecifier':
    case 'StructSpecifier': {
      const cls = extractClass(cursor, source, cursor.name === 'StructSpecifier' ? 'struct' : 'class')
      if (cls) {
        result.classes.push(cls)
        result.exports.push({ name: cls.name, type: cls.kind })
      }
      return
    }

    case 'FunctionDefinition':
      handleFunctionDefinition(cursor, source, result, outOfLineMethods)
      return

    case 'EnumSpecifier':
      handleEnum(cursor, source, result)
      return

    case 'TypeDefinition':
    case 'AliasDeclaration': {
      const name = findTypeIdentifier(cursor, source)
      if (name) result.constants.push({ name, kind: 'typedef' })
      return
    }

    case 'Declaration':
      // GDCLASS(Name, Base); lives here. We can ignore most declarations but
      // preserve top-level function declarations.
      handleDeclaration(cursor, source, result)
      return
  }
}

// --- #include handling ---

function handlePreproc (cursor, source, result) {
  // #include followed by either SystemLibString (<...>) or String ("...")
  if (!cursor.firstChild()) return
  let isInclude = false
  try {
    do {
      if (cursor.name === '#include') isInclude = true
      else if (isInclude && cursor.name === 'String') {
        // Quoted include — local dependency
        const text = source.slice(cursor.from, cursor.to).replace(/^["<]|[">]$/g, '')
        result.imports.push({ specifier: text, names: [] })
      }
      // SystemLibString (<...>) is skipped — system headers don't participate in the local graph
    } while (cursor.nextSibling())
  } finally {
    cursor.parent()
  }
}

// --- Namespace ---

function handleNamespace (cursor, source, result, outOfLineMethods) {
  if (!cursor.firstChild()) return
  let nsName = null
  try {
    do {
      if (cursor.name === 'Identifier' && !nsName) {
        const text = source.slice(cursor.from, cursor.to)
        if (text !== 'namespace') nsName = text
      } else if (cursor.name === 'CompoundStatement') {
        // Walk the body as if it were top-level
        if (cursor.firstChild()) {
          do { visitTopLevel(cursor, source, result, outOfLineMethods) } while (cursor.nextSibling())
          cursor.parent()
        }
      }
    } while (cursor.nextSibling())
  } finally {
    cursor.parent()
  }
  if (nsName && !result.namespaces.includes(nsName)) result.namespaces.push(nsName)
}

// --- Class / struct ---

function extractClass (cursor, source, kind) {
  const cls = {
    name: null,
    kind,
    extends: null,
    line: getLine(source, cursor.from),
    methods: [],
    getters: [],
    staticMethods: [],
    constructor: null
  }

  if (!cursor.firstChild()) return null
  try {
    do {
      if (cursor.name === 'TypeIdentifier' && !cls.name) {
        cls.name = source.slice(cursor.from, cursor.to)
      } else if (cursor.name === 'BaseClassClause') {
        cls.extends = extractBaseClass(cursor, source)
      } else if (cursor.name === 'FieldDeclarationList') {
        extractFieldDeclarations(cursor, source, cls)
      }
    } while (cursor.nextSibling())
  } finally {
    cursor.parent()
  }

  return cls.name ? cls : null
}

function extractBaseClass (cursor, source) {
  // BaseClassClause contains Access + TypeIdentifier (or just TypeIdentifier)
  if (!cursor.firstChild()) return null
  try {
    do {
      if (cursor.name === 'TypeIdentifier') {
        return source.slice(cursor.from, cursor.to)
      }
    } while (cursor.nextSibling())
  } finally {
    cursor.parent()
  }
  return null
}

function extractFieldDeclarations (cursor, source, cls) {
  if (!cursor.firstChild()) return
  let currentAccess = 'private'  // default for class, public for struct — not enforced here
  try {
    do {
      if (cursor.name === 'AccessSpecifier') {
        const text = source.slice(cursor.from, cursor.to)
        if (text.startsWith('public')) currentAccess = 'public'
        else if (text.startsWith('protected')) currentAccess = 'protected'
        else currentAccess = 'private'
      } else if (cursor.name === 'FieldDeclaration' || cursor.name === 'FunctionDefinition') {
        const isStatic = hasChildWithText(cursor, source, 'static')
        const decl = extractFieldOrMethod(cursor, source, cls.name)
        if (decl) {
          decl.access = currentAccess
          if (decl.isMethod) {
            const m = { name: decl.name, params: decl.params, line: decl.line }
            if (isStatic) {
              m.static = true
              cls.staticMethods.push(m)
            } else if (decl.name === cls.name) {
              cls.constructor = m
            } else {
              cls.methods.push(m)
            }
          }
          // Data fields: not separately tracked in the common interface; could add to state later
        }
      }
    } while (cursor.nextSibling())
  } finally {
    cursor.parent()
  }
}

function hasChildWithText (cursor, source, text) {
  if (!cursor.firstChild()) return false
  try {
    do {
      const t = source.slice(cursor.from, cursor.to)
      if (t === text) return true
    } while (cursor.nextSibling())
  } finally {
    cursor.parent()
  }
  return false
}

function extractFieldOrMethod (cursor, source, className) {
  // FieldDeclaration may contain a FunctionDeclarator (method) or just an identifier (data field).
  // FunctionDefinition contains a FunctionDeclarator + CompoundStatement (inline method body).
  // Return types like `Foo*` wrap the FunctionDeclarator in a PointerDeclarator — recurse through those.
  let name = null
  let params = []
  let isMethod = false
  const line = getLine(source, cursor.from)

  function visit () {
    if (!cursor.firstChild()) return
    try {
      do {
        if (cursor.name === 'FunctionDeclarator') {
          isMethod = true
          const fd = extractFunctionDeclarator(cursor, source)
          if (fd) { name = fd.name; params = fd.params }
        } else if (cursor.name === 'PointerDeclarator' || cursor.name === 'ReferenceDeclarator') {
          // Recurse: `Foo* create()` or `Foo& create()`
          visit()
        } else if ((cursor.name === 'FieldIdentifier' || cursor.name === 'Identifier') && !name) {
          name = source.slice(cursor.from, cursor.to)
        }
      } while (cursor.nextSibling())
    } finally {
      cursor.parent()
    }
  }

  visit()

  if (!name) return null
  return { name, params, isMethod, line }
}

function extractFunctionDeclarator (cursor, source) {
  let name = null
  const params = []
  let classPart = null  // for scoped names like ClassName::method

  if (!cursor.firstChild()) return null
  try {
    do {
      if (cursor.name === 'Identifier' && !name) {
        name = source.slice(cursor.from, cursor.to)
      } else if (cursor.name === 'FieldIdentifier' && !name) {
        name = source.slice(cursor.from, cursor.to)
      } else if (cursor.name === 'ScopedIdentifier') {
        // ClassName::method — extract both halves
        const parts = extractScoped(cursor, source)
        if (parts) { classPart = parts.ns; name = parts.name }
      } else if (cursor.name === 'ParameterList') {
        extractParameterList(cursor, source, params)
      }
    } while (cursor.nextSibling())
  } finally {
    cursor.parent()
  }

  if (!name) return null
  return { name, params, classPart }
}

function extractScoped (cursor, source) {
  // ScopedIdentifier: NamespaceIdentifier :: Identifier
  let ns = null
  let name = null
  if (!cursor.firstChild()) return null
  try {
    do {
      if (cursor.name === 'NamespaceIdentifier' && !ns) {
        ns = source.slice(cursor.from, cursor.to)
      } else if (cursor.name === 'Identifier') {
        name = source.slice(cursor.from, cursor.to)
      }
    } while (cursor.nextSibling())
  } finally {
    cursor.parent()
  }
  return (ns && name) ? { ns, name } : null
}

function extractParameterList (cursor, source, params) {
  if (!cursor.firstChild()) return
  try {
    do {
      if (cursor.name === 'ParameterDeclaration') {
        // Get last Identifier as param name (after the type)
        const pName = lastIdentifier(cursor, source)
        if (pName) params.push(pName)
      }
    } while (cursor.nextSibling())
  } finally {
    cursor.parent()
  }
}

function lastIdentifier (cursor, source) {
  let name = null
  if (!cursor.firstChild()) return null
  try {
    do {
      if (cursor.name === 'Identifier') {
        name = source.slice(cursor.from, cursor.to)
      }
    } while (cursor.nextSibling())
  } finally {
    cursor.parent()
  }
  return name
}

// --- Free function / out-of-line method definitions ---

function handleFunctionDefinition (cursor, source, result, outOfLineMethods) {
  // FunctionDefinition = return-type + FunctionDeclarator + CompoundStatement
  if (!cursor.firstChild()) return
  let fd = null
  const line = getLine(source, cursor.from)
  try {
    do {
      if (cursor.name === 'FunctionDeclarator') {
        fd = extractFunctionDeclarator(cursor, source)
      }
    } while (cursor.nextSibling())
  } finally {
    cursor.parent()
  }

  if (!fd) return

  if (fd.classPart) {
    // Out-of-line method: buffer until we process its class
    if (!outOfLineMethods[fd.classPart]) outOfLineMethods[fd.classPart] = []
    outOfLineMethods[fd.classPart].push({ name: fd.name, params: fd.params, line })
  } else {
    result.functions.push({ name: fd.name, params: fd.params, line })
  }
}

// --- Enum ---

function handleEnum (cursor, source, result) {
  let name = null
  const values = []
  if (!cursor.firstChild()) return
  try {
    do {
      if (cursor.name === 'TypeIdentifier' && !name) {
        name = source.slice(cursor.from, cursor.to)
      } else if (cursor.name === 'EnumeratorList') {
        if (cursor.firstChild()) {
          do {
            if (cursor.name === 'Enumerator') {
              const v = firstIdentifier(cursor, source)
              if (v) values.push(v)
            }
          } while (cursor.nextSibling())
          cursor.parent()
        }
      }
    } while (cursor.nextSibling())
  } finally {
    cursor.parent()
  }
  if (name) result.enums.push({ name, values })
}

// --- Declarations (catches #define macros via TypeDefinition, GDCLASS, etc.) ---

function handleDeclaration (cursor, source, result) {
  // Most non-function declarations are global variables or forward declarations.
  // We care about free function declarations (not definitions) at top level.
  if (!cursor.firstChild()) return
  let fd = null
  const line = getLine(source, cursor.from)
  try {
    do {
      if (cursor.name === 'FunctionDeclarator') {
        fd = extractFunctionDeclarator(cursor, source)
      }
    } while (cursor.nextSibling())
  } finally {
    cursor.parent()
  }

  if (fd && !fd.classPart) {
    // Only add if not already defined (avoid double-counting with definitions)
    if (!result.functions.find(f => f.name === fd.name)) {
      result.functions.push({ name: fd.name, params: fd.params, line })
    }
  }
}

// --- Helpers ---

function findTypeIdentifier (cursor, source) {
  if (!cursor.firstChild()) return null
  try {
    do {
      if (cursor.name === 'TypeIdentifier') {
        return source.slice(cursor.from, cursor.to)
      }
    } while (cursor.nextSibling())
  } finally {
    cursor.parent()
  }
  return null
}

function firstIdentifier (cursor, source) {
  if (!cursor.firstChild()) return null
  try {
    do {
      if (cursor.name === 'Identifier') {
        return source.slice(cursor.from, cursor.to)
      }
    } while (cursor.nextSibling())
  } finally {
    cursor.parent()
  }
  return null
}

function getLine (source, offset) {
  return source.slice(0, offset).split('\n').length
}