pumuki 6.3.307 → 6.3.308
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.
- package/CHANGELOG.md +6 -0
- package/core/facts/detectors/text/ios.test.ts +459 -0
- package/core/facts/detectors/text/ios.ts +484 -0
- package/core/facts/extractHeuristicFacts.ts +17 -4
- package/core/rules/presets/heuristics/ios.test.ts +58 -1
- package/core/rules/presets/heuristics/ios.ts +239 -0
- package/integrations/config/skillsDetectorRegistry.ts +94 -0
- package/integrations/evidence/buildEvidence.ts +46 -0
- package/integrations/lifecycle/adapter.templates.json +50 -1
- package/integrations/lifecycle/doctor.ts +41 -5
- package/integrations/lifecycle/preWriteLease.ts +5 -1
- package/package.json +1 -1
- package/scripts/framework-menu-matrix-canary-evidence.ts +42 -1
- package/scripts/framework-menu-matrix-canary-lib.ts +25 -2
- package/scripts/framework-menu-matrix-canary-scenario.ts +3 -1
- package/scripts/framework-menu-matrix-canary-types.ts +6 -1
- package/scripts/framework-menu-system-notifications-macos-dialog-payload.ts +2 -1
- package/scripts/framework-menu-system-notifications-payloads-blocked.ts +76 -2
- package/scripts/framework-menu-system-notifications-payloads.ts +1 -0
- package/skills.lock.json +1 -1
|
@@ -204,6 +204,25 @@ export const hasSwiftNonPascalCaseTypeDeclarationUsage = (source: string): boole
|
|
|
204
204
|
return collectSwiftNonPascalCaseTypeDeclarationLines(source).length > 0;
|
|
205
205
|
};
|
|
206
206
|
|
|
207
|
+
export const collectSwiftEndpointEnumLines = (source: string): readonly number[] => {
|
|
208
|
+
const matches: number[] = [];
|
|
209
|
+
const endpointEnumPattern =
|
|
210
|
+
/\benum\s+(?:APIEndpoint|[A-Za-z_][A-Za-z0-9_]*(?:Endpoint|Endpoints))\b/;
|
|
211
|
+
|
|
212
|
+
source.split(/\r?\n/).forEach((rawLine, index) => {
|
|
213
|
+
const line = stripSwiftLineForSemanticScan(rawLine);
|
|
214
|
+
if (endpointEnumPattern.test(line)) {
|
|
215
|
+
matches.push(index + 1);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
return sortedUniqueLines(matches);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
export const hasSwiftEndpointEnumUsage = (source: string): boolean => {
|
|
223
|
+
return collectSwiftEndpointEnumLines(source).length > 0;
|
|
224
|
+
};
|
|
225
|
+
|
|
207
226
|
const collectSwiftMethodBodyLines = (
|
|
208
227
|
source: string,
|
|
209
228
|
methodPattern: RegExp,
|
|
@@ -857,6 +876,62 @@ export const hasSwiftTaskDetachedUsage = (source: string): boolean => {
|
|
|
857
876
|
});
|
|
858
877
|
};
|
|
859
878
|
|
|
879
|
+
const swiftCancellationCheckPattern =
|
|
880
|
+
/\bTask\s*\.\s*isCancelled\b|\bTask\s*\.\s*checkCancellation\s*\(/;
|
|
881
|
+
|
|
882
|
+
const swiftLoopPattern = /\b(?:for\s+[A-Za-z_][A-Za-z0-9_]*\s+in|while\s+|repeat\b)/;
|
|
883
|
+
|
|
884
|
+
export const collectSwiftLongAsyncOperationWithoutCancellationCheckLines = (
|
|
885
|
+
source: string
|
|
886
|
+
): readonly number[] => {
|
|
887
|
+
const lines = source.split(/\r?\n/);
|
|
888
|
+
const matches: number[] = [];
|
|
889
|
+
|
|
890
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
891
|
+
const declarationLine = stripSwiftLineForSemanticScan(lines[index] ?? '');
|
|
892
|
+
if (!/\bfunc\s+[A-Za-z_][A-Za-z0-9_]*[\s\S]*\basync\b/.test(declarationLine)) {
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
let braceDepth =
|
|
897
|
+
countTokenOccurrences(declarationLine, '{') - countTokenOccurrences(declarationLine, '}');
|
|
898
|
+
if (braceDepth <= 0 && !declarationLine.includes('{')) {
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
const localLoopLines: number[] = [];
|
|
903
|
+
let hasCancellationCheck = swiftCancellationCheckPattern.test(declarationLine);
|
|
904
|
+
|
|
905
|
+
for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
|
|
906
|
+
const line = stripSwiftLineForSemanticScan(lines[cursor] ?? '');
|
|
907
|
+
if (swiftCancellationCheckPattern.test(line)) {
|
|
908
|
+
hasCancellationCheck = true;
|
|
909
|
+
}
|
|
910
|
+
if (swiftLoopPattern.test(line)) {
|
|
911
|
+
localLoopLines.push(cursor + 1);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
braceDepth += countTokenOccurrences(line, '{');
|
|
915
|
+
braceDepth -= countTokenOccurrences(line, '}');
|
|
916
|
+
if (braceDepth <= 0) {
|
|
917
|
+
break;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
if (!hasCancellationCheck && localLoopLines.length > 0) {
|
|
922
|
+
matches.push(...localLoopLines);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
return sortedUniqueLines(matches);
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
export const hasSwiftLongAsyncOperationWithoutCancellationCheckUsage = (
|
|
930
|
+
source: string
|
|
931
|
+
): boolean => {
|
|
932
|
+
return collectSwiftLongAsyncOperationWithoutCancellationCheckLines(source).length > 0;
|
|
933
|
+
};
|
|
934
|
+
|
|
860
935
|
const findMatchingSwiftBrace = (source: string, openBraceIndex: number): number => {
|
|
861
936
|
let depth = 0;
|
|
862
937
|
for (let index = openBraceIndex; index < source.length; index += 1) {
|
|
@@ -899,6 +974,20 @@ export const hasSwiftAsyncWithoutAwaitUsage = (source: string): boolean => {
|
|
|
899
974
|
return false;
|
|
900
975
|
};
|
|
901
976
|
|
|
977
|
+
export const collectSwiftDummyAwaitLines = (source: string): readonly number[] => {
|
|
978
|
+
return sortedUniqueLines([
|
|
979
|
+
...collectSwiftRegexLines(source, /\bawait\s+Task\s*\.\s*yield\s*\(\s*\)/),
|
|
980
|
+
...collectSwiftRegexLines(
|
|
981
|
+
source,
|
|
982
|
+
/\bawait\s+Task\s*\.\s*sleep\s*\([^)]*(?:nanoseconds\s*:\s*0\b|for\s*:\s*\.(?:zero|seconds\s*\(\s*0\s*\)))/,
|
|
983
|
+
),
|
|
984
|
+
]);
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
export const hasSwiftDummyAwaitUsage = (source: string): boolean => {
|
|
988
|
+
return collectSwiftDummyAwaitLines(source).length > 0;
|
|
989
|
+
};
|
|
990
|
+
|
|
902
991
|
export const hasSwiftEmptyCatchUsage = (source: string): boolean => {
|
|
903
992
|
const sanitized = sanitizeSwiftSourceForMultilineRegex(source);
|
|
904
993
|
return /\bcatch(?:\s+(?:let|var)\s+[A-Za-z_][A-Za-z0-9_]*)?\s*\{\s*\}/.test(sanitized);
|
|
@@ -952,6 +1041,57 @@ export const hasSwiftPackageToolsVersionBelow62Usage = (source: string): boolean
|
|
|
952
1041
|
return collectSwiftPackageToolsVersionBelow62Lines(source).length > 0;
|
|
953
1042
|
};
|
|
954
1043
|
|
|
1044
|
+
export const collectSwiftPackageDefaultIsolationNotMainActorLines = (
|
|
1045
|
+
source: string
|
|
1046
|
+
): readonly number[] => {
|
|
1047
|
+
const lines: number[] = [];
|
|
1048
|
+
|
|
1049
|
+
source.split(/\r?\n/).forEach((rawLine, index) => {
|
|
1050
|
+
const line = stripSwiftLineForSemanticScan(rawLine);
|
|
1051
|
+
const match = /\.defaultIsolation\s*\(\s*([A-Za-z_][A-Za-z0-9_.]*)\s*\)/.exec(line);
|
|
1052
|
+
if (!match) {
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
const value = (match[1] ?? '').trim().toLowerCase();
|
|
1057
|
+
if (value !== 'mainactor' && value !== 'mainactor.self') {
|
|
1058
|
+
lines.push(index + 1);
|
|
1059
|
+
}
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
return sortedUniqueLines(lines);
|
|
1063
|
+
};
|
|
1064
|
+
|
|
1065
|
+
export const hasSwiftPackageDefaultIsolationNotMainActorUsage = (source: string): boolean => {
|
|
1066
|
+
return collectSwiftPackageDefaultIsolationNotMainActorLines(source).length > 0;
|
|
1067
|
+
};
|
|
1068
|
+
|
|
1069
|
+
export const collectSwiftPackageStrictConcurrencyBelowCompleteLines = (
|
|
1070
|
+
source: string
|
|
1071
|
+
): readonly number[] => {
|
|
1072
|
+
const lines: number[] = [];
|
|
1073
|
+
|
|
1074
|
+
source.split(/\r?\n/).forEach((rawLine, index) => {
|
|
1075
|
+
const line = rawLine.trim();
|
|
1076
|
+
if (line.startsWith('//')) {
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
const match =
|
|
1080
|
+
/\.enable(?:Experimental|Upcoming)Feature\s*\(\s*"StrictConcurrency\s*=\s*(minimal|targeted)"\s*\)/i.exec(
|
|
1081
|
+
line
|
|
1082
|
+
);
|
|
1083
|
+
if (match) {
|
|
1084
|
+
lines.push(index + 1);
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
return sortedUniqueLines(lines);
|
|
1089
|
+
};
|
|
1090
|
+
|
|
1091
|
+
export const hasSwiftPackageStrictConcurrencyBelowCompleteUsage = (source: string): boolean => {
|
|
1092
|
+
return collectSwiftPackageStrictConcurrencyBelowCompleteLines(source).length > 0;
|
|
1093
|
+
};
|
|
1094
|
+
|
|
955
1095
|
export const collectSwiftStrictConcurrencyBelowCompleteLines = (source: string): readonly number[] => {
|
|
956
1096
|
const lines: number[] = [];
|
|
957
1097
|
|
|
@@ -975,6 +1115,33 @@ export const hasSwiftStrictConcurrencyBelowCompleteUsage = (source: string): boo
|
|
|
975
1115
|
return collectSwiftStrictConcurrencyBelowCompleteLines(source).length > 0;
|
|
976
1116
|
};
|
|
977
1117
|
|
|
1118
|
+
export const collectSwiftDefaultActorIsolationNotMainActorLines = (
|
|
1119
|
+
source: string
|
|
1120
|
+
): readonly number[] => {
|
|
1121
|
+
const lines: number[] = [];
|
|
1122
|
+
|
|
1123
|
+
source.split(/\r?\n/).forEach((rawLine, index) => {
|
|
1124
|
+
const line = stripSwiftLineForSemanticScan(rawLine);
|
|
1125
|
+
const match = /\bSWIFT_DEFAULT_ACTOR_ISOLATION\s*=\s*([A-Za-z_][A-Za-z0-9_.]*)\s*;?/.exec(
|
|
1126
|
+
line
|
|
1127
|
+
);
|
|
1128
|
+
if (!match) {
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
const value = (match[1] ?? '').trim().toLowerCase();
|
|
1133
|
+
if (value !== 'mainactor' && value !== 'mainactor.self') {
|
|
1134
|
+
lines.push(index + 1);
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
return sortedUniqueLines(lines);
|
|
1139
|
+
};
|
|
1140
|
+
|
|
1141
|
+
export const hasSwiftDefaultActorIsolationNotMainActorUsage = (source: string): boolean => {
|
|
1142
|
+
return collectSwiftDefaultActorIsolationNotMainActorLines(source).length > 0;
|
|
1143
|
+
};
|
|
1144
|
+
|
|
978
1145
|
export const hasSwiftOnAppearTaskUsage = (source: string): boolean => {
|
|
979
1146
|
return collectSwiftOnAppearTaskLines(source).length > 0;
|
|
980
1147
|
};
|
|
@@ -1474,6 +1641,18 @@ export const hasSwiftMainThreadBlockingSleepUsage = (source: string): boolean =>
|
|
|
1474
1641
|
});
|
|
1475
1642
|
};
|
|
1476
1643
|
|
|
1644
|
+
export const collectSwiftThreadCentricDebuggingLines = (source: string): readonly number[] => {
|
|
1645
|
+
return sortedUniqueLines([
|
|
1646
|
+
...collectSwiftRegexLines(source, /\bThread\s*\.\s*(?:current|isMainThread)\b/),
|
|
1647
|
+
...collectSwiftRegexLines(source, /\bpthread_self\s*\(/),
|
|
1648
|
+
...collectSwiftRegexLines(source, /\bpthread_mach_thread_np\s*\(/),
|
|
1649
|
+
]);
|
|
1650
|
+
};
|
|
1651
|
+
|
|
1652
|
+
export const hasSwiftThreadCentricDebuggingUsage = (source: string): boolean => {
|
|
1653
|
+
return collectSwiftThreadCentricDebuggingLines(source).length > 0;
|
|
1654
|
+
};
|
|
1655
|
+
|
|
1477
1656
|
export const hasSwiftIconOnlyControlWithoutAccessibilityLabelUsage = (source: string): boolean => {
|
|
1478
1657
|
const sanitized = sanitizeSwiftSourceForMultilineRegex(source);
|
|
1479
1658
|
const iconOnlyButtonPattern =
|
|
@@ -1569,6 +1748,227 @@ export const hasSwiftBindableMissingForObservableBindingUsage = (source: string)
|
|
|
1569
1748
|
return collectSwiftBindableMissingForObservableBindingLines(source).length > 0;
|
|
1570
1749
|
};
|
|
1571
1750
|
|
|
1751
|
+
const allowedCrossFeatureImportModules = new Set([
|
|
1752
|
+
'Combine',
|
|
1753
|
+
'CoreData',
|
|
1754
|
+
'DesignSystem',
|
|
1755
|
+
'Foundation',
|
|
1756
|
+
'LocalAuthentication',
|
|
1757
|
+
'MapKit',
|
|
1758
|
+
'Navigation',
|
|
1759
|
+
'Observation',
|
|
1760
|
+
'PhotosUI',
|
|
1761
|
+
'Shared',
|
|
1762
|
+
'SharedKernel',
|
|
1763
|
+
'SwiftData',
|
|
1764
|
+
'SwiftUI',
|
|
1765
|
+
'Testing',
|
|
1766
|
+
'UIKit',
|
|
1767
|
+
'XCTest',
|
|
1768
|
+
]);
|
|
1769
|
+
|
|
1770
|
+
const featureNameFromSwiftPath = (path: string): string | undefined => {
|
|
1771
|
+
const normalized = path.replace(/\\/g, '/');
|
|
1772
|
+
const match = /\/Features\/([^/]+)\//.exec(normalized);
|
|
1773
|
+
return match?.[1];
|
|
1774
|
+
};
|
|
1775
|
+
|
|
1776
|
+
export const collectSwiftCrossFeatureImportLines = (
|
|
1777
|
+
source: string,
|
|
1778
|
+
path: string
|
|
1779
|
+
): readonly number[] => {
|
|
1780
|
+
const currentFeature = featureNameFromSwiftPath(path);
|
|
1781
|
+
if (!currentFeature) {
|
|
1782
|
+
return [];
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
const lines: number[] = [];
|
|
1786
|
+
source.split(/\r?\n/).forEach((rawLine, index) => {
|
|
1787
|
+
const line = stripSwiftLineForSemanticScan(rawLine);
|
|
1788
|
+
const match = /^\s*import\s+(?:@\w+\s+)?([A-Za-z_][A-Za-z0-9_]*)\b/.exec(line);
|
|
1789
|
+
if (!match?.[1]) {
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
const moduleName = match[1];
|
|
1794
|
+
if (moduleName === currentFeature || allowedCrossFeatureImportModules.has(moduleName)) {
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1797
|
+
if (moduleName.endsWith('Kit') || moduleName.startsWith('Swift')) {
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
lines.push(index + 1);
|
|
1802
|
+
});
|
|
1803
|
+
|
|
1804
|
+
return sortedUniqueLines(lines);
|
|
1805
|
+
};
|
|
1806
|
+
|
|
1807
|
+
export const hasSwiftCrossFeatureImportUsage = (source: string, path: string): boolean => {
|
|
1808
|
+
return collectSwiftCrossFeatureImportLines(source, path).length > 0;
|
|
1809
|
+
};
|
|
1810
|
+
|
|
1811
|
+
type SwiftArchitectureLayer = 'domain' | 'application' | 'presentation' | 'infrastructure';
|
|
1812
|
+
|
|
1813
|
+
const forbiddenLayerImports: Record<SwiftArchitectureLayer, ReadonlySet<string>> = {
|
|
1814
|
+
domain: new Set([
|
|
1815
|
+
'Alamofire',
|
|
1816
|
+
'AppKit',
|
|
1817
|
+
'CloudKit',
|
|
1818
|
+
'Combine',
|
|
1819
|
+
'ComposableArchitecture',
|
|
1820
|
+
'CoreData',
|
|
1821
|
+
'Firebase',
|
|
1822
|
+
'GRDB',
|
|
1823
|
+
'Kingfisher',
|
|
1824
|
+
'RealmSwift',
|
|
1825
|
+
'Swinject',
|
|
1826
|
+
'SwiftData',
|
|
1827
|
+
'SwiftUI',
|
|
1828
|
+
'UIKit',
|
|
1829
|
+
'WatchKit',
|
|
1830
|
+
]),
|
|
1831
|
+
application: new Set([
|
|
1832
|
+
'Alamofire',
|
|
1833
|
+
'AppKit',
|
|
1834
|
+
'CloudKit',
|
|
1835
|
+
'ComposableArchitecture',
|
|
1836
|
+
'CoreData',
|
|
1837
|
+
'Firebase',
|
|
1838
|
+
'GRDB',
|
|
1839
|
+
'Kingfisher',
|
|
1840
|
+
'RealmSwift',
|
|
1841
|
+
'Swinject',
|
|
1842
|
+
'SwiftData',
|
|
1843
|
+
'SwiftUI',
|
|
1844
|
+
'UIKit',
|
|
1845
|
+
'WatchKit',
|
|
1846
|
+
]),
|
|
1847
|
+
presentation: new Set([
|
|
1848
|
+
'Alamofire',
|
|
1849
|
+
'CloudKit',
|
|
1850
|
+
'CoreData',
|
|
1851
|
+
'Firebase',
|
|
1852
|
+
'GRDB',
|
|
1853
|
+
'RealmSwift',
|
|
1854
|
+
'Swinject',
|
|
1855
|
+
'SwiftData',
|
|
1856
|
+
]),
|
|
1857
|
+
infrastructure: new Set([]),
|
|
1858
|
+
};
|
|
1859
|
+
|
|
1860
|
+
const swiftArchitectureLayerFromPath = (path: string): SwiftArchitectureLayer | undefined => {
|
|
1861
|
+
const normalized = path.replace(/\\/g, '/').toLowerCase();
|
|
1862
|
+
if (normalized.includes('/domain/')) {
|
|
1863
|
+
return 'domain';
|
|
1864
|
+
}
|
|
1865
|
+
if (normalized.includes('/application/') || normalized.includes('/usecases/')) {
|
|
1866
|
+
return 'application';
|
|
1867
|
+
}
|
|
1868
|
+
if (normalized.includes('/presentation/')) {
|
|
1869
|
+
return 'presentation';
|
|
1870
|
+
}
|
|
1871
|
+
if (normalized.includes('/infrastructure/')) {
|
|
1872
|
+
return 'infrastructure';
|
|
1873
|
+
}
|
|
1874
|
+
return undefined;
|
|
1875
|
+
};
|
|
1876
|
+
|
|
1877
|
+
const hasForbiddenLayerImportName = (
|
|
1878
|
+
layer: SwiftArchitectureLayer,
|
|
1879
|
+
moduleName: string
|
|
1880
|
+
): boolean => {
|
|
1881
|
+
if (forbiddenLayerImports[layer].has(moduleName)) {
|
|
1882
|
+
return true;
|
|
1883
|
+
}
|
|
1884
|
+
if (moduleName.startsWith('Firebase')) {
|
|
1885
|
+
return true;
|
|
1886
|
+
}
|
|
1887
|
+
if (layer === 'domain') {
|
|
1888
|
+
return (
|
|
1889
|
+
moduleName.includes('Application') ||
|
|
1890
|
+
moduleName.includes('Presentation') ||
|
|
1891
|
+
moduleName.includes('Infrastructure')
|
|
1892
|
+
);
|
|
1893
|
+
}
|
|
1894
|
+
if (layer === 'application') {
|
|
1895
|
+
return moduleName.includes('Presentation') || moduleName.includes('Infrastructure');
|
|
1896
|
+
}
|
|
1897
|
+
if (layer === 'presentation') {
|
|
1898
|
+
return moduleName.includes('Infrastructure');
|
|
1899
|
+
}
|
|
1900
|
+
return false;
|
|
1901
|
+
};
|
|
1902
|
+
|
|
1903
|
+
export const collectSwiftLayerDirectionViolationLines = (
|
|
1904
|
+
source: string,
|
|
1905
|
+
path: string
|
|
1906
|
+
): readonly number[] => {
|
|
1907
|
+
const layer = swiftArchitectureLayerFromPath(path);
|
|
1908
|
+
if (!layer) {
|
|
1909
|
+
return [];
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
if (layer === 'infrastructure') {
|
|
1913
|
+
return [];
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
const lines: number[] = [];
|
|
1917
|
+
source.split(/\r?\n/).forEach((rawLine, index) => {
|
|
1918
|
+
const line = stripSwiftLineForSemanticScan(rawLine);
|
|
1919
|
+
const match = /^\s*import\s+(?:@\w+\s+)?([A-Za-z_][A-Za-z0-9_]*)\b/.exec(line);
|
|
1920
|
+
if (match?.[1] && hasForbiddenLayerImportName(layer, match[1])) {
|
|
1921
|
+
lines.push(index + 1);
|
|
1922
|
+
}
|
|
1923
|
+
});
|
|
1924
|
+
|
|
1925
|
+
return sortedUniqueLines(lines);
|
|
1926
|
+
};
|
|
1927
|
+
|
|
1928
|
+
export const hasSwiftLayerDirectionViolationUsage = (source: string, path: string): boolean => {
|
|
1929
|
+
return collectSwiftLayerDirectionViolationLines(source, path).length > 0;
|
|
1930
|
+
};
|
|
1931
|
+
|
|
1932
|
+
const isSwiftAppImplementationPath = (path: string): boolean => {
|
|
1933
|
+
const normalized = path.replace(/\\/g, '/').toLowerCase();
|
|
1934
|
+
if (
|
|
1935
|
+
!normalized.endsWith('.swift') ||
|
|
1936
|
+
/(^|\/)(tests?|uitests?|testsupport|fixtures?|mocks?)(\/|$)/.test(normalized) ||
|
|
1937
|
+
/(?:tests?|spec)\.swift$/.test(normalized)
|
|
1938
|
+
) {
|
|
1939
|
+
return false;
|
|
1940
|
+
}
|
|
1941
|
+
if (normalized.includes('/sources/') || normalized.includes('/public/') || normalized.includes('/exports/')) {
|
|
1942
|
+
return false;
|
|
1943
|
+
}
|
|
1944
|
+
return normalized.includes('/apps/ios/') || normalized.includes('/ios/');
|
|
1945
|
+
};
|
|
1946
|
+
|
|
1947
|
+
export const collectSwiftExcessivePublicApiLines = (
|
|
1948
|
+
source: string,
|
|
1949
|
+
path: string
|
|
1950
|
+
): readonly number[] => {
|
|
1951
|
+
if (!isSwiftAppImplementationPath(path)) {
|
|
1952
|
+
return [];
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
const lines: number[] = [];
|
|
1956
|
+
source.split(/\r?\n/).forEach((rawLine, index) => {
|
|
1957
|
+
const line = stripSwiftLineForSemanticScan(rawLine);
|
|
1958
|
+
if (
|
|
1959
|
+
/^\s*(?:@(?:MainActor|Observable|objc|objcMembers|available|Published|ViewBuilder|discardableResult)[^\n]*\s*)*(?:public|open)\s+(?:(?:final|static|class|override|mutating|nonisolated)\s+)*(?:class|struct|enum|actor|protocol|extension|func|var|let|subscript|init)\b/.test(line)
|
|
1960
|
+
) {
|
|
1961
|
+
lines.push(index + 1);
|
|
1962
|
+
}
|
|
1963
|
+
});
|
|
1964
|
+
|
|
1965
|
+
return lines.length >= 3 ? sortedUniqueLines(lines) : [];
|
|
1966
|
+
};
|
|
1967
|
+
|
|
1968
|
+
export const hasSwiftExcessivePublicApiUsage = (source: string, path: string): boolean => {
|
|
1969
|
+
return collectSwiftExcessivePublicApiLines(source, path).length > 0;
|
|
1970
|
+
};
|
|
1971
|
+
|
|
1572
1972
|
export const hasSwiftUncheckedSendableUsage = (source: string): boolean => {
|
|
1573
1973
|
return scanCodeLikeSource(source, ({ source: swiftSource, index, current }) => {
|
|
1574
1974
|
if (current !== '@' || !swiftSource.startsWith('@unchecked', index)) {
|
|
@@ -2051,6 +2451,72 @@ export const hasSwiftOnTapGestureWithoutButtonTraitUsage = (source: string): boo
|
|
|
2051
2451
|
return collectSwiftOnTapGestureWithoutButtonTraitLines(source).length > 0;
|
|
2052
2452
|
};
|
|
2053
2453
|
|
|
2454
|
+
export const collectSwiftGlassInteractiveOnStaticElementLines = (source: string): readonly number[] => {
|
|
2455
|
+
const sanitizedLines = sanitizeSwiftSourceForMultilineRegex(source).split(/\r?\n/);
|
|
2456
|
+
const originalLines = source.split(/\r?\n/);
|
|
2457
|
+
const matches: number[] = [];
|
|
2458
|
+
|
|
2459
|
+
for (let index = 0; index < sanitizedLines.length; index += 1) {
|
|
2460
|
+
const line = sanitizedLines[index] ?? '';
|
|
2461
|
+
if (!/\.glassEffect\s*\(/.test(line)) {
|
|
2462
|
+
continue;
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
const previousWindow = sanitizedLines.slice(Math.max(0, index - 4), index).join('\n');
|
|
2466
|
+
const followingWindow = sanitizedLines
|
|
2467
|
+
.slice(index, Math.min(sanitizedLines.length, index + 8))
|
|
2468
|
+
.join('\n');
|
|
2469
|
+
const modifierWindow = `${previousWindow}\n${followingWindow}`;
|
|
2470
|
+
const currentStartsModifierChain = /^\s*\./.test(line);
|
|
2471
|
+
|
|
2472
|
+
if (!/\.interactive\s*\(/.test(modifierWindow)) {
|
|
2473
|
+
continue;
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
if (
|
|
2477
|
+
/\b(?:Button|NavigationLink|Menu|Toggle|Picker|Slider|Stepper|TextField|SecureField)\s*(?:<[^>]+>)?\s*\(/.test(
|
|
2478
|
+
modifierWindow
|
|
2479
|
+
) ||
|
|
2480
|
+
/^\s*\.onTapGesture\s*(?:\(|\{)/m.test(followingWindow) ||
|
|
2481
|
+
(currentStartsModifierChain && /\.onTapGesture\s*(?:\(|\{)/.test(previousWindow)) ||
|
|
2482
|
+
/^\s*\.accessibilityAction\s*\(/m.test(followingWindow) ||
|
|
2483
|
+
(currentStartsModifierChain && /\.accessibilityAction\s*\(/.test(previousWindow)) ||
|
|
2484
|
+
/^\s*\.accessibilityAddTraits\s*\(\s*(?:AccessibilityTraits\s*\.\s*)?\.isButton\s*\)/m.test(followingWindow) ||
|
|
2485
|
+
(currentStartsModifierChain &&
|
|
2486
|
+
/\.accessibilityAddTraits\s*\(\s*(?:AccessibilityTraits\s*\.\s*)?\.isButton\s*\)/.test(previousWindow)) ||
|
|
2487
|
+
/^\s*\.focusable\s*\(\s*true\s*\)/m.test(followingWindow) ||
|
|
2488
|
+
(currentStartsModifierChain && /\.focusable\s*\(\s*true\s*\)/.test(previousWindow))
|
|
2489
|
+
) {
|
|
2490
|
+
continue;
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
if (!/\.glassEffect\s*\(/.test(stripSwiftLineForSemanticScan(originalLines[index] ?? ''))) {
|
|
2494
|
+
continue;
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
matches.push(index + 1);
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
return sortedUniqueLines(matches);
|
|
2501
|
+
};
|
|
2502
|
+
|
|
2503
|
+
export const hasSwiftGlassInteractiveOnStaticElementUsage = (source: string): boolean => {
|
|
2504
|
+
return collectSwiftGlassInteractiveOnStaticElementLines(source).length > 0;
|
|
2505
|
+
};
|
|
2506
|
+
|
|
2507
|
+
export const collectSwiftGlassEffectIDWithoutNamespaceLines = (source: string): readonly number[] => {
|
|
2508
|
+
const sanitized = sanitizeSwiftSourceForMultilineRegex(source);
|
|
2509
|
+
if (/@Namespace\b/.test(sanitized) || /\bNamespace\s*\.\s*ID\b/.test(sanitized)) {
|
|
2510
|
+
return [];
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
return collectSwiftRegexLines(source, /\.\s*glassEffectID\s*\(/);
|
|
2514
|
+
};
|
|
2515
|
+
|
|
2516
|
+
export const hasSwiftGlassEffectIDWithoutNamespaceUsage = (source: string): boolean => {
|
|
2517
|
+
return collectSwiftGlassEffectIDWithoutNamespaceLines(source).length > 0;
|
|
2518
|
+
};
|
|
2519
|
+
|
|
2054
2520
|
export const hasSwiftStringFormatUsage = (source: string): boolean => {
|
|
2055
2521
|
return scanCodeLikeSource(source, ({ source: swiftSource, index, current }) => {
|
|
2056
2522
|
if (current !== 'S' || !hasIdentifierAt(swiftSource, index, 'String')) {
|
|
@@ -2257,6 +2723,24 @@ export const collectSwiftQuickNimbleLines = (source: string): readonly number[]
|
|
|
2257
2723
|
]);
|
|
2258
2724
|
};
|
|
2259
2725
|
|
|
2726
|
+
export const hasSwiftThirdPartyUiTestFrameworkUsage = (source: string): boolean => {
|
|
2727
|
+
return hasSwiftSanitizedRegexMatch(
|
|
2728
|
+
source,
|
|
2729
|
+
/\bimport\s+(?:KIF|EarlGrey|GREYMatchers|GREYActions|Detox|Appium|Calabash)\b|\b(?:tester|KIFUITestActor|EarlGrey|GREYMatchers|GREYActions|Detox|Appium|Calabash)\b/
|
|
2730
|
+
);
|
|
2731
|
+
};
|
|
2732
|
+
|
|
2733
|
+
export const collectSwiftThirdPartyUiTestFrameworkLines = (source: string): readonly number[] => {
|
|
2734
|
+
if (!hasSwiftThirdPartyUiTestFrameworkUsage(source)) {
|
|
2735
|
+
return [];
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
return sortedUniqueLines([
|
|
2739
|
+
...collectSwiftRegexLines(source, /\bimport\s+(?:KIF|EarlGrey|GREYMatchers|GREYActions|Detox|Appium|Calabash)\b/),
|
|
2740
|
+
...collectSwiftRegexLines(source, /\b(?:tester|KIFUITestActor|EarlGrey|GREYMatchers|GREYActions|Detox|Appium|Calabash)\b/),
|
|
2741
|
+
]);
|
|
2742
|
+
};
|
|
2743
|
+
|
|
2260
2744
|
export const hasSwiftXCTestAssertionUsage = (source: string): boolean => {
|
|
2261
2745
|
if (hasSwiftLegacyXCTestUiOrPerformanceUsage(source)) {
|
|
2262
2746
|
return false;
|