autosnippet 3.2.21 → 3.2.22

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.
@@ -6,6 +6,8 @@
6
6
  * - exclusionManager, ruleLearner, violationsStore
7
7
  * - complianceReporter, guardFeedbackLoop
8
8
  */
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
9
11
  import { resolveProjectRoot } from '#shared/resolveProjectRoot.js';
10
12
  import { ComplianceReporter } from '../../service/guard/ComplianceReporter.js';
11
13
  import { ExclusionManager } from '../../service/guard/ExclusionManager.js';
@@ -29,7 +31,37 @@ export function register(c) {
29
31
  });
30
32
  c.singleton('guardCheckEngine', (ct) => {
31
33
  const config = ct.singletons._config || {};
32
- return new GuardCheckEngine(ct.get('database'), { guardConfig: config.guard || {} });
34
+ // 基础配置(AutoSnippet 自身 config/default.json)
35
+ const baseGuard = config.guard || {};
36
+ // 项目级覆盖(.autosnippet/config.json 的 guard 段)
37
+ let projectGuard = {};
38
+ try {
39
+ const projectRoot = resolveProjectRoot(ct);
40
+ const projConfigPath = path.join(projectRoot, '.autosnippet', 'config.json');
41
+ if (fs.existsSync(projConfigPath)) {
42
+ const raw = JSON.parse(fs.readFileSync(projConfigPath, 'utf-8'));
43
+ if (raw.guard && typeof raw.guard === 'object') {
44
+ projectGuard = raw.guard;
45
+ }
46
+ }
47
+ }
48
+ catch {
49
+ /* 项目配置读取失败不阻塞 */
50
+ }
51
+ // 合并:项目级覆盖基础配置
52
+ const merged = { ...baseGuard, ...projectGuard };
53
+ if (baseGuard.codeLevelThresholds || projectGuard.codeLevelThresholds) {
54
+ merged.codeLevelThresholds = {
55
+ ...(baseGuard.codeLevelThresholds || {}),
56
+ ...(projectGuard.codeLevelThresholds || {}),
57
+ };
58
+ }
59
+ if (baseGuard.disabledRules || projectGuard.disabledRules) {
60
+ const base = Array.isArray(baseGuard.disabledRules) ? baseGuard.disabledRules : [];
61
+ const proj = Array.isArray(projectGuard.disabledRules) ? projectGuard.disabledRules : [];
62
+ merged.disabledRules = [...new Set([...base, ...proj])];
63
+ }
64
+ return new GuardCheckEngine(ct.get('database'), { guardConfig: merged });
33
65
  });
34
66
  c.singleton('exclusionManager', (ct) => {
35
67
  const projectRoot = resolveProjectRoot(ct);
@@ -24,6 +24,7 @@ interface BuiltInRule {
24
24
  category?: string;
25
25
  fixSuggestion?: string;
26
26
  excludePaths?: RegExp;
27
+ excludeLinePatterns?: string[];
27
28
  skipComments?: boolean;
28
29
  skipTestBlocks?: boolean;
29
30
  }
@@ -40,6 +41,7 @@ interface GuardRule {
40
41
  type?: string;
41
42
  fixSuggestion?: string | null;
42
43
  excludePaths?: RegExp | string;
44
+ excludeLinePatterns?: string[];
43
45
  skipComments?: boolean;
44
46
  skipTestBlocks?: boolean;
45
47
  astQuery?: {
@@ -62,9 +64,14 @@ interface GuardViolation {
62
64
  suggestedFix: string | null;
63
65
  };
64
66
  }
67
+ /** 每条规则的覆盖配置(支持数字阈值或富对象) */
68
+ interface RuleOverride {
69
+ severity?: string;
70
+ exclude?: string[];
71
+ }
65
72
  interface GuardConfig {
66
73
  disabledRules?: string[];
67
- codeLevelThresholds?: Record<string, number>;
74
+ codeLevelThresholds?: Record<string, number | RuleOverride>;
68
75
  }
69
76
  interface GuardCheckEngineOptions {
70
77
  cacheTTL?: number;
@@ -157,6 +164,11 @@ export declare class GuardCheckEngine {
157
164
  _runAstRuleChecks(code: string, language: string): GuardViolation[];
158
165
  /** 获取 AstAnalyzer 模块(静态 import,带可用性检测) */
159
166
  _getAstAnalyzer(): typeof AstAnalyzerModule;
167
+ /**
168
+ * 合并内置 + 配置级行排除模式,编译为 RegExp 数组
169
+ * 配置来自 guardConfig.codeLevelThresholds[ruleId].exclude
170
+ */
171
+ _getExcludeLineRegexes(ruleId: string, builtIn?: string[]): RegExp[];
160
172
  /**
161
173
  * 将 Guard 命中计数回写到对应 Recipe 的 guard_hit_count
162
174
  * @param violations
@@ -75,6 +75,13 @@ const BUILT_IN_RULES = {
75
75
  dimension: 'file',
76
76
  category: 'safety',
77
77
  fixSuggestion: '使用 as? 配合 guard let / if let 进行安全转换',
78
+ // UIKit 框架契约保证安全的 as! 场景
79
+ excludeLinePatterns: [
80
+ 'dequeueReusableCell.*as\\s*!',
81
+ 'dequeueReusableSupplementaryView.*as\\s*!',
82
+ 'dequeueReusableHeaderFooterView.*as\\s*!',
83
+ 'layerClass.*layer\\s+as\\s*!',
84
+ ],
78
85
  },
79
86
  'swift-force-try': {
80
87
  message: 'try! 在异常时崩溃,建议 do-catch 或 try?',
@@ -689,6 +696,9 @@ export class GuardCheckEngine {
689
696
  }
690
697
  const shouldSkipComments = !!rule.skipComments;
691
698
  const shouldSkipTestBlocks = !!rule.skipTestBlocks;
699
+ // 合并内置 + 配置级排除行模式
700
+ const ruleId = rule.id || rule.name;
701
+ const excludeLineRegexes = this._getExcludeLineRegexes(ruleId, rule.excludeLinePatterns);
692
702
  for (let i = 0; i < lines.length; i++) {
693
703
  // skipComments: 跳过注释行(doc comments / 行注释 / 块注释内)
694
704
  if (shouldSkipComments && commentLines[i]) {
@@ -699,6 +709,10 @@ export class GuardCheckEngine {
699
709
  continue;
700
710
  }
701
711
  if (re.test(lines[i])) {
712
+ // excludeLinePatterns: 跳过匹配排除模式的行(UIKit 框架契约安全等场景)
713
+ if (excludeLineRegexes.length > 0 && excludeLineRegexes.some((ep) => ep.test(lines[i]))) {
714
+ continue;
715
+ }
702
716
  violations.push({
703
717
  ruleId: rule.id || rule.name,
704
718
  message: rule.message,
@@ -711,10 +725,16 @@ export class GuardCheckEngine {
711
725
  }
712
726
  }
713
727
  }
714
- // Code-level 检查(不依赖正则)
728
+ // Code-level 检查(不依赖正则)— 仅传递数字类型阈值
729
+ const numericThresholds = {};
730
+ for (const [k, v] of Object.entries(this._guardConfig.codeLevelThresholds || {})) {
731
+ if (typeof v === 'number') {
732
+ numericThresholds[k] = v;
733
+ }
734
+ }
715
735
  violations.push(...runCodeLevelChecks(code, language, lines, {
716
736
  disabledRules: this._guardConfig.disabledRules,
717
- codeLevelThresholds: this._guardConfig.codeLevelThresholds,
737
+ codeLevelThresholds: numericThresholds,
718
738
  }));
719
739
  // AST 语义规则检查
720
740
  violations.push(...this._runAstRuleChecks(code, language));
@@ -853,6 +873,28 @@ export class GuardCheckEngine {
853
873
  _getAstAnalyzer() {
854
874
  return AstAnalyzerModule;
855
875
  }
876
+ /**
877
+ * 合并内置 + 配置级行排除模式,编译为 RegExp 数组
878
+ * 配置来自 guardConfig.codeLevelThresholds[ruleId].exclude
879
+ */
880
+ _getExcludeLineRegexes(ruleId, builtIn) {
881
+ const patterns = [...(builtIn || [])];
882
+ // 合并项目配置中的 exclude
883
+ const override = this._guardConfig.codeLevelThresholds?.[ruleId];
884
+ if (override && typeof override === 'object' && Array.isArray(override.exclude)) {
885
+ patterns.push(...override.exclude);
886
+ }
887
+ const regexes = [];
888
+ for (const p of patterns) {
889
+ try {
890
+ regexes.push(new RegExp(p));
891
+ }
892
+ catch {
893
+ this.logger.debug(`Invalid excludeLinePattern in rule ${ruleId}: ${p}`);
894
+ }
895
+ }
896
+ return regexes;
897
+ }
856
898
  /**
857
899
  * 将 Guard 命中计数回写到对应 Recipe 的 guard_hit_count
858
900
  * @param violations
@@ -103,7 +103,10 @@ export declare const AppConfigSchema: z.ZodObject<{
103
103
  }, z.core.$strip>>;
104
104
  guard: z.ZodOptional<z.ZodObject<{
105
105
  disabledRules: z.ZodDefault<z.ZodArray<z.ZodString>>;
106
- codeLevelThresholds: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodNumber>>;
106
+ codeLevelThresholds: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodNumber, z.ZodObject<{
107
+ severity: z.ZodOptional<z.ZodString>;
108
+ exclude: z.ZodOptional<z.ZodArray<z.ZodString>>;
109
+ }, z.core.$strip>]>>>;
107
110
  }, z.core.$strip>>;
108
111
  taskGraph: z.ZodOptional<z.ZodObject<{
109
112
  decision: z.ZodOptional<z.ZodObject<{
@@ -81,9 +81,16 @@ const QualityGateConfig = z.object({
81
81
  maxWarnings: z.number().int().min(0).default(20),
82
82
  minScore: z.number().int().min(0).max(100).default(70),
83
83
  });
84
+ const RuleOverrideConfig = z.union([
85
+ z.number().int().min(0),
86
+ z.object({
87
+ severity: z.string().optional(),
88
+ exclude: z.array(z.string()).optional(),
89
+ }),
90
+ ]);
84
91
  const GuardConfig = z.object({
85
92
  disabledRules: z.array(z.string()).default([]),
86
- codeLevelThresholds: z.record(z.string(), z.number().int().min(0)).default({}),
93
+ codeLevelThresholds: z.record(z.string(), RuleOverrideConfig).default({}),
87
94
  });
88
95
  const TaskDecisionConfig = z.object({
89
96
  staleDays: z.number().int().min(1).default(30),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autosnippet",
3
- "version": "3.2.21",
3
+ "version": "3.2.22",
4
4
  "description": "Extract code patterns into a knowledge base for AI coding assistants",
5
5
  "type": "module",
6
6
  "main": "dist/lib/bootstrap.js",