fastlane-plugin-fastci 0.0.1

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.
@@ -0,0 +1,328 @@
1
+ import sys
2
+ import os
3
+ import re
4
+ from datetime import datetime
5
+
6
+ def generate_periphery_html(title, input_file, output_file):
7
+ try:
8
+ # 读取 Periphery 结果
9
+ if os.path.exists(input_file):
10
+ with open(input_file, 'r', encoding='utf-8') as f:
11
+ periphery_content = f.read()
12
+ else:
13
+ periphery_content = "无扫描结果文件"
14
+
15
+ # 用于存储所有的类型
16
+ all_types = set()
17
+
18
+ # 解析 Periphery 输出并生成表格行
19
+ rows_html = ""
20
+ lines = periphery_content.strip().split('\n') if periphery_content.strip() else []
21
+
22
+ for line in lines:
23
+ line = line.strip()
24
+ # 跳过空行和系统警告
25
+ if not line or line.startswith('*') or line.startswith('warning: When using') or line.startswith('warning: Declaration conflict'):
26
+ continue
27
+
28
+ # Periphery 输出格式: /path/to/file.swift:123:45: warning: Function 'getProducts()' is unused
29
+ # 匹配模式: 文件路径:行号:列号: warning: 描述
30
+ match = re.match(r"^(.*?):(\d+):(\d+):\s*warning:\s*(.+)$", line)
31
+ if match:
32
+ file_path = match.group(1)
33
+ line_number = match.group(2)
34
+ column_number = match.group(3)
35
+ description = match.group(4)
36
+
37
+ # 从描述中提取类型
38
+ issue_type = extract_issue_type(description)
39
+ all_types.add(issue_type)
40
+
41
+ # 生成表格行
42
+ rows_html += f"""
43
+ <tr>
44
+ <td class="file-name">{os.path.basename(file_path)}</td>
45
+ <td class="line-number">{line_number}</td>
46
+ <td class="description">{description}</td>
47
+ <td class="issue-type">{issue_type}</td>
48
+ </tr>"""
49
+ else:
50
+ # 如果格式不匹配,可能是其他格式的输出,尝试简单解析
51
+ if "warning:" in line:
52
+ # 简单的备用解析
53
+ parts = line.split("warning:")
54
+ if len(parts) >= 2:
55
+ description = parts[1].strip()
56
+ issue_type = extract_issue_type(description)
57
+ all_types.add(issue_type)
58
+
59
+ rows_html += f"""
60
+ <tr>
61
+ <td class="file-name">未知文件</td>
62
+ <td class="line-number">-</td>
63
+ <td class="description">{description}</td>
64
+ <td class="issue-type">{issue_type}</td>
65
+ </tr>"""
66
+
67
+ # 统计信息
68
+ total_issues = len([line for line in lines if line.strip() and not line.startswith('*') and not line.startswith('warning: When using') and not line.startswith('warning: Declaration conflict') and 'warning:' in line])
69
+
70
+ # 生成完整的 HTML
71
+ html_content = f"""<!DOCTYPE html>
72
+ <html lang="zh-CN">
73
+ <head>
74
+ <meta charset="UTF-8">
75
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
76
+ <title>{title}</title>
77
+ <style>
78
+ body {{
79
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
80
+ margin: 0;
81
+ padding: 20px;
82
+ background-color: #f5f5f5;
83
+ }}
84
+ .container {{
85
+ max-width: 1200px;
86
+ margin: 0 auto;
87
+ background: white;
88
+ border-radius: 8px;
89
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
90
+ overflow: hidden;
91
+ }}
92
+ .header {{
93
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
94
+ color: white;
95
+ padding: 30px;
96
+ text-align: center;
97
+ position: relative;
98
+ }}
99
+ .header h1 {{
100
+ margin: 0;
101
+ font-size: 2em;
102
+ font-weight: 300;
103
+ }}
104
+ .header-timestamp {{
105
+ position: absolute;
106
+ bottom: 15px;
107
+ right: 20px;
108
+ font-size: 0.8em;
109
+ opacity: 0.8;
110
+ }}
111
+ .stats {{
112
+ display: flex;
113
+ justify-content: center;
114
+ gap: 30px;
115
+ margin-top: 20px;
116
+ }}
117
+ .stat-item {{
118
+ text-align: center;
119
+ }}
120
+ .stat-number {{
121
+ font-size: 2em;
122
+ font-weight: bold;
123
+ margin-bottom: 5px;
124
+ }}
125
+ .stat-label {{
126
+ font-size: 0.9em;
127
+ opacity: 0.9;
128
+ }}
129
+ .content {{
130
+ padding: 30px;
131
+ }}
132
+ table {{
133
+ border-collapse: collapse;
134
+ width: 100%;
135
+ margin-top: 20px;
136
+ table-layout: fixed; /* 固定表格布局 */
137
+ }}
138
+ th, td {{
139
+ border: 1px solid #ddd;
140
+ padding: 12px;
141
+ text-align: left;
142
+ vertical-align: top;
143
+ overflow: hidden;
144
+ word-wrap: break-word;
145
+ }}
146
+ th {{
147
+ background-color: #f8f9fa;
148
+ cursor: pointer;
149
+ font-weight: 600;
150
+ color: #495057;
151
+ }}
152
+ /* 设置各列的宽度比例 */
153
+ .file-name, .file-name-header {{
154
+ width: 20%;
155
+ }}
156
+ .line-number, .line-number-header {{
157
+ width: 10%;
158
+ text-align: center;
159
+ }}
160
+ .description, .description-header {{
161
+ width: 50%;
162
+ }}
163
+ .issue-type, .issue-type-header {{
164
+ width: 20%;
165
+ text-align: center;
166
+ }}
167
+ .filter-select {{
168
+ background: white;
169
+ border: 1px solid #ced4da;
170
+ border-radius: 4px;
171
+ padding: 4px 8px;
172
+ font-size: 14px;
173
+ margin-left: 10px;
174
+ }}
175
+ .hidden {{
176
+ display: none;
177
+ }}
178
+ .no-issues {{
179
+ text-align: center;
180
+ padding: 50px;
181
+ color: #28a745;
182
+ font-size: 1.2em;
183
+ background-color: #d4edda;
184
+ border-radius: 8px;
185
+ border: 1px solid #c3e6cb;
186
+ }}
187
+ tr:nth-child(even) {{
188
+ background-color: #f8f9fa;
189
+ }}
190
+ tr:hover {{
191
+ background-color: #e9ecef;
192
+ }}
193
+ </style>
194
+ <script>
195
+ function filterByType(selectedType) {{
196
+ const rows = document.querySelectorAll("tbody tr");
197
+ rows.forEach(row => {{
198
+ const typeCell = row.querySelector("td:last-child");
199
+ if (selectedType === "All" || typeCell.textContent === selectedType) {{
200
+ row.classList.remove("hidden");
201
+ }} else {{
202
+ row.classList.add("hidden");
203
+ }}
204
+ }});
205
+
206
+ // 更新显示的问题数量
207
+ updateVisibleCount();
208
+ }}
209
+
210
+ function updateVisibleCount() {{
211
+ const visibleRows = document.querySelectorAll("tbody tr:not(.hidden)");
212
+ const countElement = document.getElementById("visible-count");
213
+ if (countElement) {{
214
+ countElement.textContent = visibleRows.length;
215
+ }}
216
+ }}
217
+ </script>
218
+ </head>
219
+ <body>
220
+ <div class="container">
221
+ <div class="header">
222
+ <h1>{title}</h1>
223
+ <div class="header-timestamp">
224
+ 报告生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
225
+ </div>
226
+ <div class="stats">
227
+ <div class="stat-item">
228
+ <div class="stat-number" id="visible-count">{total_issues}</div>
229
+ <div class="stat-label">发现的问题</div>
230
+ </div>
231
+ </div>
232
+ </div>
233
+ <div class="content">"""
234
+
235
+ if total_issues == 0:
236
+ html_content += """
237
+ <div class="no-issues">
238
+ 🎉 太棒了!没有发现无用代码
239
+ </div>"""
240
+ else:
241
+ # 添加表格头部和过滤器
242
+ html_content += """
243
+ <table>
244
+ <thead>
245
+ <tr>
246
+ <th class="file-name-header">文件</th>
247
+ <th class="line-number-header">行号</th>
248
+ <th class="description-header">描述</th>
249
+ <th class="issue-type-header">
250
+ 类型
251
+ <select class="filter-select" onchange="filterByType(this.value)">
252
+ <option value="All">全部</option>"""
253
+
254
+ # 添加所有类型到下拉框
255
+ for issue_type in sorted(all_types):
256
+ html_content += f'<option value="{issue_type}">{issue_type}</option>'
257
+
258
+ html_content += """
259
+ </select>
260
+ </th>
261
+ </tr>
262
+ </thead>
263
+ <tbody>"""
264
+
265
+ # 添加表格行
266
+ html_content += rows_html
267
+
268
+ html_content += """
269
+ </tbody>
270
+ </table>"""
271
+
272
+ html_content += """
273
+ </div>
274
+ </div>
275
+ </body>
276
+ </html>"""
277
+
278
+ os.makedirs(os.path.dirname(output_file), exist_ok=True)
279
+ # 写入 HTML 文件
280
+ with open(output_file, 'w', encoding='utf-8') as f:
281
+ f.write(html_content)
282
+
283
+ print(f"HTML 报告已生成: {output_file}")
284
+
285
+ except Exception as e:
286
+ print(f"生成 HTML 报告时出错: {e}")
287
+
288
+ def extract_issue_type(description):
289
+ """从描述中提取问题类型"""
290
+ description_lower = description.lower()
291
+
292
+ # 匹配各种类型的未使用代码
293
+ if "function" in description_lower and "unused" in description_lower:
294
+ return "unused function"
295
+ elif "parameter" in description_lower and "unused" in description_lower:
296
+ return "unused parameter"
297
+ elif "variable" in description_lower and "unused" in description_lower:
298
+ return "unused variable"
299
+ elif "class" in description_lower and "unused" in description_lower:
300
+ return "unused class"
301
+ elif "enum" in description_lower and "unused" in description_lower:
302
+ return "unused enum"
303
+ elif "protocol" in description_lower and "unused" in description_lower:
304
+ return "unused protocol"
305
+ elif "struct" in description_lower and "unused" in description_lower:
306
+ return "unused struct"
307
+ elif "property" in description_lower and "unused" in description_lower:
308
+ return "unused property"
309
+ elif "typealias" in description_lower and "unused" in description_lower:
310
+ return "unused typealias"
311
+ elif "import" in description_lower and "unused" in description_lower:
312
+ return "unused import"
313
+ elif "initializer" in description_lower and "unused" in description_lower:
314
+ return "unused initializer"
315
+ elif "extension" in description_lower and "unused" in description_lower:
316
+ return "unused extension"
317
+ else:
318
+ return "other"
319
+
320
+ if __name__ == "__main__":
321
+ if len(sys.argv) != 4:
322
+ sys.exit(1)
323
+
324
+ title = sys.argv[1]
325
+ input_file = sys.argv[2]
326
+ output_file = sys.argv[3]
327
+
328
+ generate_periphery_html(title, input_file, output_file)
@@ -0,0 +1,308 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import sys
5
+ import re
6
+ import os
7
+ from datetime import datetime
8
+
9
+ def generate_fengniao_html(title, input_file, output_file):
10
+ try:
11
+ # 读取 FengNiao 结果
12
+ if os.path.exists(input_file):
13
+ with open(input_file, 'r', encoding='utf-8') as f:
14
+ fengniao_content = f.read()
15
+ else:
16
+ fengniao_content = "无扫描结果文件"
17
+
18
+ # 解析 FengNiao 输出并生成表格行
19
+ rows_html = ""
20
+ unused_images = []
21
+ lines = fengniao_content.strip().split('\n') if fengniao_content.strip() else []
22
+
23
+ for line in lines:
24
+ line = line.strip()
25
+ # 跳过空行和系统信息
26
+ if not line or line.startswith('[') or line.startswith('Searching') or line.startswith('Found'):
27
+ continue
28
+
29
+ # FengNiao 输出格式: "226.56 KB /path/to/image.png"
30
+ # 使用正则表达式匹配:文件大小 + 空格 + 文件路径
31
+ match = re.match(r'^(\d+(?:\.\d+)?\s*[KMGT]?B)\s+(.+\.(png|jpg|jpeg|gif|pdf|svg))$', line, re.IGNORECASE)
32
+
33
+ if match:
34
+ file_size = match.group(1)
35
+ file_path = match.group(2)
36
+ file_name = os.path.basename(file_path)
37
+ file_ext = os.path.splitext(file_name)[1].lower()
38
+
39
+ unused_images.append({
40
+ 'name': file_name,
41
+ 'path': file_path,
42
+ 'size': file_size,
43
+ 'ext': file_ext
44
+ })
45
+
46
+ # 生成表格行
47
+ rows_html += f"""
48
+ <tr>
49
+ <td class="file-name">{file_name}</td>
50
+ <td class="file-path">{file_path}</td>
51
+ <td class="file-size">{file_size}</td>
52
+ <td class="file-type">{file_ext}</td>
53
+ </tr>"""
54
+ else:
55
+ # 备用解析:如果正则匹配失败,检查是否包含图片扩展名
56
+ if re.search(r'\.(png|jpg|jpeg|gif|pdf|svg)$', line, re.IGNORECASE):
57
+ # 可能是只有路径没有大小的格式
58
+ file_path = line
59
+ file_name = os.path.basename(file_path)
60
+ file_ext = os.path.splitext(file_name)[1].lower()
61
+ file_size = "未知"
62
+
63
+ unused_images.append({
64
+ 'name': file_name,
65
+ 'path': file_path,
66
+ 'size': file_size,
67
+ 'ext': file_ext
68
+ })
69
+
70
+ rows_html += f"""
71
+ <tr>
72
+ <td class="file-name">{file_name}</td>
73
+ <td class="file-path">{file_path}</td>
74
+ <td class="file-size">{file_size}</td>
75
+ <td class="file-type">{file_ext}</td>
76
+ </tr>"""
77
+
78
+ # 统计信息
79
+ total_images = len(unused_images)
80
+
81
+ # 计算总大小
82
+ total_size = calculate_total_size(unused_images)
83
+
84
+ # 生成完整的 HTML
85
+ html_content = f"""<!DOCTYPE html>
86
+ <html lang="zh-CN">
87
+ <head>
88
+ <meta charset="UTF-8">
89
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
90
+ <title>{title}</title>
91
+ <style>
92
+ body {{
93
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
94
+ margin: 0;
95
+ padding: 20px;
96
+ background-color: #f5f5f5;
97
+ }}
98
+ .container {{
99
+ max-width: 1200px;
100
+ margin: 0 auto;
101
+ background: white;
102
+ border-radius: 8px;
103
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
104
+ overflow: hidden;
105
+ }}
106
+ .header {{
107
+ background: linear-gradient(135deg, #ff6b6b 0%, #ee5a24 100%);
108
+ color: white;
109
+ padding: 30px;
110
+ text-align: center;
111
+ position: relative;
112
+ }}
113
+ .header h1 {{
114
+ margin: 0;
115
+ font-size: 2em;
116
+ font-weight: 300;
117
+ }}
118
+ .header-timestamp {{
119
+ position: absolute;
120
+ bottom: 15px;
121
+ right: 20px;
122
+ font-size: 0.8em;
123
+ opacity: 0.8;
124
+ }}
125
+ .stats {{
126
+ display: flex;
127
+ justify-content: center;
128
+ gap: 30px;
129
+ margin-top: 20px;
130
+ }}
131
+ .stat-item {{
132
+ text-align: center;
133
+ }}
134
+ .stat-number {{
135
+ font-size: 2em;
136
+ font-weight: bold;
137
+ margin-bottom: 5px;
138
+ }}
139
+ .stat-label {{
140
+ font-size: 0.9em;
141
+ opacity: 0.9;
142
+ }}
143
+ .content {{
144
+ padding: 30px;
145
+ }}
146
+ table {{
147
+ border-collapse: collapse;
148
+ width: 100%;
149
+ margin-top: 20px;
150
+ table-layout: fixed; /* 固定表格布局 */
151
+ }}
152
+ th, td {{
153
+ border: 1px solid #ddd;
154
+ padding: 12px;
155
+ text-align: left;
156
+ vertical-align: top;
157
+ overflow: hidden; /* 隐藏溢出内容 */
158
+ }}
159
+ th {{
160
+ background-color: #f8f9fa;
161
+ font-weight: 600;
162
+ color: #495057;
163
+ }}
164
+ /* 设置各列的宽度 */
165
+ .file-name-header, .file-name {{
166
+ width: 25%;
167
+ }}
168
+ .file-path-header, .file-path {{
169
+ width: 45%;
170
+ word-break: break-all; /* 长路径自动换行 */
171
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
172
+ font-size: 0.85em;
173
+ color: #6c757d;
174
+ }}
175
+ .file-size-header, .file-size {{
176
+ width: 15%;
177
+ text-align: center;
178
+ }}
179
+ .file-type-header, .file-type {{
180
+ width: 15%;
181
+ text-align: center;
182
+ }}
183
+ .no-issues {{
184
+ text-align: center;
185
+ padding: 50px;
186
+ color: #28a745;
187
+ font-size: 1.2em;
188
+ background-color: #d4edda;
189
+ border-radius: 8px;
190
+ border: 1px solid #c3e6cb;
191
+ }}
192
+ tr:nth-child(even) {{
193
+ background-color: #f8f9fa;
194
+ }}
195
+ tr:hover {{
196
+ background-color: #e9ecef;
197
+ }}
198
+ </style>
199
+ </head>
200
+ <body>
201
+ <div class="container">
202
+ <div class="header">
203
+ <h1>{title}</h1>
204
+ <div class="header-timestamp">
205
+ 报告生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
206
+ </div>
207
+ <div class="stats">
208
+ <div class="stat-item">
209
+ <div class="stat-number">{total_images}</div>
210
+ <div class="stat-label">无用图片数量</div>
211
+ </div>
212
+ <div class="stat-item">
213
+ <div class="stat-number">{total_size}</div>
214
+ <div class="stat-label">总大小</div>
215
+ </div>
216
+ </div>
217
+ </div>
218
+ <div class="content">"""
219
+
220
+ if total_images == 0:
221
+ html_content += """
222
+ <div class="no-issues">
223
+ 🎉 太棒了!没有发现无用的图片文件
224
+ </div>"""
225
+ else:
226
+ # 添加表格
227
+ html_content += """
228
+ <table>
229
+ <thead>
230
+ <tr>
231
+ <th class="file-name-header">文件名</th>
232
+ <th class="file-path-header">文件路径</th>
233
+ <th class="file-size-header">文件大小</th>
234
+ <th class="file-type-header">类型</th>
235
+ </tr>
236
+ </thead>
237
+ <tbody>"""
238
+
239
+ # 添加表格行
240
+ html_content += rows_html
241
+
242
+ html_content += """
243
+ </tbody>
244
+ </table>"""
245
+
246
+ html_content += """
247
+ </div>
248
+ </div>
249
+ </body>
250
+ </html>"""
251
+
252
+ os.makedirs(os.path.dirname(output_file), exist_ok=True)
253
+ # 写入 HTML 文件
254
+ with open(output_file, 'w', encoding='utf-8') as f:
255
+ f.write(html_content)
256
+
257
+ print(f"HTML 报告已生成: {output_file}")
258
+
259
+ except Exception as e:
260
+ print(f"生成 HTML 报告时出错: {e}")
261
+
262
+ def calculate_total_size(unused_images):
263
+ """计算总文件大小"""
264
+ total_bytes = 0
265
+
266
+ for img in unused_images:
267
+ size_str = img['size']
268
+ if size_str == "未知":
269
+ continue
270
+
271
+ # 解析大小字符串,如 "226.56 KB"
272
+ match = re.match(r'(\d+(?:\.\d+)?)\s*([KMGT]?B)', size_str, re.IGNORECASE)
273
+ if match:
274
+ size_value = float(match.group(1))
275
+ size_unit = match.group(2).upper()
276
+
277
+ # 转换为字节
278
+ if size_unit == "B":
279
+ total_bytes += size_value
280
+ elif size_unit == "KB":
281
+ total_bytes += size_value * 1024
282
+ elif size_unit == "MB":
283
+ total_bytes += size_value * 1024 * 1024
284
+ elif size_unit == "GB":
285
+ total_bytes += size_value * 1024 * 1024 * 1024
286
+ elif size_unit == "TB":
287
+ total_bytes += size_value * 1024 * 1024 * 1024 * 1024
288
+
289
+ # 转换回易读格式
290
+ if total_bytes < 1024:
291
+ return f"{total_bytes:.1f} B"
292
+ elif total_bytes < 1024 * 1024:
293
+ return f"{total_bytes / 1024:.1f} KB"
294
+ elif total_bytes < 1024 * 1024 * 1024:
295
+ return f"{total_bytes / (1024 * 1024):.1f} MB"
296
+ else:
297
+ return f"{total_bytes / (1024 * 1024 * 1024):.1f} GB"
298
+
299
+ if __name__ == "__main__":
300
+ if len(sys.argv) != 4:
301
+ print("使用方法: python3 generate_fengniao_html.py <title> <input_file> <output_file>")
302
+ sys.exit(1)
303
+
304
+ title = sys.argv[1]
305
+ input_file = sys.argv[2]
306
+ output_file = sys.argv[3]
307
+
308
+ generate_fengniao_html(title, input_file, output_file)