pumuki 6.3.340 → 6.3.342

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.
@@ -1129,6 +1129,53 @@ struct FeedView: View {
1129
1129
  assert.equal(hasSwiftUiForEachConditionalViewCountUsage(safe), false);
1130
1130
  });
1131
1131
 
1132
+ test('hasSwiftUiForEachConditionalViewCountUsage preserva filas ForEach extraidas con branching interno', () => {
1133
+ const source = `
1134
+ struct OnboardingCarouselView: View {
1135
+ let pages: [OnboardingPage]
1136
+ let indicators: [OnboardingIndicator]
1137
+
1138
+ var body: some View {
1139
+ LazyVStack {
1140
+ ForEach(pages) { page in
1141
+ OnboardingPageView(page: page)
1142
+ }
1143
+ LazyHStack {
1144
+ ForEach(indicators) { indicator in
1145
+ OnboardingPageIndicatorView(indicator: indicator)
1146
+ }
1147
+ }
1148
+ }
1149
+ }
1150
+ }
1151
+
1152
+ private struct OnboardingPageView: View {
1153
+ let page: OnboardingPage
1154
+
1155
+ var body: some View {
1156
+ switch page.kind {
1157
+ case .hero:
1158
+ OnboardingHeroStepView(page: page)
1159
+ case .feature:
1160
+ OnboardingFeatureStepView(page: page)
1161
+ }
1162
+ }
1163
+ }
1164
+
1165
+ private struct OnboardingPageIndicatorView: View {
1166
+ let indicator: OnboardingIndicator
1167
+
1168
+ var body: some View {
1169
+ Capsule()
1170
+ .fill(indicator.isSelected ? Color.primary : Color.secondary)
1171
+ }
1172
+ }
1173
+ `;
1174
+
1175
+ assert.equal(hasSwiftUiForEachConditionalViewCountUsage(source), false);
1176
+ assert.deepEqual(collectSwiftUiForEachConditionalViewCountLines(source), []);
1177
+ });
1178
+
1132
1179
  test('hasSwiftViewBodyObjectCreationUsage detecta formatter creado en body y preserva dependencia externa', () => {
1133
1180
  const source = `
1134
1181
  struct PriceView: View {
@@ -2781,7 +2828,7 @@ struct StoresView: View {
2781
2828
  assert.deepEqual(collectSwiftNonLazyScrollForEachLines(source), [4, 6, 11]);
2782
2829
  assert.deepEqual(collectSwiftForEachIndicesLines(source), [6]);
2783
2830
  assert.deepEqual(collectSwiftForEachSelfIdentityLines(source), [6]);
2784
- assert.deepEqual(collectSwiftUiForEachConditionalViewCountLines(source), [6, 7, 11, 14, 20]);
2831
+ assert.deepEqual(collectSwiftUiForEachConditionalViewCountLines(source), [6, 7]);
2785
2832
  assert.deepEqual(collectSwiftInlineForEachTransformLines(source), [11]);
2786
2833
  assert.deepEqual(collectSwiftUiConditionalSameViewIdentityLines(source), [7, 14, 20]);
2787
2834
  assert.deepEqual(collectSwiftUiInlineActionLogicLines(source), [19]);
@@ -660,24 +660,99 @@ export const collectSwiftNonLazyScrollForEachLines = (source: string): readonly
660
660
  ]);
661
661
  };
662
662
 
663
- export const hasSwiftUiForEachConditionalViewCountUsage = (source: string): boolean => {
664
- const swiftSource = sanitizeSwiftSourceForMultilineRegex(source);
665
- const conditionalForEachPattern =
666
- /\bForEach\s*\([^)]*\)\s*\{[\s\S]{0,1600}\b(?:if|switch)\b[\s\S]{0,1600}\}/;
663
+ const findMatchingSwiftParen = (source: string, openParenIndex: number): number => {
664
+ let depth = 0;
665
+ for (let index = openParenIndex; index < source.length; index += 1) {
666
+ const current = source[index];
667
+ if (current === '(') {
668
+ depth += 1;
669
+ } else if (current === ')') {
670
+ depth -= 1;
671
+ if (depth === 0) {
672
+ return index;
673
+ }
674
+ }
675
+ }
676
+ return -1;
677
+ };
667
678
 
668
- return conditionalForEachPattern.test(swiftSource);
679
+ const getLineNumberAtIndex = (source: string, targetIndex: number): number => {
680
+ let lineNumber = 1;
681
+ for (let index = 0; index < targetIndex && index < source.length; index += 1) {
682
+ if (source[index] === '\n') {
683
+ lineNumber += 1;
684
+ }
685
+ }
686
+ return lineNumber;
687
+ };
688
+
689
+ const collectSwiftUiForEachDirectConditionalLines = (source: string): readonly number[] => {
690
+ const sanitized = sanitizeSwiftSourceForMultilineRegex(source);
691
+ const lines: number[] = [];
692
+ const forEachPattern = /\bForEach\s*\(/g;
693
+
694
+ for (const match of sanitized.matchAll(forEachPattern)) {
695
+ const matchIndex = match.index ?? 0;
696
+ const openParenIndex = sanitized.indexOf('(', matchIndex);
697
+ if (openParenIndex < 0) {
698
+ continue;
699
+ }
700
+
701
+ const closeParenIndex = findMatchingSwiftParen(sanitized, openParenIndex);
702
+ if (closeParenIndex < 0) {
703
+ continue;
704
+ }
705
+
706
+ const openBraceIndex = sanitized.indexOf('{', closeParenIndex);
707
+ if (openBraceIndex < 0) {
708
+ continue;
709
+ }
710
+
711
+ const closeBraceIndex = findMatchingSwiftBrace(sanitized, openBraceIndex);
712
+ if (closeBraceIndex < 0) {
713
+ continue;
714
+ }
715
+
716
+ const body = sanitized.slice(openBraceIndex + 1, closeBraceIndex);
717
+ let depth = 0;
718
+ let hasDirectConditional = false;
719
+ const conditionalPattern = /\b(?:if|switch)\b/g;
720
+
721
+ for (const conditionalMatch of body.matchAll(conditionalPattern)) {
722
+ const conditionalIndex = conditionalMatch.index ?? 0;
723
+ for (let index = 0; index < conditionalIndex; index += 1) {
724
+ const current = body[index];
725
+ if (current === '{') {
726
+ depth += 1;
727
+ } else if (current === '}') {
728
+ depth = Math.max(0, depth - 1);
729
+ }
730
+ }
731
+ if (depth === 0) {
732
+ lines.push(getLineNumberAtIndex(sanitized, matchIndex));
733
+ lines.push(getLineNumberAtIndex(sanitized, openBraceIndex + 1 + conditionalIndex));
734
+ hasDirectConditional = true;
735
+ break;
736
+ }
737
+ depth = 0;
738
+ }
739
+
740
+ if (!hasDirectConditional) {
741
+ continue;
742
+ }
743
+ }
744
+
745
+ return sortedUniqueLines(lines);
746
+ };
747
+
748
+ export const hasSwiftUiForEachConditionalViewCountUsage = (source: string): boolean => {
749
+ return collectSwiftUiForEachDirectConditionalLines(source).length > 0;
669
750
  };
670
751
 
671
752
  export const collectSwiftUiForEachConditionalViewCountLines = (
672
753
  source: string
673
754
  ): readonly number[] => {
674
- if (!hasSwiftUiForEachConditionalViewCountUsage(source)) {
675
- return [];
676
- }
677
- return sortedUniqueLines([
678
- ...collectSwiftRegexLines(source, /\bForEach\s*\(/),
679
- ...collectSwiftRegexLines(source, /\b(?:if|switch)\b/),
680
- ]);
755
+ return collectSwiftUiForEachDirectConditionalLines(source);
681
756
  };
682
757
 
683
758
  export const hasSwiftViewBodyObjectCreationUsage = (source: string): boolean => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pumuki",
3
- "version": "6.3.340",
3
+ "version": "6.3.342",
4
4
  "description": "Enterprise-grade AST Intelligence System with multi-platform support (iOS, Android, Backend, Frontend) and Feature-First + DDD + Clean Architecture enforcement. Includes dynamic violations API for intelligent querying.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -73,6 +73,9 @@ const stripTechnicalFieldsFromMessage = (message: string): string => {
73
73
  const localizeDeveloperText = (value: string): string => {
74
74
  const normalized = normalizeNotificationText(value);
75
75
  const knownTexts: ReadonlyArray<[RegExp, string]> = [
76
+ [/Dynamic Type - Font scaling autom[aá]tico/iu, 'Dynamic Type: tamaño de fuente fijo sin escala dinámica.'],
77
+ [/Dynamic Type - fuentes escalables y layouts adaptativos/iu, 'Dynamic Type: fuente fija o layout no preparado para tamaños de texto del sistema.'],
78
+ [/Use semantic SwiftUI text styles such as \.headline\/\.body, Font\.TextStyle, or UIFontMetrics\/scaled metrics when a custom size is required\./iu, 'Usa estilos semánticos de SwiftUI como .headline o .body. Si necesitas un tamaño custom, escálalo con Font.TextStyle o UIFontMetrics.'],
76
79
  [/Disallow empty catch blocks in backend runtime code\./iu, 'Catch vacío en código backend runtime.'],
77
80
  [/Action handlers should reference methods, not contain inline logic/iu, 'El handler contiene lógica inline; debe llamar a un método o comando del view model.'],
78
81
  [/Ensure constant number of views per ForEach element/iu, 'Cada elemento de ForEach debe devolver una estructura de vistas estable.'],
@@ -134,6 +134,7 @@ const humanizeRuleId = (ruleId: string): string => {
134
134
  'no-wait-for-expectations': 'waitForExpectations en XCTest',
135
135
  'ios-test-quality': 'calidad de tests iOS',
136
136
  'dynamic-type-font-scaling-automatico': 'Dynamic Type',
137
+ 'dynamic-type-font-scaling-automa-tico': 'Dynamic Type',
137
138
  'dynamic-type-fuentes-escalables-y-layouts-adaptativos': 'fuentes escalables y layouts adaptativos',
138
139
  'use-relative-layout-over-hard-coded-constants': 'layout relativo en vez de constantes fijas',
139
140
  'magic-numbers-usar-constantes-con-nombres': 'números mágicos sin constantes con nombre',
@@ -146,10 +147,15 @@ const humanizeRuleId = (ruleId: string): string => {
146
147
  .replace(/_/gu, '-');
147
148
  const parts = normalized.split('.').filter(Boolean);
148
149
  const leaf = parts.at(-1) ?? normalized;
149
- if (knownRules[leaf]) {
150
- return knownRules[leaf];
150
+ const compactLeaf = leaf.replace(/-+/gu, '-');
151
+ const accentlessLeaf = compactLeaf.normalize('NFD').replace(/[\u0300-\u036f]/gu, '');
152
+ if (knownRules[compactLeaf]) {
153
+ return knownRules[compactLeaf];
151
154
  }
152
- return leaf
155
+ if (knownRules[accentlessLeaf]) {
156
+ return knownRules[accentlessLeaf];
157
+ }
158
+ return compactLeaf
153
159
  .replace(/-/gu, ' ')
154
160
  .replace(/\bios\b/giu, 'iOS')
155
161
  .replace(/\bui\b/giu, 'UI')
@@ -160,6 +166,21 @@ const humanizeRuleId = (ruleId: string): string => {
160
166
  .trim();
161
167
  };
162
168
 
169
+ const localizeDeveloperText = (value: string): string => {
170
+ const normalized = normalizeNotificationText(value);
171
+ const knownTexts: ReadonlyArray<[RegExp, string]> = [
172
+ [/Dynamic Type - Font scaling autom[aá]tico/iu, 'Dynamic Type: tamaño de fuente fijo sin escala dinámica.'],
173
+ [/Dynamic Type - fuentes escalables y layouts adaptativos/iu, 'Dynamic Type: fuente fija o layout no preparado para tamaños de texto del sistema.'],
174
+ [/Use semantic SwiftUI text styles such as \.headline\/\.body, Font\.TextStyle, or UIFontMetrics\/scaled metrics when a custom size is required\./iu, 'Usa estilos semánticos de SwiftUI como .headline o .body. Si necesitas un tamaño custom, escálalo con Font.TextStyle o UIFontMetrics.'],
175
+ ];
176
+ for (const [pattern, replacement] of knownTexts) {
177
+ if (pattern.test(normalized)) {
178
+ return replacement;
179
+ }
180
+ }
181
+ return normalized;
182
+ };
183
+
163
184
  const formatVisibleRule = (cause: BlockedCause): string => {
164
185
  const ruleId = formatCauseRule(cause);
165
186
  const readable = humanizeRuleId(ruleId);
@@ -188,13 +209,13 @@ const stripTechnicalFieldsFromMessage = (message: string): string =>
188
209
  const formatCauseProblem = (cause: BlockedCause): string => {
189
210
  const messageField = extractMessageField(cause.message, 'message');
190
211
  if (messageField) {
191
- return messageField;
212
+ return localizeDeveloperText(messageField);
192
213
  }
193
- return stripTechnicalFieldsFromMessage(cause.message) || cause.code;
214
+ return localizeDeveloperText(stripTechnicalFieldsFromMessage(cause.message) || cause.code);
194
215
  };
195
216
 
196
217
  const formatCauseFix = (cause: BlockedCause): string =>
197
- normalizeNotificationText(
218
+ localizeDeveloperText(
198
219
  cause.remediation ??
199
220
  'Corrige la violación indicada con regla, fichero y línea, y vuelve a intentar el commit.'
200
221
  );