config.ts 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. /**
  2. * Configuration Management
  3. *
  4. * Load, save, and validate CodeGraph configuration.
  5. */
  6. import * as fs from 'fs';
  7. import * as path from 'path';
  8. import picomatch from 'picomatch';
  9. import { CodeGraphConfig, DEFAULT_CONFIG, Language, NodeKind } from './types';
  10. import { normalizePath } from './utils';
  11. /**
  12. * Configuration filename
  13. */
  14. export const CONFIG_FILENAME = 'config.json';
  15. /**
  16. * Get the config file path for a project
  17. */
  18. export function getConfigPath(projectRoot: string): string {
  19. return path.join(projectRoot, '.codegraph', CONFIG_FILENAME);
  20. }
  21. /**
  22. * Check if a regex pattern is safe from ReDoS attacks.
  23. *
  24. * Rejects patterns with nested quantifiers (e.g., (a+)+, (a*)*) which
  25. * are the primary source of catastrophic backtracking. Also rejects
  26. * excessively long patterns and validates compilability.
  27. */
  28. function isSafeRegex(pattern: string): boolean {
  29. // Reject excessively long patterns
  30. if (pattern.length > 500) return false;
  31. // Reject nested quantifiers: (...)+ followed by +, *, or {
  32. // These are the primary cause of catastrophic backtracking
  33. if (/([+*}])\s*[+*{]/.test(pattern)) return false;
  34. if (/\([^)]*[+*][^)]*\)[+*{]/.test(pattern)) return false;
  35. // Verify the pattern is a valid regex
  36. try {
  37. new RegExp(pattern);
  38. return true;
  39. } catch {
  40. return false;
  41. }
  42. }
  43. /**
  44. * Validate a configuration object
  45. */
  46. export function validateConfig(config: unknown): config is CodeGraphConfig {
  47. if (typeof config !== 'object' || config === null) {
  48. return false;
  49. }
  50. const c = config as Record<string, unknown>;
  51. // Required fields
  52. if (typeof c.version !== 'number') return false;
  53. if (typeof c.rootDir !== 'string') return false;
  54. if (!Array.isArray(c.include)) return false;
  55. if (!Array.isArray(c.exclude)) return false;
  56. if (!Array.isArray(c.languages)) return false;
  57. if (!Array.isArray(c.frameworks)) return false;
  58. if (typeof c.maxFileSize !== 'number') return false;
  59. if (typeof c.extractDocstrings !== 'boolean') return false;
  60. if (typeof c.trackCallSites !== 'boolean') return false;
  61. // Validate include/exclude are string arrays
  62. if (!c.include.every((p) => typeof p === 'string')) return false;
  63. if (!c.exclude.every((p) => typeof p === 'string')) return false;
  64. // Validate languages
  65. const validLanguages: Language[] = [
  66. 'typescript',
  67. 'javascript',
  68. 'python',
  69. 'go',
  70. 'rust',
  71. 'java',
  72. 'svelte',
  73. 'unknown',
  74. ];
  75. if (!c.languages.every((l) => validLanguages.includes(l as Language))) return false;
  76. // Validate frameworks
  77. for (const fw of c.frameworks) {
  78. if (typeof fw !== 'object' || fw === null) return false;
  79. const framework = fw as Record<string, unknown>;
  80. if (typeof framework.name !== 'string') return false;
  81. }
  82. // Validate custom patterns if present
  83. if (c.customPatterns !== undefined) {
  84. if (!Array.isArray(c.customPatterns)) return false;
  85. for (const pattern of c.customPatterns) {
  86. if (typeof pattern !== 'object' || pattern === null) return false;
  87. const p = pattern as Record<string, unknown>;
  88. if (typeof p.name !== 'string') return false;
  89. if (typeof p.pattern !== 'string') return false;
  90. if (typeof p.kind !== 'string') return false;
  91. // Validate regex is compilable and reject patterns with known ReDoS risks
  92. if (!isSafeRegex(p.pattern)) return false;
  93. }
  94. }
  95. return true;
  96. }
  97. /**
  98. * Merge configuration with defaults
  99. */
  100. function mergeConfig(
  101. defaults: CodeGraphConfig,
  102. overrides: Partial<CodeGraphConfig>
  103. ): CodeGraphConfig {
  104. return {
  105. version: overrides.version ?? defaults.version,
  106. rootDir: overrides.rootDir ?? defaults.rootDir,
  107. include: overrides.include ?? defaults.include,
  108. exclude: overrides.exclude ?? defaults.exclude,
  109. languages: overrides.languages ?? defaults.languages,
  110. frameworks: overrides.frameworks ?? defaults.frameworks,
  111. maxFileSize: overrides.maxFileSize ?? defaults.maxFileSize,
  112. extractDocstrings: overrides.extractDocstrings ?? defaults.extractDocstrings,
  113. trackCallSites: overrides.trackCallSites ?? defaults.trackCallSites,
  114. customPatterns: overrides.customPatterns ?? defaults.customPatterns,
  115. };
  116. }
  117. /**
  118. * Load configuration from a project
  119. */
  120. export function loadConfig(projectRoot: string): CodeGraphConfig {
  121. const configPath = getConfigPath(projectRoot);
  122. if (!fs.existsSync(configPath)) {
  123. // Return default config with adjusted rootDir
  124. return {
  125. ...DEFAULT_CONFIG,
  126. rootDir: projectRoot,
  127. };
  128. }
  129. try {
  130. const content = fs.readFileSync(configPath, 'utf-8');
  131. const parsed = JSON.parse(content) as unknown;
  132. // Merge with defaults to ensure all fields are present
  133. const merged = mergeConfig(DEFAULT_CONFIG, parsed as Partial<CodeGraphConfig>);
  134. merged.rootDir = projectRoot; // Always use actual project root
  135. if (!validateConfig(merged)) {
  136. throw new Error('Invalid configuration format');
  137. }
  138. return merged;
  139. } catch (error) {
  140. if (error instanceof SyntaxError) {
  141. throw new Error(`Invalid JSON in config file: ${configPath}`);
  142. }
  143. throw error;
  144. }
  145. }
  146. /**
  147. * Save configuration to a project
  148. */
  149. export function saveConfig(projectRoot: string, config: CodeGraphConfig): void {
  150. const configPath = getConfigPath(projectRoot);
  151. const dir = path.dirname(configPath);
  152. // Ensure directory exists
  153. if (!fs.existsSync(dir)) {
  154. fs.mkdirSync(dir, { recursive: true });
  155. }
  156. // Create a copy without rootDir (it's always derived from project path)
  157. const toSave = { ...config };
  158. delete (toSave as Partial<CodeGraphConfig>).rootDir;
  159. const content = JSON.stringify(toSave, null, 2);
  160. // Atomic write: write to temp file then rename to prevent partial/corrupt configs
  161. const tmpPath = configPath + '.tmp';
  162. fs.writeFileSync(tmpPath, content, 'utf-8');
  163. fs.renameSync(tmpPath, configPath);
  164. }
  165. /**
  166. * Create default configuration for a new project
  167. */
  168. export function createDefaultConfig(projectRoot: string): CodeGraphConfig {
  169. return {
  170. ...DEFAULT_CONFIG,
  171. rootDir: projectRoot,
  172. };
  173. }
  174. /**
  175. * Update specific configuration values
  176. */
  177. export function updateConfig(
  178. projectRoot: string,
  179. updates: Partial<CodeGraphConfig>
  180. ): CodeGraphConfig {
  181. const current = loadConfig(projectRoot);
  182. const updated = mergeConfig(current, updates);
  183. updated.rootDir = projectRoot;
  184. saveConfig(projectRoot, updated);
  185. return updated;
  186. }
  187. /**
  188. * Add patterns to include list
  189. */
  190. export function addIncludePatterns(projectRoot: string, patterns: string[]): CodeGraphConfig {
  191. const config = loadConfig(projectRoot);
  192. const newPatterns = patterns.filter((p) => !config.include.includes(p));
  193. config.include = [...config.include, ...newPatterns];
  194. saveConfig(projectRoot, config);
  195. return config;
  196. }
  197. /**
  198. * Add patterns to exclude list
  199. */
  200. export function addExcludePatterns(projectRoot: string, patterns: string[]): CodeGraphConfig {
  201. const config = loadConfig(projectRoot);
  202. const newPatterns = patterns.filter((p) => !config.exclude.includes(p));
  203. config.exclude = [...config.exclude, ...newPatterns];
  204. saveConfig(projectRoot, config);
  205. return config;
  206. }
  207. /**
  208. * Add a custom pattern
  209. */
  210. export function addCustomPattern(
  211. projectRoot: string,
  212. name: string,
  213. pattern: string,
  214. kind: NodeKind
  215. ): CodeGraphConfig {
  216. const config = loadConfig(projectRoot);
  217. if (!config.customPatterns) {
  218. config.customPatterns = [];
  219. }
  220. // Check for duplicate name
  221. const existing = config.customPatterns.find((p) => p.name === name);
  222. if (existing) {
  223. existing.pattern = pattern;
  224. existing.kind = kind;
  225. } else {
  226. config.customPatterns.push({ name, pattern, kind });
  227. }
  228. saveConfig(projectRoot, config);
  229. return config;
  230. }
  231. /**
  232. * Check if a file path matches the include/exclude patterns
  233. */
  234. export function shouldIncludeFile(filePath: string, config: CodeGraphConfig): boolean {
  235. // Normalize to forward slashes so Windows backslash paths match glob patterns
  236. filePath = normalizePath(filePath);
  237. // Simple glob matching (for now, just check if any pattern matches)
  238. // A full implementation would use a proper glob library
  239. const matchesPattern = (pattern: string, filePath: string): boolean => {
  240. return picomatch.isMatch(filePath, pattern, { dot: true });
  241. };
  242. // Check exclude patterns first
  243. for (const pattern of config.exclude) {
  244. if (matchesPattern(pattern, filePath)) {
  245. return false;
  246. }
  247. }
  248. // Check include patterns
  249. for (const pattern of config.include) {
  250. if (matchesPattern(pattern, filePath)) {
  251. return true;
  252. }
  253. }
  254. // Default to not including if no pattern matches
  255. return false;
  256. }