use std::path::Path;
use crate::core::ast::parser::ParsedModule;
use crate::core::recipe::{FileAnalysis, FileClassification};
use crate::recipes::js_to_ts::types::{JsTsPattern, TypeInferenceEngine, TypeInferenceHints};
use super::types::MigrationMode;
pub struct JsToTsDetector {
mode: MigrationMode,
}
impl JsToTsDetector {
pub fn new(mode: MigrationMode) -> Self {
Self { mode }
}
#[allow(dead_code)]
pub fn analyze(&self, _parsed: &ParsedModule, source: &str) -> FileAnalysis {
let patterns = self.detect_patterns(source);
let classification = self.classify(&patterns);
let confidence = self.calculate_confidence(&patterns);
FileAnalysis {
path: _parsed.path.clone(),
detected_patterns: patterns.iter().map(|p| p.label().to_owned()).collect(),
confidence_score: confidence,
classification,
is_transform_safe: classification == FileClassification::Safe,
tags: Default::default(),
}
}
fn detect_patterns(&self, source: &str) -> Vec<JsTsPattern> {
let mut patterns = Vec::new();
patterns.push(JsTsPattern::PlainJavaScript);
if source.contains("require(") {
patterns.push(JsTsPattern::CommonJSRequire);
}
if source.contains("module.exports") {
patterns.push(JsTsPattern::CommonJSExports);
}
if source.contains("import ") && source.contains(" from ") {
patterns.push(JsTsPattern::EsModuleImport);
}
let has_jsx_pattern = (source.contains("React") || source.contains("function "))
&& (source.contains("<") && source.contains("/>") || source.contains("</"));
if has_jsx_pattern {
patterns.push(JsTsPattern::ReactJSX);
}
if source.contains("@param")
|| source.contains("@type")
|| source.contains("@returns")
|| source.contains("@typedef")
{
patterns.push(JsTsPattern::JSDocAnnotations);
}
if source.contains(": any") || source.contains("any") {
patterns.push(JsTsPattern::ImplicitAny);
}
patterns
}
fn classify(&self, patterns: &[JsTsPattern]) -> FileClassification {
match self.mode {
MigrationMode::Conservative => {
if patterns.contains(&JsTsPattern::ReactJSX)
|| patterns.contains(&JsTsPattern::CommonJSRequire)
{
FileClassification::Risky
} else {
FileClassification::Safe
}
}
MigrationMode::Balanced => {
if patterns.contains(&JsTsPattern::ReactJSX)
&& patterns.contains(&JsTsPattern::CommonJSRequire)
{
FileClassification::Risky
} else {
FileClassification::Safe
}
}
MigrationMode::Aggressive => FileClassification::Safe,
}
}
fn calculate_confidence(&self, patterns: &[JsTsPattern]) -> u8 {
let base_score = 50u8;
let mut score = base_score;
for pattern in patterns {
score = score.saturating_add(match pattern {
JsTsPattern::PlainJavaScript => 0,
JsTsPattern::CommonJSRequire => 15,
JsTsPattern::CommonJSExports => 10,
JsTsPattern::JSDocAnnotations => 10,
JsTsPattern::ImplicitAny => 5,
JsTsPattern::ReactJSX => 15,
JsTsPattern::TypeAnnotatedFile => 20,
JsTsPattern::EsModuleImport => 15,
});
}
score.min(100)
}
}
pub fn analyze_js_file(path: &Path, source: &str, mode: MigrationMode) -> Option<FileAnalysis> {
let ext = path.extension()?.to_str()?;
if ext != "js" && ext != "jsx" && ext != "mjs" {
return None;
}
let detector = JsToTsDetector::new(mode);
let _hints = TypeInferenceEngine::analyze_for_inference(source);
let patterns = detector.detect_patterns(source);
let classification = detector.classify(&patterns);
let confidence = detector.calculate_confidence(&patterns);
Some(FileAnalysis {
path: path.to_path_buf(),
detected_patterns: patterns.iter().map(|p| p.label().to_owned()).collect(),
confidence_score: confidence,
classification,
is_transform_safe: classification == FileClassification::Safe,
tags: Default::default(),
})
}
#[allow(dead_code)]
pub fn extract_inference_hints(source: &str) -> TypeInferenceHints {
TypeInferenceEngine::analyze_for_inference(source)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
fn analyze(source: &str) -> FileAnalysis {
analyze_js_file(Path::new("fixture.js"), source, MigrationMode::Conservative)
.expect("should detect JS patterns")
}
#[test]
fn detects_plain_javascript() {
let analysis = analyze("const x = 1;");
assert!(
analysis
.detected_patterns
.contains(&"plain JavaScript".to_string())
);
assert_eq!(analysis.classification, FileClassification::Safe);
}
#[test]
fn detects_commonjs() {
let analysis = analyze("const lib = require('lib'); module.exports = lib;");
assert!(
analysis
.detected_patterns
.contains(&"CommonJS require".to_string())
);
assert_eq!(analysis.classification, FileClassification::Risky);
}
#[test]
fn detects_jsx() {
let analysis = analyze("function App() { return <div>Hello</div>; }");
assert!(
analysis
.detected_patterns
.contains(&"React JSX".to_string())
);
assert_eq!(analysis.classification, FileClassification::Risky);
}
#[test]
fn detects_jsdoc() {
let analysis = analyze("/** @param {string} name */\nfunction greet(name) {}");
assert!(
analysis
.detected_patterns
.contains(&"JSDoc annotations".to_string())
);
}
#[test]
fn detects_implicit_any() {
let analysis = analyze("const x: any = 1;");
assert!(
analysis
.detected_patterns
.contains(&"implicit any patterns".to_string())
);
}
#[test]
fn conservative_mode_considers_commonjs_risky() {
let analysis = analyze("const lib = require('lib');");
assert_eq!(analysis.classification, FileClassification::Risky);
}
#[test]
fn balanced_mode_is_more_permissive() {
let analysis = analyze_js_file(
Path::new("fixture.js"),
"const lib = require('lib');",
MigrationMode::Balanced,
)
.expect("should detect");
assert_eq!(analysis.classification, FileClassification::Safe);
}
#[test]
fn aggressive_mode_is_most_permissive() {
let analysis = analyze_js_file(
Path::new("fixture.jsx"),
"function App() { return <div>Hello</div>; }",
MigrationMode::Aggressive,
)
.expect("should detect");
assert_eq!(analysis.classification, FileClassification::Safe);
}
#[test]
fn skips_non_js_files() {
let result = analyze_js_file(
Path::new("fixture.ts"),
"const x: string = 'hi';",
MigrationMode::Conservative,
);
assert!(result.is_none());
}
}