thuban 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -65,12 +65,16 @@ class GhostCodeDetector {
65
65
  continue;
66
66
  }
67
67
 
68
+ // Strip comments and strings before counting references to avoid false negatives
69
+ // from functions that appear only in commented-out code
70
+ const strippedContent = this._stripCommentsAndStrings(content);
71
+
68
72
  for (const fn of functions) {
69
73
  if (fn.name.length < 3) continue; // Skip very short names (likely false positives)
70
74
 
71
75
  // Count how many times this function name appears (excluding its definition)
72
76
  const regex = new RegExp(`\\b${this._escapeRegex(fn.name)}\\b`, 'g');
73
- const matches = content.match(regex);
77
+ const matches = strippedContent.match(regex);
74
78
  if (matches) {
75
79
  const relPath = path.relative(this.rootPath, file);
76
80
  // Subtract 1 if this is the file where it's defined (the definition itself)
@@ -119,7 +123,7 @@ class GhostCodeDetector {
119
123
  _extractFunction(line, lineIndex, lines) {
120
124
  const trimmed = line.trim();
121
125
 
122
- // function name(
126
+ // JavaScript/TypeScript: function name(
123
127
  let match = trimmed.match(/^(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/);
124
128
  if (match) {
125
129
  return {
@@ -130,7 +134,7 @@ class GhostCodeDetector {
130
134
  };
131
135
  }
132
136
 
133
- // const name = (async) function
137
+ // JavaScript/TypeScript: const name = (async) function
134
138
  match = trimmed.match(/^(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?function/);
135
139
  if (match) {
136
140
  return {
@@ -141,7 +145,7 @@ class GhostCodeDetector {
141
145
  };
142
146
  }
143
147
 
144
- // const name = (...) =>
148
+ // JavaScript/TypeScript: const name = (...) =>
145
149
  match = trimmed.match(/^(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?\(?/);
146
150
  if (match && (trimmed.includes('=>') || (lineIndex + 1 < lines.length && lines[lineIndex + 1].includes('=>')))) {
147
151
  return {
@@ -152,7 +156,51 @@ class GhostCodeDetector {
152
156
  };
153
157
  }
154
158
 
155
- // class method: name(
159
+ // Kotlin: fun functionName(
160
+ match = trimmed.match(/^(?:private\s+|public\s+|internal\s+|protected\s+)?(?:suspend\s+)?fun\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*[(<]/);
161
+ if (match) {
162
+ return {
163
+ name: match[1],
164
+ type: 'kotlin-fun',
165
+ isExport: !trimmed.startsWith('private'),
166
+ isLifecycle: this._isLifecycle(match[1]),
167
+ };
168
+ }
169
+
170
+ // Rust: fn function_name(
171
+ match = trimmed.match(/^(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?fn\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*[(<]/);
172
+ if (match) {
173
+ return {
174
+ name: match[1],
175
+ type: 'rust-fn',
176
+ isExport: /^pub/.test(trimmed),
177
+ isLifecycle: this._isLifecycle(match[1]),
178
+ };
179
+ }
180
+
181
+ // PHP: function functionName( or public/private/protected function functionName(
182
+ match = trimmed.match(/^(?:(?:public|private|protected|static)\s+)*function\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/);
183
+ if (match) {
184
+ return {
185
+ name: match[1],
186
+ type: 'php-function',
187
+ isExport: !/private/.test(trimmed),
188
+ isLifecycle: this._isLifecycle(match[1]),
189
+ };
190
+ }
191
+
192
+ // Ruby: def method_name or def self.method_name
193
+ match = trimmed.match(/^def\s+(?:self\.)?([a-zA-Z_][a-zA-Z0-9_?!]*)\s*[(\s]?/);
194
+ if (match) {
195
+ return {
196
+ name: match[1],
197
+ type: 'ruby-def',
198
+ isExport: !match[1].startsWith('_'),
199
+ isLifecycle: this._isLifecycle(match[1]),
200
+ };
201
+ }
202
+
203
+ // Class method: name(
156
204
  match = trimmed.match(/^(?:async\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\([^)]*\)\s*{/);
157
205
  if (match && !['if', 'for', 'while', 'switch', 'catch', 'constructor', 'render', 'toString'].includes(match[1])) {
158
206
  return {
@@ -177,6 +225,7 @@ class GhostCodeDetector {
177
225
 
178
226
  _isLifecycle(name) {
179
227
  const lifecycle = [
228
+ // JS/TS lifecycle
180
229
  'constructor', 'render', 'componentDidMount', 'componentWillUnmount',
181
230
  'componentDidUpdate', 'shouldComponentUpdate', 'getSnapshotBeforeUpdate',
182
231
  'getDerivedStateFromProps', 'useEffect', 'useState', 'useMemo',
@@ -184,6 +233,15 @@ class GhostCodeDetector {
184
233
  'setup', 'teardown', 'beforeEach', 'afterEach', 'beforeAll', 'afterAll',
185
234
  'init', 'destroy', 'configure', 'bootstrap', 'main',
186
235
  'get', 'set', 'post', 'put', 'delete', 'patch', 'handle',
236
+ // Kotlin/Java lifecycle
237
+ 'onCreate', 'onStart', 'onResume', 'onPause', 'onStop', 'onDestroy',
238
+ 'onCreateView', 'onViewCreated', 'onDestroyView', 'hashCode', 'equals',
239
+ // Rust lifecycle / trait methods
240
+ 'new', 'default', 'drop', 'fmt', 'from', 'into', 'clone', 'copy',
241
+ // Ruby lifecycle
242
+ 'initialize', 'to_s', 'to_str', 'inspect', 'freeze', 'dup',
243
+ // PHP lifecycle
244
+ '__construct', '__destruct', '__toString', '__get', '__set', '__call',
187
245
  ];
188
246
  return lifecycle.includes(name) || name.startsWith('_') || name.startsWith('on');
189
247
  }
@@ -221,6 +279,23 @@ class GhostCodeDetector {
221
279
  _escapeRegex(str) {
222
280
  return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
223
281
  }
282
+
283
+ _stripCommentsAndStrings(content) {
284
+ // Replace string literals with placeholder (preserve length approximately)
285
+ let result = content
286
+ // Template literals
287
+ .replace(/`[^`\\]*(?:\\[\s\S][^`\\]*)*`/g, '""')
288
+ // Double-quoted strings
289
+ .replace(/"[^"\\\n]*(?:\\.[^"\\\n]*)*"/g, '""')
290
+ // Single-quoted strings
291
+ .replace(/'[^'\\\n]*(?:\\.[^'\\\n]*)*'/g, "''")
292
+ // Block comments (JS/Java/Go/Rust/PHP/Kotlin)
293
+ .replace(/\/\*[\s\S]*?\*\//g, ' ')
294
+ // Line comments: //, #
295
+ .replace(/\/\/[^\n]*/g, ' ')
296
+ .replace(/#[^\n]*/g, ' ');
297
+ return result;
298
+ }
224
299
  }
225
300
 
226
301
  module.exports = GhostCodeDetector;
@@ -73,9 +73,9 @@ class HallucinationDetector {
73
73
  this.pythonPhantomAPIs = [
74
74
  { pattern: /os\.path\.exists_sync\s*\(/, suggestion: 'Use os.path.exists() — exists_sync does not exist in Python', name: 'os.path.exists_sync', id: 'PY_HALL001' },
75
75
  { pattern: /json\.tryParse\s*\(/, suggestion: 'Use json.loads() with try/except', name: 'json.tryParse', id: 'PY_HALL002' },
76
- { pattern: /list\.flatMap\s*\(/, suggestion: 'Python lists have no flatMap — use list comprehension', name: 'list.flatMap', id: 'PY_HALL003' },
77
- { pattern: /dict\.merge\s*\(/, suggestion: 'Use {**dict1, **dict2} or dict1 | dict2 (3.9+)', name: 'dict.merge', id: 'PY_HALL004' },
78
- { pattern: /string\.format_map\s*\((?!.*\bself\b)/, suggestion: 'str.format_map exists but is rarely correct — did you mean .format()?', name: 'string.format_map misuse', id: 'PY_HALL005' },
76
+ { pattern: /\w+\.flatMap\s*\(/, suggestion: 'Python lists have no flatMap — use list comprehension or itertools.chain.from_iterable()', name: 'list.flatMap', id: 'PY_HALL003' },
77
+ { pattern: /\w+\.merge\s*\((?!.*\bself\b)/, suggestion: 'Use {**dict1, **dict2} or dict1 | dict2 (3.9+)', name: 'dict.merge', id: 'PY_HALL004' },
78
+ { pattern: /\w+\.format_map\s*\(/, suggestion: 'str.format_map exists but is rarely correct — did you mean .format()?', name: 'string.format_map misuse', id: 'PY_HALL005' },
79
79
  { pattern: /from\s+collections\s+import\s+OrderedDict.*#.*maintain\s+order/i, suggestion: 'Regular dict maintains order since Python 3.7 — OrderedDict is unnecessary', name: 'Unnecessary OrderedDict', id: 'PY_HALL006' },
80
80
  { pattern: /async\s+def\s+\w+.*asyncio\.sleep\s*\(\s*0\s*\)\s*#.*yield/i, suggestion: 'asyncio.sleep(0) to yield is a code smell — review async design', name: 'asyncio.sleep(0) hack', id: 'PY_HALL007' },
81
81
  { pattern: /import\s+tensorflow\.v2/, suggestion: 'tensorflow.v2 is not a real module — use import tensorflow', name: 'tensorflow.v2', id: 'PY_HALL008' },
@@ -104,13 +104,13 @@ class HallucinationDetector {
104
104
  { pattern: /import\s+\*/, id: 'PY_SMELL007', name: 'Wildcard Import', message: 'Wildcard import — pollutes namespace, hides dependencies' },
105
105
  ];
106
106
 
107
- // ─── Go phantom APIs ────────────────────────────────────
108
- this.goPhantomAPIs = [
109
- { pattern: /ioutil\.ReadFile\s*\(/, suggestion: 'ioutil removed in Go 1.16 — use os.ReadFile()', name: 'ioutil.ReadFile', id: 'GO_HALL001' },
110
- { pattern: /ioutil\.WriteFile\s*\(/, suggestion: 'ioutil removed in Go 1.16 — use os.WriteFile()', name: 'ioutil.WriteFile', id: 'GO_HALL002' },
111
- { pattern: /ioutil\.TempDir\s*\(/, suggestion: 'ioutil removed in Go 1.16 — use os.MkdirTemp()', name: 'ioutil.TempDir', id: 'GO_HALL003' },
112
- { pattern: /ioutil\.ReadAll\s*\(/, suggestion: 'ioutil removed in Go 1.16 — use io.ReadAll()', name: 'ioutil.ReadAll', id: 'GO_HALL004' },
113
- { pattern: /strings\.Title\s*\(/, suggestion: 'strings.Title deprecated in Go 1.18 — use cases.Title from golang.org/x/text', name: 'strings.Title', id: 'GO_HALL005' },
107
+ // ─── Go deprecated APIs (formerly labelled phantom — they exist but are removed) ─
108
+ this.goDeprecated = [
109
+ { pattern: /ioutil\.ReadFile\s*\(/, suggestion: 'ioutil removed in Go 1.16 — use os.ReadFile()', since: 'Go 1.16', name: 'ioutil.ReadFile', id: 'GO_DEPR001' },
110
+ { pattern: /ioutil\.WriteFile\s*\(/, suggestion: 'ioutil removed in Go 1.16 — use os.WriteFile()', since: 'Go 1.16', name: 'ioutil.WriteFile', id: 'GO_DEPR002' },
111
+ { pattern: /ioutil\.TempDir\s*\(/, suggestion: 'ioutil removed in Go 1.16 — use os.MkdirTemp()', since: 'Go 1.16', name: 'ioutil.TempDir', id: 'GO_DEPR003' },
112
+ { pattern: /ioutil\.ReadAll\s*\(/, suggestion: 'ioutil removed in Go 1.16 — use io.ReadAll()', since: 'Go 1.16', name: 'ioutil.ReadAll', id: 'GO_DEPR004' },
113
+ { pattern: /strings\.Title\s*\(/, suggestion: 'strings.Title deprecated in Go 1.18 — use cases.Title from golang.org/x/text', since: 'Go 1.18', name: 'strings.Title', id: 'GO_DEPR005' },
114
114
  ];
115
115
 
116
116
  this.goSmells = [
@@ -120,15 +120,42 @@ class HallucinationDetector {
120
120
  ];
121
121
 
122
122
  // ─── Rust phantom APIs ──────────────────────────────────
123
+ this.rustPhantomAPIs = [
124
+ { pattern: /std::fs::File::open_async\s*\(/, suggestion: 'Use tokio::fs::File::open() for async file I/O', name: 'File::open_async', id: 'RS_HALL001' },
125
+ { pattern: /Vec::flatten\s*\(/, suggestion: 'Use .into_iter().flatten() — Vec has no flatten() method', name: 'Vec::flatten', id: 'RS_HALL002' },
126
+ { pattern: /HashMap::get_or_default\s*\(/, suggestion: 'Use .get().unwrap_or(&default) or entry().or_insert()', name: 'HashMap::get_or_default', id: 'RS_HALL003' },
127
+ { pattern: /String::from_utf8_lossy_owned\s*\(/, suggestion: 'Use String::from_utf8_lossy().into_owned()', name: 'String::from_utf8_lossy_owned', id: 'RS_HALL004' },
128
+ { pattern: /\.async_iter\s*\(/, suggestion: 'Use futures::stream::iter() from the futures crate', name: '.async_iter()', id: 'RS_HALL005' },
129
+ { pattern: /std::net::TcpStream::connect_async\s*\(/, suggestion: 'Use tokio::net::TcpStream::connect()', name: 'TcpStream::connect_async', id: 'RS_HALL006' },
130
+ { pattern: /std::thread::sleep_async\s*\(/, suggestion: 'Use tokio::time::sleep() for async sleep', name: 'thread::sleep_async', id: 'RS_HALL007' },
131
+ { pattern: /Vec::remove_all\s*\(/, suggestion: 'Use .retain(|x| condition) or .clear()', name: 'Vec::remove_all', id: 'RS_HALL008' },
132
+ { pattern: /str::split_whitespace_n\s*\(/, suggestion: 'Use .splitn(n, char::is_whitespace) instead', name: 'str::split_whitespace_n', id: 'RS_HALL009' },
133
+ { pattern: /\.map_async\s*\(/, suggestion: 'Use futures::future::join_all() or tokio::task::spawn', name: '.map_async()', id: 'RS_HALL010' },
134
+ ];
135
+
136
+ this.rustDeprecated = [
137
+ { pattern: /std::sync::ONCE_INIT/, suggestion: 'std::sync::ONCE_INIT removed — use Once::new()', since: 'Rust 1.38', name: 'ONCE_INIT', id: 'RS_DEPR001' },
138
+ { pattern: /\bstd::mem::uninitialized\s*\(/, suggestion: 'std::mem::uninitialized() removed — use MaybeUninit::uninit()', since: 'Rust 1.39', name: 'mem::uninitialized', id: 'RS_DEPR002' },
139
+ { pattern: /\btry!\s*\(/, suggestion: 'try!() macro removed — use the ? operator', since: 'Rust 2018', name: 'try!() macro', id: 'RS_DEPR003' },
140
+ { pattern: /extern\s+crate\s+std\s*;/, suggestion: 'extern crate std is implicit since Rust 2018 edition', since: 'Rust 2018', name: 'extern crate std', id: 'RS_DEPR004' },
141
+ { pattern: /extern\s+crate\s+alloc\s*;/, suggestion: 'extern crate alloc is implicit in 2018+ edition', since: 'Rust 2018', name: 'extern crate alloc', id: 'RS_DEPR005' },
142
+ { pattern: /std::error::Error::cause\s*\(/, suggestion: '.cause() deprecated — use .source() instead', since: 'Rust 1.33', name: 'Error::cause()', id: 'RS_DEPR006' },
143
+ ];
144
+
123
145
  this.rustSmells = [
124
146
  { pattern: /todo!\s*\(\s*\)/, id: 'RS_SMELL001', name: 'todo! macro', message: 'todo!() macro — will panic at runtime' },
125
147
  { pattern: /unimplemented!\s*\(\s*\)/, id: 'RS_SMELL002', name: 'unimplemented! macro', message: 'unimplemented!() macro — will panic at runtime' },
126
148
  { pattern: /\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)/i, id: 'RS_SMELL003', name: 'Placeholder TODO', message: 'AI-generated placeholder — likely unfinished' },
127
149
  { pattern: /unwrap\(\).*unwrap\(\)/, id: 'RS_SMELL004', name: 'Double Unwrap', message: 'Chained unwrap() — will panic on None/Err' },
128
150
  { pattern: /localhost:\d{4}(?!.*(?:dev|test|local|development))/i, id: 'RS_SMELL005', name: 'Hardcoded Localhost', message: 'Hardcoded localhost URL — will fail in production' },
151
+ { pattern: /unsafe\s*\{/, id: 'RS_SMELL006', name: 'Unsafe Block', message: 'unsafe block — verify memory safety guarantees' },
152
+ { pattern: /\.unwrap\(\)\s*;/, id: 'RS_SMELL007', name: 'Bare unwrap()', message: '.unwrap() will panic on None/Err — use ? or match' },
153
+ { pattern: /println!\s*\(\s*["'].*[Dd]ebug/, id: 'RS_SMELL008', name: 'Debug println!', message: 'Debug println! left in code — use the log crate' },
154
+ { pattern: /eprintln!\s*\(\s*["'].*[Dd]ebug/, id: 'RS_SMELL009', name: 'Debug eprintln!', message: 'Debug eprintln! left in code — use the log crate' },
155
+ { pattern: /#\[allow\(dead_code\)\]/, id: 'RS_SMELL010', name: 'Suppressed Dead Code', message: '#[allow(dead_code)] suppresses warnings — remove unused code' },
129
156
  ];
130
157
 
131
- // ─── Java/C# common issues ──────────────────────────────
158
+ // ─── Java smells ─────────────────────────────────────────
132
159
  this.javaSmells = [
133
160
  { pattern: /System\.out\.println\s*\(.*debug/i, id: 'JAVA_SMELL001', name: 'Debug Println', message: 'Debug System.out.println — use a logger' },
134
161
  { pattern: /\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)/i, id: 'JAVA_SMELL002', name: 'Placeholder TODO', message: 'AI-generated placeholder — likely unfinished' },
@@ -137,12 +164,132 @@ class HallucinationDetector {
137
164
  { pattern: /localhost:\d{4}(?!.*(?:dev|test|local|development))/i, id: 'JAVA_SMELL005', name: 'Hardcoded Localhost', message: 'Hardcoded localhost URL — will fail in production' },
138
165
  ];
139
166
 
167
+ // ─── C# smells (separate from Java) ─────────────────────
168
+ this.csharpSmells = [
169
+ { pattern: /Console\.WriteLine\s*\(.*[Dd]ebug/i, id: 'CS_SMELL001', name: 'Debug WriteLine', message: 'Debug Console.WriteLine — use ILogger or Debug.Write' },
170
+ { pattern: /\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)/i, id: 'CS_SMELL002', name: 'Placeholder TODO', message: 'AI-generated placeholder — likely unfinished' },
171
+ { pattern: /throw\s+new\s+(?:NotImplementedException)\s*\(/, id: 'CS_SMELL003', name: 'Not Implemented', message: 'Stub implementation — will throw at runtime' },
172
+ { pattern: /catch\s*\(\s*Exception\s+\w+\s*\)\s*\{\s*\}/, id: 'CS_SMELL004', name: 'Empty Catch', message: 'Empty catch block — swallows all exceptions' },
173
+ { pattern: /localhost:\d{4}(?!.*(?:dev|test|local|development))/i, id: 'CS_SMELL005', name: 'Hardcoded Localhost', message: 'Hardcoded localhost URL — will fail in production' },
174
+ ];
175
+
176
+ // ─── Kotlin phantom APIs ──────────────────────────────────
177
+ this.kotlinPhantomAPIs = [
178
+ { pattern: /\.flatMap\s*\{[^}]*\}(?!.*asSequence)/, suggestion: 'Use .flatMap { } — verify this is not confusing sequence vs list behavior', name: 'flatMap confusion', id: 'KT_HALL001' },
179
+ { pattern: /listOf\(\)\.stream\s*\(/, suggestion: 'Kotlin lists have no .stream() — use .asSequence() or direct collection ops', name: 'listOf().stream()', id: 'KT_HALL002' },
180
+ { pattern: /String\.format\s*\(/, suggestion: 'Prefer Kotlin string templates "$variable" over String.format()', name: 'String.format Java-style', id: 'KT_HALL003' },
181
+ { pattern: /\bObject\s*\(\s*\)/, suggestion: 'Kotlin uses Any() not Object() — or use object keyword for singletons', name: 'Object() Java-style', id: 'KT_HALL004' },
182
+ { pattern: /\.forEach\s*\(\s*::println\s*\)(?!.*debug)/i, suggestion: 'println is for debug output — use a logger', name: 'println via forEach', id: 'KT_HALL005' },
183
+ { pattern: /coroutineScope\s*\{\s*launch\s*\{[^}]*\}\s*\}(?!\s*\.join)/, suggestion: 'Unjoined coroutine in coroutineScope may cause issues — ensure structured concurrency', name: 'Unawaited launch', id: 'KT_HALL006' },
184
+ { pattern: /Dispatchers\.IO\s*\+\s*Dispatchers/, suggestion: 'Combining Dispatchers is not valid — use one dispatcher at a time', name: 'Combined Dispatchers', id: 'KT_HALL007' },
185
+ { pattern: /\.toList\(\)\.stream\(\)/, suggestion: 'Use Kotlin sequences (.asSequence()) instead of .toList().stream()', name: 'toList().stream()', id: 'KT_HALL008' },
186
+ { pattern: /companion object.*getInstance/i, suggestion: 'Kotlin idiom for singletons is object keyword, not companion object + getInstance()', name: 'Java-style singleton', id: 'KT_HALL009' },
187
+ { pattern: /lateinit var.*\?/, suggestion: 'lateinit properties cannot be nullable — remove ? or use by lazy {}', name: 'lateinit nullable', id: 'KT_HALL010' },
188
+ ];
189
+
190
+ this.kotlinDeprecated = [
191
+ { pattern: /\bapply\s+plugin:\s*['"]kotlin-android-extensions['"]/, suggestion: 'kotlin-android-extensions deprecated — use view binding or Jetpack ViewBinding', since: 'Kotlin 1.8', name: 'kotlin-android-extensions', id: 'KT_DEPR001' },
192
+ { pattern: /\bkotlinx\.android\.synthetic/, suggestion: 'Synthetic imports deprecated — use view binding or findViewById', since: 'Kotlin 1.8', name: 'Synthetic imports', id: 'KT_DEPR002' },
193
+ { pattern: /\bCoroutineScope\(EmptyCoroutineContext\)/, suggestion: 'EmptyCoroutineContext coroutine scope is error-prone — use viewModelScope or lifecycleScope', since: 'Kotlin Coroutines 1.6', name: 'EmptyCoroutineContext scope', id: 'KT_DEPR003' },
194
+ { pattern: /\bBuildersKt\.launch\b/, suggestion: 'Internal BuildersKt APIs are deprecated — use coroutineScope { launch {} }', since: 'Kotlin Coroutines 1.5', name: 'BuildersKt.launch', id: 'KT_DEPR004' },
195
+ { pattern: /\basyncLazy\s*\{/, suggestion: 'asyncLazy {} is not a standard Kotlin API — use lazy {} or async {}', since: 'Kotlin 1.6', name: 'asyncLazy', id: 'KT_DEPR005' },
196
+ ];
197
+
198
+ this.kotlinSmells = [
199
+ { pattern: /\bprintln\s*\(/, id: 'KT_SMELL001', name: 'Debug println', message: 'println() is for debug output — use a proper logger' },
200
+ { pattern: /\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)/i, id: 'KT_SMELL002', name: 'Placeholder TODO', message: 'AI-generated placeholder — likely unfinished' },
201
+ { pattern: /TODO\s*\(\s*\)/, id: 'KT_SMELL003', name: 'TODO() stub', message: 'TODO() will throw NotImplementedError at runtime' },
202
+ { pattern: /!!\s*\./, id: 'KT_SMELL004', name: 'Non-null assertion chain', message: '!! non-null assertion — will throw NullPointerException if null' },
203
+ { pattern: /\bas\s+\w+\b(?!.*\?)/, suggestion: 'Unsafe cast — prefer safe cast (as? Type) with null check', id: 'KT_SMELL005', name: 'Unsafe cast', message: 'Unsafe cast with as — will throw ClassCastException if wrong type' },
204
+ { pattern: /localhost:\d{4}(?!.*(?:dev|test|local|development))/i, id: 'KT_SMELL006', name: 'Hardcoded Localhost', message: 'Hardcoded localhost URL — will fail in production' },
205
+ { pattern: /catch\s*\(\s*e:\s*Exception\s*\)\s*\{\s*\}/, id: 'KT_SMELL007', name: 'Empty Catch', message: 'Empty catch block — swallows all exceptions' },
206
+ { pattern: /throw\s+NotImplementedError\s*\(/, id: 'KT_SMELL008', name: 'Not Implemented', message: 'Stub implementation — will throw at runtime' },
207
+ ];
208
+
209
+ // ─── PHP phantom APIs ─────────────────────────────────────
210
+ this.phpPhantomAPIs = [
211
+ { pattern: /array_flatten\s*\(/, suggestion: 'PHP has no array_flatten() — use array_merge(...$array) or a recursive function', name: 'array_flatten', id: 'PHP_HALL001' },
212
+ { pattern: /str_contains_all\s*\(/, suggestion: 'PHP has no str_contains_all() — use multiple str_contains() calls', name: 'str_contains_all', id: 'PHP_HALL002' },
213
+ { pattern: /array_unique_values\s*\(/, suggestion: 'PHP has no array_unique_values() — use array_values(array_unique())', name: 'array_unique_values', id: 'PHP_HALL003' },
214
+ { pattern: /\$pdo->fetchAll\s*\(/, suggestion: 'fetchAll() is on PDOStatement not PDO — call $stmt->fetchAll()', name: 'PDO::fetchAll', id: 'PHP_HALL004' },
215
+ { pattern: /json_decode_safe\s*\(/, suggestion: 'PHP has no json_decode_safe() — wrap json_decode() with json_last_error() check', name: 'json_decode_safe', id: 'PHP_HALL005' },
216
+ { pattern: /str_replace_all\s*\(/, suggestion: 'PHP has no str_replace_all() — use str_replace() which already replaces all occurrences', name: 'str_replace_all', id: 'PHP_HALL006' },
217
+ { pattern: /array_map_keys\s*\(/, suggestion: 'PHP has no array_map_keys() — use array_combine(array_map(...), array_keys())', name: 'array_map_keys', id: 'PHP_HALL007' },
218
+ { pattern: /\$request->getJson\s*\(/, suggestion: 'Not a standard PHP function — use json_decode(file_get_contents("php://input"))', name: '$request->getJson', id: 'PHP_HALL008' },
219
+ { pattern: /Date::now\s*\(/, suggestion: 'PHP has no Date::now() — use new DateTime() or time()', name: 'Date::now', id: 'PHP_HALL009' },
220
+ { pattern: /\bawait\s+/, suggestion: 'PHP has no await keyword — use synchronous code or ReactPHP for async', name: 'await keyword', id: 'PHP_HALL010' },
221
+ ];
222
+
223
+ this.phpDeprecated = [
224
+ { pattern: /\bmysql_connect\s*\(/, suggestion: 'mysql_* functions removed in PHP 7 — use PDO or MySQLi', since: 'PHP 7.0', name: 'mysql_connect', id: 'PHP_DEPR001' },
225
+ { pattern: /\bmysql_query\s*\(/, suggestion: 'mysql_* functions removed in PHP 7 — use PDO or MySQLi', since: 'PHP 7.0', name: 'mysql_query', id: 'PHP_DEPR002' },
226
+ { pattern: /\bmysql_fetch_array\s*\(/, suggestion: 'mysql_* functions removed in PHP 7 — use PDO or MySQLi', since: 'PHP 7.0', name: 'mysql_fetch_array', id: 'PHP_DEPR003' },
227
+ { pattern: /\bereg\s*\(/, suggestion: 'ereg() removed in PHP 7 — use preg_match()', since: 'PHP 7.0', name: 'ereg()', id: 'PHP_DEPR004' },
228
+ { pattern: /\bsplit\s*\(/, suggestion: 'split() removed in PHP 7 — use preg_split() or explode()', since: 'PHP 7.0', name: 'split()', id: 'PHP_DEPR005' },
229
+ { pattern: /\bcreate_function\s*\(/, suggestion: 'create_function() removed in PHP 8 — use anonymous functions (closures)', since: 'PHP 8.0', name: 'create_function', id: 'PHP_DEPR006' },
230
+ { pattern: /\bmagic_quotes_gpc\b/, suggestion: 'magic_quotes_gpc removed in PHP 7 — sanitize inputs manually', since: 'PHP 7.0', name: 'magic_quotes_gpc', id: 'PHP_DEPR007' },
231
+ { pattern: /\beach\s*\(/, suggestion: 'each() deprecated in PHP 7.2, removed in PHP 8 — use foreach', since: 'PHP 8.0', name: 'each()', id: 'PHP_DEPR008' },
232
+ { pattern: /\bmcrypt_/, suggestion: 'mcrypt removed in PHP 7.2 — use OpenSSL functions', since: 'PHP 7.2', name: 'mcrypt functions', id: 'PHP_DEPR009' },
233
+ ];
234
+
235
+ this.phpSmells = [
236
+ { pattern: /\beval\s*\(/, id: 'PHP_SMELL001', name: 'eval() Usage', message: 'eval() is dangerous — code injection risk' },
237
+ { pattern: /\bextract\s*\(\s*\$_(?:GET|POST|REQUEST)/, id: 'PHP_SMELL002', name: 'extract() from superglobal', message: 'extract($_GET/_POST) — variable injection vulnerability' },
238
+ { pattern: /\$\$\w+/, id: 'PHP_SMELL003', name: 'Variable variable', message: 'Variable variables ($$var) — hard to trace and injection risk' },
239
+ { pattern: /\bshell_exec\s*\(/, id: 'PHP_SMELL004', name: 'shell_exec()', message: 'shell_exec() — remote code execution risk if input unsanitized' },
240
+ { pattern: /\bsystem\s*\(/, id: 'PHP_SMELL005', name: 'system()', message: 'system() — remote code execution risk if input unsanitized' },
241
+ { pattern: /\bpassthru\s*\(/, id: 'PHP_SMELL006', name: 'passthru()', message: 'passthru() — remote code execution risk if input unsanitized' },
242
+ { pattern: /\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)/i, id: 'PHP_SMELL007', name: 'Placeholder TODO', message: 'AI-generated placeholder — likely unfinished' },
243
+ { pattern: /localhost:\d{4}(?!.*(?:dev|test|local|development))/i, id: 'PHP_SMELL008', name: 'Hardcoded Localhost', message: 'Hardcoded localhost URL — will fail in production' },
244
+ { pattern: /\$_(?:GET|POST|REQUEST)\[.*\].*SELECT.*\./i, id: 'PHP_SMELL009', name: 'SQL Injection Risk', message: 'User input directly in SQL string — use prepared statements' },
245
+ { pattern: /\bvar_dump\s*\(/, id: 'PHP_SMELL010', name: 'Debug var_dump', message: 'var_dump() is for debug only — remove before production' },
246
+ ];
247
+
248
+ // ─── Ruby phantom APIs ────────────────────────────────────
249
+ this.rubyPhantomAPIs = [
250
+ { pattern: /\.flatten_map\s*\{/, suggestion: 'Ruby has no .flatten_map — use .flat_map { }', name: '.flatten_map', id: 'RB_HALL001' },
251
+ { pattern: /Array\.of\s*\(/, suggestion: 'Ruby has no Array.of() — use Array() or []', name: 'Array.of()', id: 'RB_HALL002' },
252
+ { pattern: /Hash\.from_array\s*\(/, suggestion: 'Ruby has no Hash.from_array() — use array.to_h or Hash[array]', name: 'Hash.from_array', id: 'RB_HALL003' },
253
+ { pattern: /\.async\s*\{/, suggestion: 'Ruby has no .async {} — use Thread.new or concurrent-ruby gem', name: '.async block', id: 'RB_HALL004' },
254
+ { pattern: /String\.format\s*\(/, suggestion: 'Ruby has no String.format() — use string interpolation or % operator', name: 'String.format', id: 'RB_HALL005' },
255
+ { pattern: /\bpromise\s*\{/, suggestion: 'Ruby has no built-in promise {} — use concurrent-ruby gem or Thread', name: 'promise block', id: 'RB_HALL006' },
256
+ { pattern: /\.try!\s*\(/, suggestion: 'Ruby has no .try!() — use &. (safe navigation) or Rails .try()', name: '.try!()', id: 'RB_HALL007' },
257
+ { pattern: /JSON\.safe_parse\s*\(/, suggestion: 'Ruby has no JSON.safe_parse — use JSON.parse with rescue', name: 'JSON.safe_parse', id: 'RB_HALL008' },
258
+ { pattern: /ActiveRecord::Base\.execute\s*\(/, suggestion: 'ActiveRecord::Base has no .execute() — use connection.execute() or where()', name: 'Base.execute', id: 'RB_HALL009' },
259
+ { pattern: /\.collect_map\s*\{/, suggestion: 'Ruby has no .collect_map — use .map { }.flatten(1) or .flat_map', name: '.collect_map', id: 'RB_HALL010' },
260
+ ];
261
+
262
+ this.rubyDeprecated = [
263
+ { pattern: /\brequire\s+['"]thread['"]/, suggestion: 'require "thread" is deprecated — Thread is now built-in without require', since: 'Ruby 2.0', name: 'require "thread"', id: 'RB_DEPR001' },
264
+ { pattern: /\bObject#type\b|\.type\s*==/, suggestion: '.type is deprecated — use .class or .is_a?', since: 'Ruby 1.8', name: 'Object#type', id: 'RB_DEPR002' },
265
+ { pattern: /\bERB::Util\.html_escape\b/, suggestion: 'ERB::Util.html_escape deprecated — use CGI.escapeHTML or h() in views', since: 'Ruby 2.6', name: 'ERB::Util.html_escape', id: 'RB_DEPR003' },
266
+ { pattern: /\bFile\.exists\?\s*\(/, suggestion: 'File.exists? deprecated — use File.exist? (no s)', since: 'Ruby 2.2', name: 'File.exists?', id: 'RB_DEPR004' },
267
+ { pattern: /\bDir\.exists\?\s*\(/, suggestion: 'Dir.exists? deprecated — use Dir.exist? (no s)', since: 'Ruby 2.2', name: 'Dir.exists?', id: 'RB_DEPR005' },
268
+ { pattern: /\bObject#returning\b|\s+returning\s+/, suggestion: 'Object#returning removed from Rails core — use tap { |obj| }', since: 'Rails 3.0', name: 'Object#returning', id: 'RB_DEPR006' },
269
+ ];
270
+
271
+ this.rubySmells = [
272
+ { pattern: /\bputs\s+(?!STDOUT)/, id: 'RB_SMELL001', name: 'Debug puts', message: 'puts is for debug output — use Rails logger or a logging library' },
273
+ { pattern: /\bp\s+\w/, id: 'RB_SMELL002', name: 'Debug p()', message: 'p() is for debug output — remove before production' },
274
+ { pattern: /\beval\s*\(/, id: 'RB_SMELL003', name: 'eval() Usage', message: 'eval() is dangerous — code injection risk' },
275
+ { pattern: /\bsystem\s*\(/, id: 'RB_SMELL004', name: 'system() Shell Call', message: 'system() call — command injection risk if input unsanitized' },
276
+ { pattern: /`[^`]+`/, id: 'RB_SMELL005', name: 'Backtick Shell Exec', message: 'Backtick shell execution — command injection risk' },
277
+ { pattern: /\bexec\s*\(/, id: 'RB_SMELL006', name: 'exec() call', message: 'exec() replaces the process — ensure this is intentional' },
278
+ { pattern: /\brescue\s*$|\brescue\s+Exception\b/, id: 'RB_SMELL007', name: 'Bare rescue', message: 'Bare rescue catches all exceptions including fatal ones — be specific' },
279
+ { pattern: /["'].*#\{.*sql.*\}.*["']/i, id: 'RB_SMELL008', name: 'SQL String Interpolation', message: 'SQL built with string interpolation — use parameterized queries' },
280
+ { pattern: /\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)/i, id: 'RB_SMELL009', name: 'Placeholder TODO', message: 'AI-generated placeholder — likely unfinished' },
281
+ { pattern: /raise\s+NotImplementedError/, id: 'RB_SMELL010', name: 'Not Implemented', message: 'Stub implementation — will raise at runtime' },
282
+ ];
283
+
140
284
  // Language extensions mapping
141
285
  this.jsExtensions = new Set(['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs']);
142
286
  this.pyExtensions = new Set(['.py', '.pyw']);
143
287
  this.goExtensions = new Set(['.go']);
144
288
  this.rustExtensions = new Set(['.rs']);
145
- this.javaExtensions = new Set(['.java', '.kt', '.cs']);
289
+ this.javaExtensions = new Set(['.java', '.kt']);
290
+ this.csharpExtensions = new Set(['.cs']);
291
+ this.phpExtensions = new Set(['.php']);
292
+ this.rubyExtensions = new Set(['.rb']);
146
293
 
147
294
  // Patterns suggesting AI-generated code with common issues
148
295
  this.aiCodeSmells = [
@@ -160,13 +307,13 @@ class HallucinationDetector {
160
307
  { pattern: /new Buffer\s*\(/, suggestion: 'Use Buffer.from(), Buffer.alloc(), or Buffer.allocUnsafe()', since: 'Node 6', name: 'new Buffer()', id: 'DEPR_API001' },
161
308
  { pattern: /require\s*\(\s*['"]sys['"]\s*\)/, suggestion: 'Use require("util") — sys was removed in Node 1.0', since: 'Node 1.0', name: 'require("sys")', id: 'DEPR_API002' },
162
309
  { pattern: /fs\.exists\s*\((?!Sync)/, suggestion: 'Use fs.access() or fs.stat() — fs.exists() is deprecated', since: 'Node 1.0', name: 'fs.exists()', id: 'DEPR_API003' },
163
- { pattern: /domain\.create\s*\(/, suggestion: 'Use async_hooks or try/catch — domain module is deprecated', since: 'Node 4', name: 'domain.create()', id: 'DEPR_API004' },
164
- { pattern: /require\s*\(\s*['"]punycode['"]\s*\)/, suggestion: 'Use userland punycode package — built-in is deprecated', since: 'Node 7', name: 'require("punycode")', id: 'DEPR_API005' },
310
+ { pattern: /domain\.create\s*\(/, suggestion: 'Use async_hooks or try/catch — domain module is deprecated', since: 'Node 4', name: 'domain.create()', id: 'DEPR_API004', fixable: false },
311
+ { pattern: /require\s*\(\s*['"]punycode['"]\s*\)/, suggestion: 'Use userland punycode package — built-in is deprecated', since: 'Node 7', name: 'require("punycode")', id: 'DEPR_API005', fixable: false },
165
312
  { pattern: /url\.parse\s*\(/, suggestion: 'Use new URL() — url.parse() is legacy', since: 'Node 11', name: 'url.parse()', id: 'DEPR_API006' },
166
- { pattern: /querystring\.(parse|stringify)\s*\(/, suggestion: 'Use URLSearchParams — querystring is legacy', since: 'Node 14', name: 'querystring', id: 'DEPR_API007' },
313
+ { pattern: /querystring\.(parse|stringify)\s*\(/, suggestion: 'Use URLSearchParams — querystring is legacy', since: 'Node 14', name: 'querystring', id: 'DEPR_API007', fixable: false },
167
314
  { pattern: /util\.isArray\s*\(/, suggestion: 'Use Array.isArray() — util type checks are deprecated', since: 'Node 4', name: 'util.isArray()', id: 'DEPR_API008' },
168
315
  { pattern: /util\.isFunction\s*\(/, suggestion: 'Use typeof fn === "function"', since: 'Node 4', name: 'util.isFunction()', id: 'DEPR_API009' },
169
- { pattern: /util\.pump\s*\(/, suggestion: 'Use stream.pipeline() — util.pump() was removed', since: 'Node 1.0', name: 'util.pump()', id: 'DEPR_API010' },
316
+ { pattern: /util\.pump\s*\(/, suggestion: 'Use stream.pipeline() — util.pump() was removed', since: 'Node 1.0', name: 'util.pump()', id: 'DEPR_API010', fixable: false },
170
317
  ];
171
318
  }
172
319
 
@@ -202,8 +349,11 @@ class HallucinationDetector {
202
349
  const isGo = this.goExtensions.has(ext);
203
350
  const isRust = this.rustExtensions.has(ext);
204
351
  const isJava = this.javaExtensions.has(ext);
352
+ const isCSharp = this.csharpExtensions.has(ext);
353
+ const isPHP = this.phpExtensions.has(ext);
354
+ const isRuby = this.rubyExtensions.has(ext);
205
355
 
206
- if (!isJS && !isPython && !isGo && !isRust && !isJava) continue;
356
+ if (!isJS && !isPython && !isGo && !isRust && !isJava && !isCSharp && !isPHP && !isRuby) continue;
207
357
 
208
358
  const relPath = path.relative(this.config.rootPath, file);
209
359
 
@@ -246,13 +396,33 @@ class HallucinationDetector {
246
396
  this._checkLanguagePatterns(relPath, content, lines, results, ignoredLines, this.pythonPhantomAPIs, 'phantomAPIs');
247
397
  this._checkLanguagePatterns(relPath, content, lines, results, ignoredLines, this.pythonDeprecated, 'deprecatedAPIs');
248
398
  this._checkLanguageSmells(relPath, content, lines, results, ignoredLines, this.pythonSmells);
399
+ } else if (isPHP) {
400
+ this._checkLanguagePatterns(relPath, content, lines, results, ignoredLines, this.phpPhantomAPIs, 'phantomAPIs');
401
+ this._checkLanguagePatterns(relPath, content, lines, results, ignoredLines, this.phpDeprecated, 'deprecatedAPIs');
402
+ this._checkLanguageSmells(relPath, content, lines, results, ignoredLines, this.phpSmells);
403
+ } else if (isRuby) {
404
+ this._checkLanguagePatterns(relPath, content, lines, results, ignoredLines, this.rubyPhantomAPIs, 'phantomAPIs');
405
+ this._checkLanguagePatterns(relPath, content, lines, results, ignoredLines, this.rubyDeprecated, 'deprecatedAPIs');
406
+ this._checkLanguageSmells(relPath, content, lines, results, ignoredLines, this.rubySmells);
249
407
  } else if (isGo) {
250
- this._checkLanguagePatterns(relPath, content, lines, results, ignoredLines, this.goPhantomAPIs, 'phantomAPIs');
408
+ this._checkLanguagePatterns(relPath, content, lines, results, ignoredLines, this.goDeprecated, 'deprecatedAPIs');
251
409
  this._checkLanguageSmells(relPath, content, lines, results, ignoredLines, this.goSmells);
252
410
  } else if (isRust) {
411
+ this._checkLanguagePatterns(relPath, content, lines, results, ignoredLines, this.rustPhantomAPIs, 'phantomAPIs');
412
+ this._checkLanguagePatterns(relPath, content, lines, results, ignoredLines, this.rustDeprecated, 'deprecatedAPIs');
253
413
  this._checkLanguageSmells(relPath, content, lines, results, ignoredLines, this.rustSmells);
254
414
  } else if (isJava) {
255
- this._checkLanguageSmells(relPath, content, lines, results, ignoredLines, this.javaSmells);
415
+ // Java and Kotlin share javaExtensions (.java, .kt)
416
+ const isKotlin = ext === '.kt';
417
+ if (isKotlin) {
418
+ this._checkLanguagePatterns(relPath, content, lines, results, ignoredLines, this.kotlinPhantomAPIs, 'phantomAPIs');
419
+ this._checkLanguagePatterns(relPath, content, lines, results, ignoredLines, this.kotlinDeprecated, 'deprecatedAPIs');
420
+ this._checkLanguageSmells(relPath, content, lines, results, ignoredLines, this.kotlinSmells);
421
+ } else {
422
+ this._checkLanguageSmells(relPath, content, lines, results, ignoredLines, this.javaSmells);
423
+ }
424
+ } else if (isCSharp) {
425
+ this._checkLanguageSmells(relPath, content, lines, results, ignoredLines, this.csharpSmells);
256
426
  }
257
427
 
258
428
  } catch (e) { /* skip unreadable */ }