Skip to content

Commit 8506936

Browse files
firehoopercolbymchenryclaude
authored
90 scala support (colbymchenry#91)
Co-authored-by: Colby McHenry <me@colbymchenry.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 00b2989 commit 8506936

6 files changed

Lines changed: 425 additions & 2 deletions

File tree

__tests__/extraction.test.ts

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3080,6 +3080,276 @@ describe('Directory Exclusion', () => {
30803080
});
30813081
});
30823082

3083+
// =============================================================================
3084+
// Scala
3085+
// =============================================================================
3086+
3087+
describe('Scala Extraction', () => {
3088+
describe('Language detection', () => {
3089+
it('should detect Scala files', () => {
3090+
expect(detectLanguage('Main.scala')).toBe('scala');
3091+
expect(detectLanguage('script.sc')).toBe('scala');
3092+
expect(detectLanguage('src/UserService.scala')).toBe('scala');
3093+
});
3094+
3095+
it('should report Scala as supported', () => {
3096+
expect(isLanguageSupported('scala')).toBe(true);
3097+
expect(getSupportedLanguages()).toContain('scala');
3098+
});
3099+
});
3100+
3101+
describe('Class extraction', () => {
3102+
it('should extract class definitions', () => {
3103+
const code = `
3104+
class UserService(private val repo: UserRepository) {
3105+
def findUser(id: String): Option[String] = Some(id)
3106+
}
3107+
`;
3108+
const result = extractFromSource('UserService.scala', code);
3109+
const cls = result.nodes.find((n) => n.kind === 'class' && n.name === 'UserService');
3110+
expect(cls).toBeDefined();
3111+
expect(cls?.language).toBe('scala');
3112+
});
3113+
3114+
it('should extract object definitions as class kind', () => {
3115+
const code = `
3116+
object DatabaseConfig {
3117+
val url = "jdbc:postgresql://localhost/mydb"
3118+
}
3119+
`;
3120+
const result = extractFromSource('Config.scala', code);
3121+
const obj = result.nodes.find((n) => n.kind === 'class' && n.name === 'DatabaseConfig');
3122+
expect(obj).toBeDefined();
3123+
});
3124+
3125+
it('should extract trait definitions as trait kind', () => {
3126+
const code = `
3127+
trait Repository[A] {
3128+
def findById(id: String): Option[A]
3129+
def save(entity: A): Unit
3130+
}
3131+
`;
3132+
const result = extractFromSource('Repository.scala', code);
3133+
const trait_ = result.nodes.find((n) => n.kind === 'trait' && n.name === 'Repository');
3134+
expect(trait_).toBeDefined();
3135+
});
3136+
});
3137+
3138+
describe('Method and function extraction', () => {
3139+
it('should extract method definitions inside a class', () => {
3140+
const code = `
3141+
class Calculator {
3142+
def add(a: Int, b: Int): Int = a + b
3143+
def divide(a: Double, b: Double): Double = a / b
3144+
}
3145+
`;
3146+
const result = extractFromSource('Calculator.scala', code);
3147+
const methods = result.nodes.filter((n) => n.kind === 'method');
3148+
expect(methods.find((m) => m.name === 'add')).toBeDefined();
3149+
expect(methods.find((m) => m.name === 'divide')).toBeDefined();
3150+
});
3151+
3152+
it('should extract method signatures', () => {
3153+
const code = `
3154+
class Greeter {
3155+
def greet(name: String): String = s"Hello, \${name}!"
3156+
}
3157+
`;
3158+
const result = extractFromSource('Greeter.scala', code);
3159+
const method = result.nodes.find((n) => n.name === 'greet');
3160+
expect(method?.signature).toContain('name: String');
3161+
expect(method?.signature).toContain('String');
3162+
});
3163+
3164+
it('should extract top-level function definitions as functions', () => {
3165+
const code = `
3166+
def factorial(n: Int): Int = if (n <= 1) 1 else n * factorial(n - 1)
3167+
def greet(name: String): String = s"Hello, \${name}!"
3168+
`;
3169+
const result = extractFromSource('utils.scala', code);
3170+
const fns = result.nodes.filter((n) => n.kind === 'function');
3171+
expect(fns.find((f) => f.name === 'factorial')).toBeDefined();
3172+
expect(fns.find((f) => f.name === 'greet')).toBeDefined();
3173+
});
3174+
});
3175+
3176+
describe('Val and var extraction', () => {
3177+
it('should extract val inside a class as field', () => {
3178+
const code = `
3179+
class Config {
3180+
val timeout: Int = 30
3181+
val host: String = "localhost"
3182+
}
3183+
`;
3184+
const result = extractFromSource('Config.scala', code);
3185+
const fields = result.nodes.filter((n) => n.kind === 'field');
3186+
expect(fields.find((f) => f.name === 'timeout')).toBeDefined();
3187+
expect(fields.find((f) => f.name === 'host')).toBeDefined();
3188+
});
3189+
3190+
it('should extract var inside a class as field', () => {
3191+
const code = `
3192+
class Counter {
3193+
var count: Int = 0
3194+
}
3195+
`;
3196+
const result = extractFromSource('Counter.scala', code);
3197+
const field = result.nodes.find((n) => n.kind === 'field' && n.name === 'count');
3198+
expect(field).toBeDefined();
3199+
});
3200+
3201+
it('should extract top-level val as constant', () => {
3202+
const code = `
3203+
val MaxConnections: Int = 100
3204+
val DefaultTimeout = 30
3205+
`;
3206+
const result = extractFromSource('constants.scala', code);
3207+
const consts = result.nodes.filter((n) => n.kind === 'constant');
3208+
expect(consts.find((c) => c.name === 'MaxConnections')).toBeDefined();
3209+
});
3210+
3211+
it('should extract top-level var as variable', () => {
3212+
const code = `
3213+
var retries: Int = 3
3214+
`;
3215+
const result = extractFromSource('state.scala', code);
3216+
const v = result.nodes.find((n) => n.kind === 'variable' && n.name === 'retries');
3217+
expect(v).toBeDefined();
3218+
});
3219+
3220+
it('should include type in val/var signature', () => {
3221+
const code = `
3222+
class Service {
3223+
val timeout: Int = 30
3224+
}
3225+
`;
3226+
const result = extractFromSource('Service.scala', code);
3227+
const field = result.nodes.find((n) => n.name === 'timeout');
3228+
expect(field?.signature).toContain('timeout');
3229+
expect(field?.signature).toContain('Int');
3230+
});
3231+
});
3232+
3233+
describe('Enum extraction', () => {
3234+
it('should extract enum definitions', () => {
3235+
const code = `
3236+
enum Color:
3237+
case Red
3238+
case Green
3239+
case Blue
3240+
`;
3241+
const result = extractFromSource('Color.scala', code);
3242+
const enumNode = result.nodes.find((n) => n.kind === 'enum' && n.name === 'Color');
3243+
expect(enumNode).toBeDefined();
3244+
});
3245+
3246+
it('should extract enum cases as enum_member', () => {
3247+
const code = `
3248+
enum Direction:
3249+
case North
3250+
case South
3251+
case East
3252+
case West
3253+
`;
3254+
const result = extractFromSource('Direction.scala', code);
3255+
const members = result.nodes.filter((n) => n.kind === 'enum_member');
3256+
expect(members.find((m) => m.name === 'North')).toBeDefined();
3257+
expect(members.find((m) => m.name === 'South')).toBeDefined();
3258+
expect(members.length).toBeGreaterThanOrEqual(4);
3259+
});
3260+
});
3261+
3262+
describe('Type alias extraction', () => {
3263+
it('should extract type aliases', () => {
3264+
const code = `
3265+
type UserId = String
3266+
type UserMap = Map[String, String]
3267+
`;
3268+
const result = extractFromSource('types.scala', code);
3269+
const aliases = result.nodes.filter((n) => n.kind === 'type_alias');
3270+
expect(aliases.find((a) => a.name === 'UserId')).toBeDefined();
3271+
expect(aliases.find((a) => a.name === 'UserMap')).toBeDefined();
3272+
});
3273+
});
3274+
3275+
describe('Import extraction', () => {
3276+
it('should extract import declarations', () => {
3277+
const code = `
3278+
import scala.collection.mutable.ListBuffer
3279+
import scala.concurrent.Future
3280+
`;
3281+
const result = extractFromSource('imports.scala', code);
3282+
const imports = result.nodes.filter((n) => n.kind === 'import');
3283+
expect(imports.length).toBeGreaterThanOrEqual(2);
3284+
});
3285+
});
3286+
3287+
describe('Visibility modifiers', () => {
3288+
it('should extract private visibility', () => {
3289+
const code = `
3290+
class Service {
3291+
private val secret: String = "abc"
3292+
private def helper(): Unit = {}
3293+
}
3294+
`;
3295+
const result = extractFromSource('Service.scala', code);
3296+
const secretField = result.nodes.find((n) => n.name === 'secret');
3297+
expect(secretField?.visibility).toBe('private');
3298+
const helperMethod = result.nodes.find((n) => n.name === 'helper');
3299+
expect(helperMethod?.visibility).toBe('private');
3300+
});
3301+
3302+
it('should extract protected visibility', () => {
3303+
const code = `
3304+
class Base {
3305+
protected def helperMethod(): Unit = {}
3306+
}
3307+
`;
3308+
const result = extractFromSource('Base.scala', code);
3309+
const method = result.nodes.find((n) => n.name === 'helperMethod');
3310+
expect(method?.visibility).toBe('protected');
3311+
});
3312+
3313+
it('should default to public visibility', () => {
3314+
const code = `
3315+
class Greeter {
3316+
def hello(): Unit = {}
3317+
}
3318+
`;
3319+
const result = extractFromSource('Greeter.scala', code);
3320+
const method = result.nodes.find((n) => n.name === 'hello');
3321+
expect(method?.visibility).toBe('public');
3322+
});
3323+
});
3324+
3325+
describe('Inheritance', () => {
3326+
it('should extract extends relationships', () => {
3327+
const code = `
3328+
class AdminUser extends User {
3329+
def adminAction(): Unit = {}
3330+
}
3331+
`;
3332+
const result = extractFromSource('AdminUser.scala', code);
3333+
const extendsRefs = result.unresolvedReferences.filter((r) => r.referenceKind === 'extends');
3334+
expect(extendsRefs.find((r) => r.referenceName === 'User')).toBeDefined();
3335+
});
3336+
});
3337+
3338+
describe('Call extraction', () => {
3339+
it('should extract function call expressions', () => {
3340+
const code = `
3341+
def processData(): Unit = {
3342+
val result = computeResult()
3343+
println(result)
3344+
}
3345+
`;
3346+
const result = extractFromSource('processor.scala', code);
3347+
const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls');
3348+
expect(calls.length).toBeGreaterThan(0);
3349+
});
3350+
});
3351+
});
3352+
30833353
describe('Vue Extraction', () => {
30843354
it('should detect Vue files', () => {
30853355
expect(detectLanguage('App.vue')).toBe('vue');

src/extraction/grammars.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const WASM_GRAMMAR_FILES: Record<GrammarLanguage, string> = {
3434
kotlin: 'tree-sitter-kotlin.wasm',
3535
dart: 'tree-sitter-dart.wasm',
3636
pascal: 'tree-sitter-pascal.wasm',
37+
scala: 'tree-sitter-scala.wasm',
3738
};
3839

3940
/**
@@ -75,6 +76,8 @@ export const EXTENSION_MAP: Record<string, Language> = {
7576
'.lpr': 'pascal',
7677
'.dfm': 'pascal',
7778
'.fmx': 'pascal',
79+
'.scala': 'scala',
80+
'.sc': 'scala',
7881
};
7982

8083
/**
@@ -122,8 +125,8 @@ export async function loadGrammarsForLanguages(languages: Language[]): Promise<v
122125
for (const lang of toLoad) {
123126
const wasmFile = WASM_GRAMMAR_FILES[lang];
124127
try {
125-
// Pascal ships its own WASM (not in tree-sitter-wasms)
126-
const wasmPath = lang === 'pascal'
128+
// Pascal and Scala ship their own WASMs (not in tree-sitter-wasms)
129+
const wasmPath = (lang === 'pascal' || lang === 'scala')
127130
? path.join(__dirname, 'wasm', wasmFile)
128131
: require.resolve(`tree-sitter-wasms/out/${wasmFile}`);
129132
const language = await WasmLanguage.load(wasmPath);
@@ -287,6 +290,7 @@ export function getLanguageDisplayName(language: Language): string {
287290
vue: 'Vue',
288291
liquid: 'Liquid',
289292
pascal: 'Pascal / Delphi',
293+
scala: 'Scala',
290294
unknown: 'Unknown',
291295
};
292296
return names[language] || language;

src/extraction/languages/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { swiftExtractor } from './swift';
2222
import { kotlinExtractor } from './kotlin';
2323
import { dartExtractor } from './dart';
2424
import { pascalExtractor } from './pascal';
25+
import { scalaExtractor } from './scala';
2526

2627
export const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
2728
typescript: typescriptExtractor,
@@ -41,4 +42,5 @@ export const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
4142
kotlin: kotlinExtractor,
4243
dart: dartExtractor,
4344
pascal: pascalExtractor,
45+
scala: scalaExtractor,
4446
};

0 commit comments

Comments
 (0)