@@ -2,6 +2,7 @@ use regex::Regex;
22use serde:: { Deserialize , Serialize } ;
33use std:: collections:: HashSet ;
44use std:: fs;
5+ use std:: io:: { BufRead , BufReader } ;
56use std:: path:: Path ;
67
78#[ derive( Debug , Clone , Serialize , Deserialize ) ]
@@ -253,8 +254,13 @@ fn scan_for_leaked_secrets(project_path: &Path) -> Vec<LeakedSecret> {
253254 // 要扫描的文件扩展名
254255 let scan_extensions = [ "ts" , "tsx" , "js" , "jsx" , "py" , "rs" , "go" , "java" , "rb" ] ;
255256
256- // 要排除的目录
257- let exclude_dirs = [ "node_modules" , "target" , ".git" , "dist" , "build" , "__pycache__" , ".venv" , "venv" ] ;
257+ // 要排除的目录(包括构建产物)
258+ let exclude_dirs = [
259+ "node_modules" , "target" , ".git" , "dist" , "build" , "__pycache__" , ".venv" , "venv" ,
260+ ".next" , ".nuxt" , ".output" , "out" , ".turbo" , ".vercel" , ".netlify" ,
261+ "coverage" , ".nyc_output" , ".cache" , ".parcel-cache" ,
262+ "chunks" , "ssr" , "static" , // Next.js 内部目录
263+ ] ;
258264
259265 scan_directory ( project_path, & secret_pattern, & scan_extensions, & exclude_dirs, & mut secrets) ;
260266
@@ -335,3 +341,162 @@ fn scan_directory(
335341 }
336342 }
337343}
344+
345+ /// 文件行数统计
346+ #[ derive( Debug , Clone , Serialize , Deserialize ) ]
347+ pub struct FileLineCount {
348+ pub file : String ,
349+ pub lines : usize ,
350+ }
351+
352+ /// 扫描项目文件,按行数倒序返回
353+ pub fn scan_file_lines ( project_path : & str , limit : usize , ignored_paths : & [ String ] ) -> Result < Vec < FileLineCount > , String > {
354+ let path = Path :: new ( project_path) ;
355+ let mut files: Vec < FileLineCount > = Vec :: new ( ) ;
356+
357+ // 要扫描的文件扩展名
358+ let scan_extensions = [
359+ "ts" , "tsx" , "js" , "jsx" , "vue" , "svelte" ,
360+ "py" , "rs" , "go" , "java" , "rb" , "php" ,
361+ "css" , "scss" , "less" ,
362+ "html" , "md" , "json" , "yaml" , "yml" , "toml" ,
363+ ] ;
364+
365+ // 要排除的目录
366+ let exclude_dirs = [
367+ "node_modules" , "target" , ".git" , "dist" , "build" , "__pycache__" , ".venv" , "venv" ,
368+ ".next" , ".nuxt" , ".output" , "out" , ".turbo" , ".vercel" , ".netlify" ,
369+ "coverage" , ".nyc_output" , ".cache" , ".parcel-cache" ,
370+ "chunks" , "ssr" , "static" , ".svelte-kit" ,
371+ ] ;
372+
373+ scan_files_recursive ( path, path, & scan_extensions, & exclude_dirs, & mut files) ;
374+
375+ // 按行数倒序排序
376+ files. sort_by ( |a, b| b. lines . cmp ( & a. lines ) ) ;
377+
378+ // 过滤掉用户忽略的路径(在限制条数之前)
379+ if !ignored_paths. is_empty ( ) {
380+ files. retain ( |f| {
381+ !ignored_paths. iter ( ) . any ( |ignored| {
382+ f. file == * ignored || f. file . starts_with ( & format ! ( "{}/" , ignored) )
383+ } )
384+ } ) ;
385+ }
386+
387+ // 限制返回数量
388+ files. truncate ( limit) ;
389+
390+ Ok ( files)
391+ }
392+
393+ fn scan_files_recursive (
394+ dir : & Path ,
395+ root : & Path ,
396+ extensions : & [ & str ] ,
397+ exclude_dirs : & [ & str ] ,
398+ files : & mut Vec < FileLineCount > ,
399+ ) {
400+ let entries = match fs:: read_dir ( dir) {
401+ Ok ( e) => e,
402+ Err ( _) => return ,
403+ } ;
404+
405+ for entry in entries. flatten ( ) {
406+ let path = entry. path ( ) ;
407+ let file_name = path. file_name ( ) . unwrap_or_default ( ) . to_string_lossy ( ) ;
408+
409+ if path. is_dir ( ) {
410+ if exclude_dirs. iter ( ) . any ( |& d| file_name == d) {
411+ continue ;
412+ }
413+ scan_files_recursive ( & path, root, extensions, exclude_dirs, files) ;
414+ } else if path. is_file ( ) {
415+ let ext = path. extension ( ) . unwrap_or_default ( ) . to_string_lossy ( ) ;
416+ if !extensions. iter ( ) . any ( |& e| ext == e) {
417+ continue ;
418+ }
419+
420+ // 排除锁文件和自动生成的文件
421+ let excluded_files = [
422+ "package-lock.json" , "pnpm-lock.yaml" , "yarn.lock" , "bun.lockb" ,
423+ "Cargo.lock" , "poetry.lock" , "Pipfile.lock" , "composer.lock" ,
424+ ".d.ts" , // 类型声明文件
425+ ] ;
426+ if excluded_files. iter ( ) . any ( |& f| file_name. ends_with ( f) ) {
427+ continue ;
428+ }
429+
430+ // 统计行数
431+ if let Ok ( file) = fs:: File :: open ( & path) {
432+ let reader = BufReader :: new ( file) ;
433+ let line_count = reader. lines ( ) . count ( ) ;
434+
435+ // 获取相对路径(相对于项目根目录)
436+ let relative_path = path
437+ . strip_prefix ( root)
438+ . unwrap_or ( & path)
439+ . to_string_lossy ( )
440+ . to_string ( ) ;
441+
442+ files. push ( FileLineCount {
443+ file : relative_path,
444+ lines : line_count,
445+ } ) ;
446+ }
447+ }
448+ }
449+ }
450+
451+ /// 将 missing keys 添加到 .env 文件
452+ pub fn add_missing_keys_to_env ( project_path : & str , keys : Vec < String > ) -> Result < usize , String > {
453+ let path = Path :: new ( project_path) ;
454+ let env_path = path. join ( ".env" ) ;
455+ let env_example_path = path. join ( ".env.example" ) ;
456+
457+ // 读取 .env.example 获取默认值
458+ let example_values: std:: collections:: HashMap < String , String > = if env_example_path. exists ( ) {
459+ let content = fs:: read_to_string ( & env_example_path) . unwrap_or_default ( ) ;
460+ content
461+ . lines ( )
462+ . filter_map ( |line| {
463+ let line = line. trim ( ) ;
464+ if line. is_empty ( ) || line. starts_with ( '#' ) {
465+ return None ;
466+ }
467+ line. find ( '=' ) . map ( |pos| {
468+ let key = line[ ..pos] . trim ( ) . to_string ( ) ;
469+ let value = line[ pos + 1 ..] . trim ( ) . to_string ( ) ;
470+ ( key, value)
471+ } )
472+ } )
473+ . collect ( )
474+ } else {
475+ std:: collections:: HashMap :: new ( )
476+ } ;
477+
478+ // 读取现有 .env 内容
479+ let mut env_content = if env_path. exists ( ) {
480+ fs:: read_to_string ( & env_path) . unwrap_or_default ( )
481+ } else {
482+ String :: new ( )
483+ } ;
484+
485+ // 确保以换行结尾
486+ if !env_content. is_empty ( ) && !env_content. ends_with ( '\n' ) {
487+ env_content. push ( '\n' ) ;
488+ }
489+
490+ // 添加 missing keys
491+ let mut added_count = 0 ;
492+ for key in & keys {
493+ let default_value = example_values. get ( key) . cloned ( ) . unwrap_or_default ( ) ;
494+ env_content. push_str ( & format ! ( "{}={}\n " , key, default_value) ) ;
495+ added_count += 1 ;
496+ }
497+
498+ // 写入文件
499+ fs:: write ( & env_path, env_content) . map_err ( |e| e. to_string ( ) ) ?;
500+
501+ Ok ( added_count)
502+ }
0 commit comments