// Copyright 2025 Chris Bunting
// Brief: Configuration file parser for Static Analysis MCP Server
// Scope: Parses configuration files for various static analysis tools
import { readFileSync, existsSync } from 'fs';
import { join, dirname } from 'path';
import * as YAML from 'yaml';
import * as TOML from 'toml';
import * as xml2js from 'xml2js';
import { Language } from '@mcp-code-analysis/shared-types';
export interface ConfigFile {
path: string;
language: Language;
format: ConfigFormat;
content: any;
}
export enum ConfigFormat {
JSON = 'json',
YAML = 'yaml',
TOML = 'toml',
XML = 'xml',
INI = 'ini',
PROPERTIES = 'properties'
}
export class ConfigParser {
private configFiles: Map<Language, string[]> = new Map([
[Language.JAVASCRIPT, [
'.eslintrc',
'.eslintrc.json',
'.eslintrc.yaml',
'.eslintrc.yml',
'.eslintrc.js',
'package.json',
'.jshintrc'
]],
[Language.TYPESCRIPT, [
'tsconfig.json',
'.eslintrc',
'.eslintrc.json',
'.eslintrc.yaml',
'.eslintrc.yml',
'.eslintrc.js',
'package.json'
]],
[Language.PYTHON, [
'pylintrc',
'.pylintrc',
'setup.cfg',
'pyproject.toml',
'tox.ini',
'.flake8'
]],
[Language.JAVA, [
'pom.xml',
'build.gradle',
'build.gradle.kts',
'checkstyle.xml',
'pmd.xml',
'spotbugs.xml',
'.checkstyle'
]],
[Language.C, [
'.clang-format',
'.clang-tidy',
'Makefile',
'CMakeLists.txt'
]],
[Language.CPP, [
'.clang-format',
'.clang-tidy',
'Makefile',
'CMakeLists.txt',
'compile_commands.json'
]],
[Language.GO, [
'.golangci.yml',
'.golangci.yaml',
'.golangci-lint.yml',
'.golangci-lint.yaml',
'go.mod'
]],
[Language.RUST, [
'Cargo.toml',
'.clippy.toml',
'rustfmt.toml',
'.rustfmt.toml'
]]
]);
async findConfigFiles(projectPath: string, language?: Language): Promise<ConfigFile[]> {
const configFiles: ConfigFile[] = [];
const languages = language ? [language] : Array.from(this.configFiles.keys());
for (const lang of languages) {
const configFileNames = this.configFiles.get(lang) || [];
for (const configFileName of configFileNames) {
const configPath = join(projectPath, configFileName);
if (existsSync(configPath)) {
try {
const configFile = await this.parseConfigFile(configPath, lang);
configFiles.push(configFile);
} catch (error) {
// Skip invalid config files
console.warn(`Failed to parse config file ${configPath}:`, error);
}
}
}
}
return configFiles;
}
async parseConfigFile(filePath: string, language: Language): Promise<ConfigFile> {
const content = readFileSync(filePath, 'utf-8');
const format = this.detectConfigFormat(filePath, content);
const parsedContent = await this.parseContent(content, format);
return {
path: filePath,
language,
format,
content: parsedContent
};
}
private detectConfigFormat(filePath: string, content: string): ConfigFormat {
const extension = filePath.split('.').pop()?.toLowerCase();
const basename = filePath.split('/').pop() || '';
switch (extension) {
case 'json':
return ConfigFormat.JSON;
case 'yaml':
case 'yml':
return ConfigFormat.YAML;
case 'toml':
return ConfigFormat.TOML;
case 'xml':
return ConfigFormat.XML;
case 'ini':
return ConfigFormat.INI;
case 'properties':
return ConfigFormat.PROPERTIES;
default:
// Try to detect by content
if (content.trim().startsWith('{')) {
return ConfigFormat.JSON;
} else if (content.trim().startsWith('---')) {
return ConfigFormat.YAML;
} else if (content.includes(' = ')) {
return ConfigFormat.TOML;
} else if (content.trim().startsWith('<')) {
return ConfigFormat.XML;
} else if (content.includes('[') && content.includes(']')) {
return ConfigFormat.INI;
}
return ConfigFormat.JSON; // Default fallback
}
}
private async parseContent(content: string, format: ConfigFormat): Promise<any> {
switch (format) {
case ConfigFormat.JSON:
return JSON.parse(content);
case ConfigFormat.YAML:
return YAML.parse(content);
case ConfigFormat.TOML:
return TOML.parse(content);
case ConfigFormat.XML:
return new Promise((resolve, reject) => {
xml2js.parseString(content, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
});
case ConfigFormat.INI:
return this.parseIni(content);
case ConfigFormat.PROPERTIES:
return this.parseProperties(content);
default:
throw new Error(`Unsupported config format: ${format}`);
}
}
private parseIni(content: string): any {
const result: any = {};
const lines = content.split('\n');
let currentSection = '';
for (const line of lines) {
const trimmedLine = line.trim();
if (trimmedLine.startsWith('[') && trimmedLine.endsWith(']')) {
currentSection = trimmedLine.slice(1, -1);
result[currentSection] = result[currentSection] || {};
} else if (trimmedLine && !trimmedLine.startsWith(';') && !trimmedLine.startsWith('#')) {
const [key, ...valueParts] = trimmedLine.split('=');
if (key && valueParts.length > 0) {
const value = valueParts.join('=').trim();
if (currentSection) {
result[currentSection][key.trim()] = value;
} else {
result[key.trim()] = value;
}
}
}
}
return result;
}
private parseProperties(content: string): any {
const result: any = {};
const lines = content.split('\n');
for (const line of lines) {
const trimmedLine = line.trim();
if (trimmedLine && !trimmedLine.startsWith('#') && !trimmedLine.startsWith('!')) {
const [key, ...valueParts] = trimmedLine.split('=');
if (key && valueParts.length > 0) {
const value = valueParts.join('=').trim();
result[key.trim()] = value;
}
}
}
return result;
}
extractESLintConfig(configFile: ConfigFile): any {
if (configFile.language === Language.JAVASCRIPT || configFile.language === Language.TYPESCRIPT) {
if (configFile.path.endsWith('package.json')) {
return configFile.content.eslintConfig || {};
}
return configFile.content;
}
return {};
}
extractPylintConfig(configFile: ConfigFile): any {
if (configFile.language === Language.PYTHON) {
if (configFile.format === ConfigFormat.INI) {
return configFile.content;
} else if (configFile.format === ConfigFormat.TOML && configFile.content.tool?.pylint) {
return configFile.content.tool.pylint;
}
}
return {};
}
extractJavaConfig(configFile: ConfigFile): any {
if (configFile.language === Language.JAVA) {
if (configFile.path.endsWith('pom.xml')) {
return this.extractMavenConfig(configFile.content);
} else if (configFile.path.includes('gradle')) {
return this.extractGradleConfig(configFile.content);
}
}
return {};
}
private extractMavenConfig(pomContent: any): any {
const config: any = {};
if (pomContent.project?.properties) {
config.maven = pomContent.project.properties;
}
if (pomContent.project?.build?.plugins) {
const plugins = Array.isArray(pomContent.project.build.plugins.plugin)
? pomContent.project.build.plugins.plugin
: [pomContent.project.build.plugins.plugin];
config.plugins = plugins.map((plugin: any) => ({
groupId: plugin.groupId,
artifactId: plugin.artifactId,
version: plugin.version,
configuration: plugin.configuration
}));
}
return config;
}
private extractGradleConfig(gradleContent: any): any {
// This is a simplified extraction - in practice, Gradle files are Groovy/Kotlin scripts
// and would need more sophisticated parsing
return {
gradle: gradleContent
};
}
mergeConfigs(configFiles: ConfigFile[]): any {
const merged: any = {};
for (const configFile of configFiles) {
const languageConfig = this.extractLanguageConfig(configFile);
Object.assign(merged, languageConfig);
}
return merged;
}
private extractLanguageConfig(configFile: ConfigFile): any {
switch (configFile.language) {
case Language.JAVASCRIPT:
case Language.TYPESCRIPT:
return this.extractESLintConfig(configFile);
case Language.PYTHON:
return this.extractPylintConfig(configFile);
case Language.JAVA:
return this.extractJavaConfig(configFile);
default:
return configFile.content;
}
}
validateConfig(configFile: ConfigFile): boolean {
try {
switch (configFile.language) {
case Language.JAVASCRIPT:
case Language.TYPESCRIPT:
return this.validateESLintConfig(configFile);
case Language.PYTHON:
return this.validatePylintConfig(configFile);
case Language.JAVA:
return this.validateJavaConfig(configFile);
default:
return true;
}
} catch (error) {
return false;
}
}
private validateESLintConfig(configFile: ConfigFile): boolean {
const config = this.extractESLintConfig(configFile);
return typeof config === 'object' && config !== null;
}
private validatePylintConfig(configFile: ConfigFile): boolean {
const config = this.extractPylintConfig(configFile);
return typeof config === 'object' && config !== null;
}
private validateJavaConfig(configFile: ConfigFile): boolean {
const config = this.extractJavaConfig(configFile);
return typeof config === 'object' && config !== null;
}
}