diff --git a/gitnexus-shared/src/language-detection.ts b/gitnexus-shared/src/language-detection.ts index f9073b8c0..76e314f22 100644 --- a/gitnexus-shared/src/language-detection.ts +++ b/gitnexus-shared/src/language-detection.ts @@ -43,6 +43,7 @@ const EXTENSION_MAP: Record = { [SupportedLanguages.Dart]: ['.dart'], [SupportedLanguages.Vue]: ['.vue'], [SupportedLanguages.Cobol]: ['.cbl', '.cob', '.cpy', '.cobol'], + [SupportedLanguages.Julia]: ['.jl'], } satisfies Record; // Ensure exhaustiveness /** Pre-built reverse lookup: extension → language (built once at module load). */ @@ -111,6 +112,7 @@ const SYNTAX_MAP: Record = { [SupportedLanguages.Dart]: 'dart', [SupportedLanguages.Vue]: 'typescript', [SupportedLanguages.Cobol]: 'cobol', + [SupportedLanguages.Julia]: 'julia', } satisfies Record; // Ensure exhaustiveness /** Non-code file extensions → Prism-compatible syntax identifiers */ diff --git a/gitnexus-shared/src/languages.ts b/gitnexus-shared/src/languages.ts index 29edb8d1e..8fb5891f6 100644 --- a/gitnexus-shared/src/languages.ts +++ b/gitnexus-shared/src/languages.ts @@ -22,4 +22,5 @@ export enum SupportedLanguages { Vue = 'vue', /** Standalone regex processor — no tree-sitter, no LanguageProvider. */ Cobol = 'cobol', + Julia = 'julia', } diff --git a/gitnexus-shared/src/scope-resolution/language-classification.ts b/gitnexus-shared/src/scope-resolution/language-classification.ts index 10c556cda..0fc1b01b3 100644 --- a/gitnexus-shared/src/scope-resolution/language-classification.ts +++ b/gitnexus-shared/src/scope-resolution/language-classification.ts @@ -41,6 +41,7 @@ export const LanguageClassifications: Readonly true; /** Dart: public if no leading underscore (convention, same as Python). */ export const dartExportChecker: ExportChecker = (_node, name) => !name.startsWith('_'); + +/** + * Julia: walk ancestors looking for an export_statement. + * Julia uses explicit `export` keyword to mark public symbols. + * Symbols not in an export list are still accessible via qualified names + * but are not exported — we treat un-exported symbols as non-public. + * Fallback: if no export statement is found in the module, treat all + * non-underscore names as public (common for scripts without explicit exports). + */ +export const juliaExportChecker: ExportChecker = (node, name) => { + if (name.startsWith('_')) return false; + let current: SyntaxNode | null = node; + while (current) { + if (current.type === 'module_definition') { + // Check siblings inside module body for export_statement listing this name + const body = current.childForFieldName('body') ?? current; + for (let i = 0; i < body.childCount; i++) { + const child = body.child(i); + if (child?.type === 'export_statement' && child.text?.includes(name)) return true; + } + // Module found but name not in any export — treat as internal + return false; + } + current = current.parent; + } + // Top-level (script) — treat all non-underscore names as public + return true; +}; diff --git a/gitnexus/src/core/ingestion/field-extractors/configs/julia.ts b/gitnexus/src/core/ingestion/field-extractors/configs/julia.ts new file mode 100644 index 000000000..ecf6b0e9e --- /dev/null +++ b/gitnexus/src/core/ingestion/field-extractors/configs/julia.ts @@ -0,0 +1,58 @@ +// gitnexus/src/core/ingestion/field-extractors/configs/julia.ts +// Verified against tree-sitter-julia grammar + +import { SupportedLanguages } from 'gitnexus-shared'; +import type { FieldExtractionConfig } from '../generic.js'; + +/** + * Julia field extraction config. + * + * Julia struct fields appear as typed or untyped identifiers inside struct bodies: + * struct Point + * x::Float64 + * y::Float64 + * end + * + * In tree-sitter-julia the struct body contains field declarations. + * Each field is either an identifier or a typed_parameter node. + */ +export const juliaConfig: FieldExtractionConfig = { + language: SupportedLanguages.Julia, + typeDeclarationNodes: ['struct_definition'], + fieldNodeTypes: ['identifier', 'typed_parameter'], + bodyNodeTypes: ['field_declaration_list'], + defaultVisibility: 'public', + + extractName(node) { + if (node.type === 'identifier') return node.text; + if (node.type === 'typed_parameter') { + const nameNode = node.childForFieldName('name') ?? node.firstNamedChild; + return nameNode?.type === 'identifier' ? nameNode.text : undefined; + } + return undefined; + }, + + extractType(node) { + if (node.type === 'typed_parameter') { + const typeNode = node.childForFieldName('type') ?? node.namedChild(1); + return typeNode?.text?.trim(); + } + return undefined; + }, + + extractVisibility(_node) { + // Julia struct fields are always public (no access modifiers) + return 'public'; + }, + + isStatic(_node) { + return false; + }, + + isReadonly(_node) { + // Immutable structs (struct, not mutable struct) have readonly fields. + // The mutable keyword is on the parent struct_definition, not on individual fields. + // We return false here; the struct-level immutability is handled elsewhere. + return false; + }, +}; diff --git a/gitnexus/src/core/ingestion/import-resolvers/configs/julia.ts b/gitnexus/src/core/ingestion/import-resolvers/configs/julia.ts new file mode 100644 index 000000000..f91ccbf20 --- /dev/null +++ b/gitnexus/src/core/ingestion/import-resolvers/configs/julia.ts @@ -0,0 +1,33 @@ +/** + * Julia import resolution config. + * + * Julia supports two import forms: + * import Foo → namespace import (Foo.bar access) + * using Foo → wildcard import (all exported names in scope) + * using Foo: bar → named import + * import Foo: bar → named import + * + * Within a package, `include("file.jl")` is used for relative file inclusion. + * We handle include() as a special import form resolved via suffix matching. + */ + +import { SupportedLanguages } from 'gitnexus-shared'; +import type { ImportResolutionConfig, ImportResolverStrategy } from '../types.js'; +import { suffixResolve } from '../utils.js'; +import { createStandardStrategy } from '../standard.js'; + +/** Julia include("file.jl") resolution strategy. */ +export const juliaIncludeStrategy: ImportResolverStrategy = (rawImportPath, _filePath, ctx) => { + // Strip quotes and resolve as relative .jl path + const cleaned = rawImportPath.replace(/^["']|["']$/g, ''); + if (!cleaned.endsWith('.jl')) return null; + + const pathParts = cleaned.replace(/^\.\//, '').split('/').filter(Boolean); + const resolved = suffixResolve(pathParts, ctx.normalizedFileList, ctx.allFileList, ctx.index); + return resolved ? { kind: 'files', files: [resolved] } : null; +}; + +export const juliaImportConfig: ImportResolutionConfig = { + language: SupportedLanguages.Julia, + strategies: [juliaIncludeStrategy, createStandardStrategy(SupportedLanguages.Julia)], +}; diff --git a/gitnexus/src/core/ingestion/languages/index.ts b/gitnexus/src/core/ingestion/languages/index.ts index 6191e881b..eb1bf749b 100644 --- a/gitnexus/src/core/ingestion/languages/index.ts +++ b/gitnexus/src/core/ingestion/languages/index.ts @@ -25,6 +25,7 @@ import { swiftProvider } from './swift.js'; import { dartProvider } from './dart.js'; import { vueProvider } from './vue.js'; import { cobolProvider } from './cobol.js'; +import { juliaProvider } from './julia.js'; export const providers = { [SupportedLanguages.JavaScript]: javascriptProvider, @@ -43,6 +44,7 @@ export const providers = { [SupportedLanguages.Dart]: dartProvider, [SupportedLanguages.Vue]: vueProvider, [SupportedLanguages.Cobol]: cobolProvider, + [SupportedLanguages.Julia]: juliaProvider, } satisfies Record; /** Get provider by language enum (always succeeds for SupportedLanguages). */ diff --git a/gitnexus/src/core/ingestion/languages/julia.ts b/gitnexus/src/core/ingestion/languages/julia.ts new file mode 100644 index 000000000..6c22d0b4c --- /dev/null +++ b/gitnexus/src/core/ingestion/languages/julia.ts @@ -0,0 +1,108 @@ +/** + * Julia language provider. + * + * Julia uses wildcard-leaf import semantics: `using Foo` brings all exported + * names into scope in one hop. `import Foo` uses namespace access (Foo.bar). + * Within packages, `include("file.jl")` handles relative file inclusion. + * + * Key Julia traits: + * - importSemantics: 'wildcard-leaf' (using Foo / import Foo) + * - No mandatory visibility modifiers — export list controls public API + * - Multiple dispatch: functions specialize on argument types + * - Structs are class-like (no methods in body; methods defined externally) + */ + +import { SupportedLanguages } from 'gitnexus-shared'; +import { createClassExtractor } from '../class-extractors/generic.js'; +import { juliaClassConfig } from '../class-extractors/configs/julia.js'; +import { defineLanguage } from '../language-provider.js'; +import { typeConfig as juliaTypeConfig } from '../type-extractors/julia.js'; +import { juliaExportChecker } from '../export-detection.js'; +import { createImportResolver } from '../import-resolvers/resolver-factory.js'; +import { juliaImportConfig } from '../import-resolvers/configs/julia.js'; +import { JULIA_QUERIES } from '../tree-sitter-queries.js'; +import { createFieldExtractor } from '../field-extractors/generic.js'; +import { juliaConfig as juliaFieldConfig } from '../field-extractors/configs/julia.js'; +import { createMethodExtractor } from '../method-extractors/generic.js'; +import { juliaMethodConfig } from '../method-extractors/configs/julia.js'; +import { createVariableExtractor } from '../variable-extractors/generic.js'; +import { juliaVariableConfig } from '../variable-extractors/configs/julia.js'; +import { createCallExtractor } from '../call-extractors/generic.js'; +import { juliaCallConfig } from '../call-extractors/configs/julia.js'; +import { createHeritageExtractor } from '../heritage-extractors/generic.js'; + +const BUILT_INS: ReadonlySet = new Set([ + 'print', + 'println', + 'typeof', + 'length', + 'size', + 'push!', + 'pop!', + 'append!', + 'map', + 'filter', + 'reduce', + 'sum', + 'prod', + 'minimum', + 'maximum', + 'sort', + 'sort!', + 'collect', + 'enumerate', + 'zip', + 'keys', + 'values', + 'haskey', + 'get', + 'getindex', + 'setindex!', + 'error', + 'throw', + 'isa', + 'isdefined', + 'isnothing', + 'ismissing', + 'eltype', + 'zeros', + 'ones', + 'rand', + 'randn', + 'copy', + 'deepcopy', + 'string', + 'Symbol', + 'parse', + 'convert', + 'promote', + 'similar', + 'fill', + 'repeat', + 'reshape', + 'hcat', + 'vcat', + 'cat', + 'tuple', + 'nameof', + 'supertype', + 'subtypes', +]); + +export const juliaProvider = defineLanguage({ + id: SupportedLanguages.Julia, + extensions: ['.jl'], + entryPointPatterns: [/^main$/, /^run$/, /^execute$/], + treeSitterQueries: JULIA_QUERIES, + typeConfig: juliaTypeConfig, + exportChecker: juliaExportChecker, + importResolver: createImportResolver(juliaImportConfig), + importSemantics: 'wildcard-leaf', + callExtractor: createCallExtractor(juliaCallConfig), + fieldExtractor: createFieldExtractor(juliaFieldConfig), + methodExtractor: createMethodExtractor(juliaMethodConfig), + variableExtractor: createVariableExtractor(juliaVariableConfig), + classExtractor: createClassExtractor(juliaClassConfig), + heritageExtractor: createHeritageExtractor(SupportedLanguages.Julia), + builtInNames: BUILT_INS, +}); diff --git a/gitnexus/src/core/ingestion/method-extractors/configs/julia.ts b/gitnexus/src/core/ingestion/method-extractors/configs/julia.ts new file mode 100644 index 000000000..3d3315fd1 --- /dev/null +++ b/gitnexus/src/core/ingestion/method-extractors/configs/julia.ts @@ -0,0 +1,119 @@ +// gitnexus/src/core/ingestion/method-extractors/configs/julia.ts +// Verified against tree-sitter-julia grammar + +import { SupportedLanguages } from 'gitnexus-shared'; +import type { MethodExtractionConfig, ParameterInfo } from '../../method-types.js'; +import type { SyntaxNode } from '../../utils/ast-helpers.js'; + +/** + * Extract parameters from a Julia function definition. + * + * Julia parameters can be: + * - Plain identifier: `x` + * - Typed: `x::Int` + * - With default: `x=1` + * - Typed with default: `x::Int=1` + * - Splat: `args...` + * - Keyword: `; kw=default` + */ +function extractJuliaParameters(node: SyntaxNode): ParameterInfo[] { + const paramList = node.childForFieldName('parameters'); + if (!paramList) return []; + + const params: ParameterInfo[] = []; + + for (let i = 0; i < paramList.namedChildCount; i++) { + const param = paramList.namedChild(i); + if (!param) continue; + + switch (param.type) { + case 'identifier': { + params.push({ name: param.text, type: null, rawType: null, isOptional: false, isVariadic: false }); + break; + } + case 'typed_parameter': { + const nameNode = param.childForFieldName('name') ?? param.firstNamedChild; + const typeNode = param.childForFieldName('type') ?? param.namedChild(1); + if (nameNode) { + params.push({ + name: nameNode.text, + type: typeNode?.text?.trim() ?? null, + rawType: typeNode?.text?.trim() ?? null, + isOptional: false, + isVariadic: false, + }); + } + break; + } + case 'optional_parameter': { + const nameNode = param.childForFieldName('name') ?? param.firstNamedChild; + const typeNode = param.childForFieldName('type'); + if (nameNode) { + params.push({ + name: nameNode.text, + type: typeNode?.text?.trim() ?? null, + rawType: typeNode?.text?.trim() ?? null, + isOptional: true, + isVariadic: false, + }); + } + break; + } + case 'splat_parameter': { + const nameNode = param.firstNamedChild; + if (nameNode) { + params.push({ name: nameNode.text, type: null, rawType: null, isOptional: false, isVariadic: true }); + } + break; + } + default: + break; + } + } + return params; +} + +function extractJuliaReturnType(node: SyntaxNode): string | undefined { + const retType = node.childForFieldName('return_type'); + return retType?.text?.trim(); +} + +export const juliaMethodConfig: MethodExtractionConfig = { + language: SupportedLanguages.Julia, + // Julia methods are defined outside structs — no struct body method extraction + typeDeclarationNodes: ['struct_definition'], + methodNodeTypes: ['function_definition', 'short_function_definition'], + bodyNodeTypes: ['field_declaration_list'], + + extractName(node) { + const nameNode = node.childForFieldName('name'); + return nameNode?.text; + }, + + extractReturnType: extractJuliaReturnType, + extractParameters: extractJuliaParameters, + + extractVisibility(_node) { + return 'public'; + }, + + isStatic(_node) { + return false; + }, + + isAbstract(_node) { + return false; + }, + + isFinal(_node) { + return false; + }, + + extractAnnotations(_node) { + return []; + }, + + isAsync(_node) { + return false; + }, +}; diff --git a/gitnexus/src/core/ingestion/tree-sitter-queries.ts b/gitnexus/src/core/ingestion/tree-sitter-queries.ts index f67823c8c..7483e2d11 100644 --- a/gitnexus/src/core/ingestion/tree-sitter-queries.ts +++ b/gitnexus/src/core/ingestion/tree-sitter-queries.ts @@ -1512,6 +1512,61 @@ export const DART_QUERIES = ` (type_identifier) @heritage.trait))) @heritage `; +// Julia queries - works with tree-sitter-julia +export const JULIA_QUERIES = ` +; ── Structs (class-like types) ─────────────────────────────────────────────── +(struct_definition + name: (identifier) @name) @definition.class + +; ── Abstract types ──────────────────────────────────────────────────────────── +(abstract_definition + name: (identifier) @name) @definition.class + +; ── Modules ─────────────────────────────────────────────────────────────────── +(module_definition + name: (identifier) @name) @definition.module + +; ── Function definitions ────────────────────────────────────────────────────── +(function_definition + name: (identifier) @name) @definition.function + +; ── Short-form function definitions: f(x) = expr ───────────────────────────── +(short_function_definition + name: (identifier) @name) @definition.function + +; ── Macro definitions ───────────────────────────────────────────────────────── +(macro_definition + name: (identifier) @name) @definition.function + +; ── Import statements ───────────────────────────────────────────────────────── +(import_statement) @import + +(using_statement) @import + +; ── Call expressions ────────────────────────────────────────────────────────── +(call_expression + (identifier) @call.name) @call + +(call_expression + (field_expression + (identifier) @call.name)) @call + +; ── Heritage: struct Foo <: Bar ─────────────────────────────────────────────── +(struct_definition + name: (identifier) @heritage.class + supertype: (type_clause + (identifier) @heritage.extends)) @heritage + +(abstract_definition + name: (identifier) @heritage.class + supertype: (type_clause + (identifier) @heritage.extends)) @heritage + +; ── Variable assignments at module scope ────────────────────────────────────── +(assignment + left: (identifier) @name) @definition.variable +`; + import { SupportedLanguages } from 'gitnexus-shared'; export const LANGUAGE_QUERIES: Record = { @@ -1531,4 +1586,5 @@ export const LANGUAGE_QUERIES: Record = { [SupportedLanguages.Dart]: DART_QUERIES, [SupportedLanguages.Vue]: TYPESCRIPT_QUERIES, // Vue