sdd-forge 0.1.0-alpha.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.
- package/LICENSE +21 -0
- package/README.md +45 -0
- package/package.json +23 -0
- package/src/analyzers/analyze-controllers.js +85 -0
- package/src/analyzers/analyze-extras.js +944 -0
- package/src/analyzers/analyze-models.js +130 -0
- package/src/analyzers/analyze-routes.js +50 -0
- package/src/analyzers/analyze-shells.js +75 -0
- package/src/analyzers/lib/php-array-parser.js +138 -0
- package/src/analyzers/scan.js +116 -0
- package/src/bin/sdd-forge.js +117 -0
- package/src/engine/directive-parser.js +72 -0
- package/src/engine/init.js +253 -0
- package/src/engine/populate.js +192 -0
- package/src/engine/readme.js +174 -0
- package/src/engine/renderers.js +150 -0
- package/src/engine/resolver.js +568 -0
- package/src/engine/tfill.js +617 -0
- package/src/flow/flow.js +183 -0
- package/src/forge/forge.js +684 -0
- package/src/help.js +55 -0
- package/src/lib/cli.js +83 -0
- package/src/lib/config.js +40 -0
- package/src/lib/process.js +32 -0
- package/src/projects/add.js +73 -0
- package/src/projects/projects.js +91 -0
- package/src/projects/setdefault.js +37 -0
- package/src/spec/gate.js +101 -0
- package/src/spec/spec.js +198 -0
- package/src/templates/checks/check-controller-coverage.sh +46 -0
- package/src/templates/checks/check-db-coverage.sh +87 -0
- package/src/templates/checks/check-node-cli-docs.sh +125 -0
- package/src/templates/checks/check-temp-docs.sh +100 -0
- package/src/templates/checks/generate-change-log.sh +142 -0
- package/src/templates/checks/self-review-temp-docs.sh +18 -0
- package/src/templates/locale/ja/messages.json +9 -0
- package/src/templates/locale/ja/node-cli/01_overview.md +23 -0
- package/src/templates/locale/ja/node-cli/02_cli_commands.md +23 -0
- package/src/templates/locale/ja/node-cli/03_configuration.md +23 -0
- package/src/templates/locale/ja/node-cli/04_internal_design.md +30 -0
- package/src/templates/locale/ja/node-cli/05_development.md +49 -0
- package/src/templates/locale/ja/node-cli/README.md +41 -0
- package/src/templates/locale/ja/php-mvc/01_architecture.md +23 -0
- package/src/templates/locale/ja/php-mvc/02_stack_and_ops.md +45 -0
- package/src/templates/locale/ja/php-mvc/03_project_structure.md +46 -0
- package/src/templates/locale/ja/php-mvc/04_development.md +69 -0
- package/src/templates/locale/ja/php-mvc/05_auth_and_session.md +48 -0
- package/src/templates/locale/ja/php-mvc/06_database_architecture.md +23 -0
- package/src/templates/locale/ja/php-mvc/07_db_tables.md +31 -0
- package/src/templates/locale/ja/php-mvc/08_controller_routes.md +33 -0
- package/src/templates/locale/ja/php-mvc/09_business_logic.md +27 -0
- package/src/templates/locale/ja/php-mvc/10_batch_and_shell.md +25 -0
- package/src/templates/locale/ja/php-mvc/README.md +34 -0
- package/src/templates/locale/ja/prompts.json +4 -0
- package/src/templates/locale/ja/sections.json +6 -0
- package/src/templates/review-checklist.md +48 -0
|
@@ -0,0 +1,944 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* tools/analyzers/analyze-extras.js
|
|
4
|
+
*
|
|
5
|
+
* 既存 4 解析器(controllers, models, shells, routes)で未カバーの領域を一括解析する。
|
|
6
|
+
* - const.php 設定定数
|
|
7
|
+
* - bootstrap.php 初期化処理
|
|
8
|
+
* - AppController 共通処理
|
|
9
|
+
* - AppModel 共通処理
|
|
10
|
+
* - View ヘルパー
|
|
11
|
+
* - 共通ライブラリ (Lib/)
|
|
12
|
+
* - ビヘイビア
|
|
13
|
+
* - SQL テンプレート
|
|
14
|
+
* - レイアウト・エレメント
|
|
15
|
+
* - フロントエンドアセット (JS/CSS)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import fs from "fs";
|
|
19
|
+
import path from "path";
|
|
20
|
+
import { stripBlockComments } from "./lib/php-array-parser.js";
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// 定数解析: app/Config/const.php
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
function analyzeConstants(appDir) {
|
|
26
|
+
const filePath = path.join(appDir, "Config", "const.php");
|
|
27
|
+
if (!fs.existsSync(filePath)) return { scalars: [], selectOptions: [] };
|
|
28
|
+
|
|
29
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
30
|
+
const scalars = [];
|
|
31
|
+
const selectOptions = [];
|
|
32
|
+
|
|
33
|
+
// 全 $config 行を走査し、array(...) を含むかで分岐
|
|
34
|
+
const allRe = /\$config\s*\[?\s*["']([^"']+)["']\s*\]?\s*=\s*/g;
|
|
35
|
+
let m;
|
|
36
|
+
while ((m = allRe.exec(raw)) !== null) {
|
|
37
|
+
const name = m[1];
|
|
38
|
+
const rest = raw.slice(m.index + m[0].length);
|
|
39
|
+
|
|
40
|
+
if (/^\s*array\s*\(/.test(rest)) {
|
|
41
|
+
// 配列定数 → 括弧バランスで中身を抽出
|
|
42
|
+
const openIdx = rest.indexOf("(");
|
|
43
|
+
let depth = 1;
|
|
44
|
+
let i = openIdx + 1;
|
|
45
|
+
while (i < rest.length && depth > 0) {
|
|
46
|
+
if (rest[i] === "(") depth++;
|
|
47
|
+
else if (rest[i] === ")") depth--;
|
|
48
|
+
i++;
|
|
49
|
+
}
|
|
50
|
+
if (depth === 0) {
|
|
51
|
+
const body = rest.slice(openIdx + 1, i - 1);
|
|
52
|
+
const options = [];
|
|
53
|
+
const optRe = /["']([^"']+)["']\s*=>\s*["']([^"']+)["']/g;
|
|
54
|
+
let om;
|
|
55
|
+
while ((om = optRe.exec(body)) !== null) {
|
|
56
|
+
options.push({ key: om[1], label: om[2] });
|
|
57
|
+
}
|
|
58
|
+
selectOptions.push({ name, options });
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
// スカラー定数
|
|
62
|
+
const valMatch = rest.match(/^(.+?)\s*;/);
|
|
63
|
+
if (valMatch) {
|
|
64
|
+
let value = valMatch[1].trim();
|
|
65
|
+
value = value.replace(/^["']|["']$/g, "");
|
|
66
|
+
scalars.push({ name, value });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { scalars, selectOptions };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Bootstrap 解析: app/Config/bootstrap.php
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
function analyzeBootstrap(appDir) {
|
|
78
|
+
const filePath = path.join(appDir, "Config", "bootstrap.php");
|
|
79
|
+
if (!fs.existsSync(filePath)) return {};
|
|
80
|
+
|
|
81
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
82
|
+
const src = stripBlockComments(raw);
|
|
83
|
+
// 行コメントも除去(コメント内の CakePlugin::load 等を誤検出しないため)
|
|
84
|
+
const active = src.replace(/(?:^|\n)\s*(?:\/\/|#).*$/gm, "");
|
|
85
|
+
const result = {
|
|
86
|
+
siteTitle: "",
|
|
87
|
+
environments: [],
|
|
88
|
+
plugins: [],
|
|
89
|
+
logChannels: [],
|
|
90
|
+
classPaths: [],
|
|
91
|
+
configureWrites: [],
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// サイトタイトル
|
|
95
|
+
const titleMatch = active.match(/Configure::write\s*\(\s*['"]SITE_TITLE['"]\s*,\s*['"]([^'"]+)['"]\s*\)/);
|
|
96
|
+
if (titleMatch) result.siteTitle = titleMatch[1];
|
|
97
|
+
|
|
98
|
+
// 環境判定
|
|
99
|
+
const envRe = /CAKE_ENV['"]\s*,\s*['"](\w+)['"]/g;
|
|
100
|
+
const envSet = new Set();
|
|
101
|
+
let em;
|
|
102
|
+
while ((em = envRe.exec(active)) !== null) {
|
|
103
|
+
envSet.add(em[1]);
|
|
104
|
+
}
|
|
105
|
+
result.environments = [...envSet].sort();
|
|
106
|
+
|
|
107
|
+
// プラグイン
|
|
108
|
+
const pluginRe = /CakePlugin::load\s*\(\s*['"](\w+)['"]/g;
|
|
109
|
+
while ((em = pluginRe.exec(active)) !== null) {
|
|
110
|
+
result.plugins.push(em[1]);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ログチャネル
|
|
114
|
+
const logRe = /CakeLog::config\s*\(\s*['"]([^'"]+)['"]/g;
|
|
115
|
+
while ((em = logRe.exec(active)) !== null) {
|
|
116
|
+
result.logChannels.push(em[1]);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// App::build クラスパス
|
|
120
|
+
const buildRe = /['"](\w+)['"]\s*=>\s*array\s*\(\s*APP\s*\.\s*['"]([^'"]+)['"]/g;
|
|
121
|
+
while ((em = buildRe.exec(active)) !== null) {
|
|
122
|
+
result.classPaths.push({ type: em[1], path: em[2] });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Configure::write 一覧
|
|
126
|
+
const cwRe = /Configure\s*::\s*write\s*\(\s*['"]([^'"]+)["']\s*,\s*(.+?)\s*\)/g;
|
|
127
|
+
while ((em = cwRe.exec(active)) !== null) {
|
|
128
|
+
const key = em[1];
|
|
129
|
+
let value = em[2].trim();
|
|
130
|
+
// 配列値は省略表現にする
|
|
131
|
+
if (value.startsWith("array")) {
|
|
132
|
+
value = "array(...)";
|
|
133
|
+
}
|
|
134
|
+
// クォートを除去
|
|
135
|
+
value = value.replace(/^["']|["']$/g, "");
|
|
136
|
+
result.configureWrites.push({ key, value });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// AppController 解析
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
function analyzeAppController(appDir) {
|
|
146
|
+
const filePath = path.join(appDir, "Controller", "AppController.php");
|
|
147
|
+
if (!fs.existsSync(filePath)) return {};
|
|
148
|
+
|
|
149
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
150
|
+
const src = stripBlockComments(raw);
|
|
151
|
+
|
|
152
|
+
const result = {
|
|
153
|
+
components: [],
|
|
154
|
+
helpers: [],
|
|
155
|
+
authConfig: {},
|
|
156
|
+
methods: [],
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// components 配列から Session, Auth, Acl をトップレベルのみ抽出
|
|
160
|
+
// 括弧バランスで第一階層だけ読み取る
|
|
161
|
+
const compSection = src.match(/\$components\s*=\s*array\s*\(/);
|
|
162
|
+
if (compSection) {
|
|
163
|
+
const startIdx = compSection.index + compSection[0].length;
|
|
164
|
+
let depth = 1;
|
|
165
|
+
let i = startIdx;
|
|
166
|
+
while (i < src.length && depth > 0) {
|
|
167
|
+
if (src[i] === "(") depth++;
|
|
168
|
+
else if (src[i] === ")") depth--;
|
|
169
|
+
i++;
|
|
170
|
+
}
|
|
171
|
+
const compBody = src.slice(startIdx, i - 1);
|
|
172
|
+
|
|
173
|
+
// トップレベルの要素を分割(depth=0 のカンマで区切る)
|
|
174
|
+
let d = 0;
|
|
175
|
+
let last = 0;
|
|
176
|
+
const segments = [];
|
|
177
|
+
for (let j = 0; j < compBody.length; j++) {
|
|
178
|
+
if (compBody[j] === "(" || compBody[j] === "[") d++;
|
|
179
|
+
else if (compBody[j] === ")" || compBody[j] === "]") d--;
|
|
180
|
+
else if (compBody[j] === "," && d === 0) {
|
|
181
|
+
segments.push(compBody.slice(last, j).trim());
|
|
182
|
+
last = j + 1;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
segments.push(compBody.slice(last).trim());
|
|
186
|
+
|
|
187
|
+
for (const seg of segments) {
|
|
188
|
+
if (!seg) continue;
|
|
189
|
+
// 'Session' or 'Auth' => array(...) or 'Acl'
|
|
190
|
+
const nameMatch = seg.match(/^['"](\w+)['"]/);
|
|
191
|
+
if (nameMatch) {
|
|
192
|
+
result.components.push(nameMatch[1]);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// helpers
|
|
198
|
+
const helperMatch = src.match(/\$helpers\s*=\s*array\s*\(([^)]+)\)/);
|
|
199
|
+
if (helperMatch) {
|
|
200
|
+
const body = helperMatch[1];
|
|
201
|
+
// 'Html' => array('className' => 'MyHtml') パターン
|
|
202
|
+
const helperRe = /['"](\w+)['"]\s*=>\s*array\s*\(\s*['"]className['"]\s*=>\s*['"](\w+)['"]/g;
|
|
203
|
+
let hm;
|
|
204
|
+
while ((hm = helperRe.exec(body)) !== null) {
|
|
205
|
+
result.helpers.push({ name: hm[1], className: hm[2] });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Auth 設定を抽出
|
|
210
|
+
const authSection = src.match(/'Auth'\s*=>\s*array\s*\(([\s\S]*?)\),\s*'Acl'/);
|
|
211
|
+
if (authSection) {
|
|
212
|
+
const authBody = authSection[1];
|
|
213
|
+
// authorize
|
|
214
|
+
const authorizeMatch = authBody.match(/['"]authorize['"]\s*=>\s*array\s*\(\s*['"](\w+)['"]/);
|
|
215
|
+
if (authorizeMatch) result.authConfig.authorize = authorizeMatch[1];
|
|
216
|
+
// authenticate
|
|
217
|
+
const authMatch = authBody.match(/['"]authenticate['"]\s*=>\s*array\s*\(\s*['"](\w+)['"]/);
|
|
218
|
+
if (authMatch) result.authConfig.authenticate = authMatch[1];
|
|
219
|
+
// userModel
|
|
220
|
+
const userModelMatch = authBody.match(/['"]userModel['"]\s*=>\s*['"](\w+)['"]/);
|
|
221
|
+
if (userModelMatch) result.authConfig.userModel = userModelMatch[1];
|
|
222
|
+
// username field
|
|
223
|
+
const fieldMatch = authBody.match(/['"]username['"]\s*=>\s*['"](\w+)['"]/);
|
|
224
|
+
if (fieldMatch) result.authConfig.loginField = fieldMatch[1];
|
|
225
|
+
// loginRedirect
|
|
226
|
+
const loginRedirMatch = authBody.match(/['"]loginRedirect['"]\s*=>\s*array\s*\(\s*['"]controller['"]\s*=>\s*['"](\w+)['"]/);
|
|
227
|
+
if (loginRedirMatch) result.authConfig.loginRedirect = loginRedirMatch[1] + "/index";
|
|
228
|
+
// logoutRedirect
|
|
229
|
+
const logoutRedirMatch = authBody.match(/['"]logoutRedirect['"]\s*=>\s*array\s*\(\s*['"]controller['"]\s*=>\s*['"](\w+)['"][^)]*['"]action['"]\s*=>\s*['"](\w+)['"]/);
|
|
230
|
+
if (logoutRedirMatch) result.authConfig.logoutRedirect = logoutRedirMatch[1] + "/" + logoutRedirMatch[2];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// メソッド一覧
|
|
234
|
+
const fnRe = /(public|protected|private)\s+function\s+(\w+)\s*\(/g;
|
|
235
|
+
let fm;
|
|
236
|
+
while ((fm = fnRe.exec(src)) !== null) {
|
|
237
|
+
result.methods.push({ name: fm[2], visibility: fm[1] });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
// AppModel 解析
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
function analyzeAppModel(appDir) {
|
|
247
|
+
const filePath = path.join(appDir, "Model", "AppModel.php");
|
|
248
|
+
if (!fs.existsSync(filePath)) return {};
|
|
249
|
+
|
|
250
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
251
|
+
const src = stripBlockComments(raw);
|
|
252
|
+
|
|
253
|
+
const result = {
|
|
254
|
+
behaviors: [],
|
|
255
|
+
callbacks: [],
|
|
256
|
+
auditFields: [],
|
|
257
|
+
methods: [],
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// actsAs
|
|
261
|
+
const actsAsMatch = src.match(/\$actsAs\s*=\s*array\s*\(\s*["'](\w+)["']/);
|
|
262
|
+
if (actsAsMatch) result.behaviors.push(actsAsMatch[1]);
|
|
263
|
+
|
|
264
|
+
// コールバック
|
|
265
|
+
if (/function\s+beforeSave\s*\(/.test(src)) result.callbacks.push("beforeSave");
|
|
266
|
+
if (/function\s+afterSave\s*\(/.test(src)) result.callbacks.push("afterSave");
|
|
267
|
+
|
|
268
|
+
// 監査フィールド
|
|
269
|
+
const auditFields = ["created_by", "created_ts", "updated_by", "updated_ts"];
|
|
270
|
+
for (const field of auditFields) {
|
|
271
|
+
if (src.includes(`'${field}'`)) result.auditFields.push(field);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// メソッド一覧
|
|
275
|
+
const fnRe = /(public\s+)?function\s+(\w+)\s*\(/g;
|
|
276
|
+
let fm;
|
|
277
|
+
const methodDescs = {
|
|
278
|
+
picureWithSize: "画像横幅バリデーション(パイプ区切り)",
|
|
279
|
+
beforeSave: "自動タイムスタンプ・監査フィールド設定",
|
|
280
|
+
afterSave: "進捗管理更新・FEデータ削除フラグ処理",
|
|
281
|
+
invalidDate: "日付バリデーション",
|
|
282
|
+
sqldump: "SQL デバッグダンプ",
|
|
283
|
+
sql: "SQL テンプレートファイル読み込み・実行",
|
|
284
|
+
escapeQuote: "シングルクォートエスケープ",
|
|
285
|
+
replaseParam: "SQL パラメータ置換",
|
|
286
|
+
updateProcessUpdate: "コンテンツ・タイトル最終更新日時の更新",
|
|
287
|
+
saveAllAtOnce: "500件単位バッチ INSERT",
|
|
288
|
+
bulkInsert: "INSERT ON DUPLICATE KEY UPDATE",
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
while ((fm = fnRe.exec(src)) !== null) {
|
|
292
|
+
const name = fm[2];
|
|
293
|
+
if (name === "__construct") continue;
|
|
294
|
+
result.methods.push({
|
|
295
|
+
name,
|
|
296
|
+
description: methodDescs[name] || name,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
// ヘルパー解析: app/View/Helper/*.php
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
function analyzeHelpers(appDir) {
|
|
307
|
+
const helperDir = path.join(appDir, "View", "Helper");
|
|
308
|
+
if (!fs.existsSync(helperDir)) return [];
|
|
309
|
+
|
|
310
|
+
const files = fs.readdirSync(helperDir).filter((f) => f.endsWith(".php"));
|
|
311
|
+
const helpers = [];
|
|
312
|
+
|
|
313
|
+
for (const file of files) {
|
|
314
|
+
const filePath = path.join(helperDir, file);
|
|
315
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
316
|
+
const src = stripBlockComments(raw);
|
|
317
|
+
|
|
318
|
+
const classMatch = src.match(/class\s+(\w+)\s+extends\s+(\w+)/);
|
|
319
|
+
if (!classMatch) continue;
|
|
320
|
+
|
|
321
|
+
const className = classMatch[1];
|
|
322
|
+
const extendsClass = classMatch[2];
|
|
323
|
+
|
|
324
|
+
// 公開メソッド
|
|
325
|
+
const methods = [];
|
|
326
|
+
const fnRe = /(?:public\s+)?function\s+(\w+)\s*\(/g;
|
|
327
|
+
let fm;
|
|
328
|
+
while ((fm = fnRe.exec(src)) !== null) {
|
|
329
|
+
const name = fm[1];
|
|
330
|
+
if (!name.startsWith("__")) methods.push(name);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// 依存ヘルパー
|
|
334
|
+
const depHelpers = [];
|
|
335
|
+
const depMatch = src.match(/\$helpers\s*=\s*array\s*\(([^)]+)\)/);
|
|
336
|
+
if (depMatch) {
|
|
337
|
+
const depRe = /['"](\w+)['"]/g;
|
|
338
|
+
let dm;
|
|
339
|
+
while ((dm = depRe.exec(depMatch[1])) !== null) {
|
|
340
|
+
depHelpers.push(dm[1]);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
helpers.push({
|
|
345
|
+
className,
|
|
346
|
+
extends: extendsClass,
|
|
347
|
+
file: "app/View/Helper/" + file,
|
|
348
|
+
methods,
|
|
349
|
+
dependsOn: depHelpers,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return helpers;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
357
|
+
// ライブラリ解析: app/Lib/*.php
|
|
358
|
+
// ---------------------------------------------------------------------------
|
|
359
|
+
function analyzeLibraries(appDir) {
|
|
360
|
+
const libDir = path.join(appDir, "Lib");
|
|
361
|
+
if (!fs.existsSync(libDir)) return [];
|
|
362
|
+
|
|
363
|
+
const files = fs.readdirSync(libDir).filter((f) => f.endsWith(".php"));
|
|
364
|
+
const libraries = [];
|
|
365
|
+
|
|
366
|
+
for (const file of files) {
|
|
367
|
+
const filePath = path.join(libDir, file);
|
|
368
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
369
|
+
const src = stripBlockComments(raw);
|
|
370
|
+
|
|
371
|
+
const classMatch = src.match(/class\s+(\w+)/);
|
|
372
|
+
if (!classMatch) continue;
|
|
373
|
+
|
|
374
|
+
const className = classMatch[1];
|
|
375
|
+
|
|
376
|
+
// static メソッド
|
|
377
|
+
const staticMethods = [];
|
|
378
|
+
const fnRe = /(?:public\s+)?static\s+function\s+(\w+)\s*\(/g;
|
|
379
|
+
let fm;
|
|
380
|
+
while ((fm = fnRe.exec(src)) !== null) {
|
|
381
|
+
staticMethods.push(fm[1]);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// 通常メソッド
|
|
385
|
+
const methods = [];
|
|
386
|
+
const nfnRe = /(?:public\s+)?function\s+(\w+)\s*\(/g;
|
|
387
|
+
while ((fm = nfnRe.exec(src)) !== null) {
|
|
388
|
+
if (!fm[0].includes("static")) methods.push(fm[1]);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// メール送信有無
|
|
392
|
+
const hasMail = /CakeEmail/.test(raw);
|
|
393
|
+
|
|
394
|
+
libraries.push({
|
|
395
|
+
className,
|
|
396
|
+
file: "app/Lib/" + file,
|
|
397
|
+
staticMethods,
|
|
398
|
+
methods,
|
|
399
|
+
hasMail,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return libraries;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ---------------------------------------------------------------------------
|
|
407
|
+
// ビヘイビア解析: app/Model/Behavior/*.php
|
|
408
|
+
// ---------------------------------------------------------------------------
|
|
409
|
+
function analyzeBehaviors(appDir) {
|
|
410
|
+
const behaviorDir = path.join(appDir, "Model", "Behavior");
|
|
411
|
+
if (!fs.existsSync(behaviorDir)) return [];
|
|
412
|
+
|
|
413
|
+
const files = fs.readdirSync(behaviorDir).filter((f) => f.endsWith(".php"));
|
|
414
|
+
const behaviors = [];
|
|
415
|
+
|
|
416
|
+
for (const file of files) {
|
|
417
|
+
const filePath = path.join(behaviorDir, file);
|
|
418
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
419
|
+
const src = stripBlockComments(raw);
|
|
420
|
+
|
|
421
|
+
const classMatch = src.match(/class\s+(\w+)\s+extends\s+(\w+)/);
|
|
422
|
+
if (!classMatch) continue;
|
|
423
|
+
|
|
424
|
+
const className = classMatch[1];
|
|
425
|
+
|
|
426
|
+
const methods = [];
|
|
427
|
+
const fnRe = /(?:public\s+)?function\s+(\w+)\s*\(/g;
|
|
428
|
+
let fm;
|
|
429
|
+
while ((fm = fnRe.exec(src)) !== null) {
|
|
430
|
+
const name = fm[1];
|
|
431
|
+
if (!name.startsWith("__") && name !== "setup") methods.push(name);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
behaviors.push({
|
|
435
|
+
className,
|
|
436
|
+
file: "app/Model/Behavior/" + file,
|
|
437
|
+
methods,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return behaviors;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ---------------------------------------------------------------------------
|
|
445
|
+
// SQL テンプレート解析: app/Model/Sql/*.sql
|
|
446
|
+
// ---------------------------------------------------------------------------
|
|
447
|
+
function analyzeSqlFiles(appDir) {
|
|
448
|
+
const sqlDir = path.join(appDir, "Model", "Sql");
|
|
449
|
+
if (!fs.existsSync(sqlDir)) return [];
|
|
450
|
+
|
|
451
|
+
const files = fs.readdirSync(sqlDir).filter((f) => f.endsWith(".sql"));
|
|
452
|
+
const sqlFiles = [];
|
|
453
|
+
|
|
454
|
+
for (const file of files) {
|
|
455
|
+
const filePath = path.join(sqlDir, file);
|
|
456
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
457
|
+
const lines = content.split("\n").length;
|
|
458
|
+
|
|
459
|
+
// パラメータプレースホルダー: /*param_name*/ パターン
|
|
460
|
+
const params = new Set();
|
|
461
|
+
const paramRe = /\/\*(\w+)\*\//g;
|
|
462
|
+
let pm;
|
|
463
|
+
while ((pm = paramRe.exec(content)) !== null) {
|
|
464
|
+
params.add(pm[1]);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// 参照テーブル: FROM / JOIN 句
|
|
468
|
+
const tables = new Set();
|
|
469
|
+
const tableRe = /(?:FROM|JOIN)\s+(\w+)/gi;
|
|
470
|
+
let tm;
|
|
471
|
+
while ((tm = tableRe.exec(content)) !== null) {
|
|
472
|
+
const tbl = tm[1].toLowerCase();
|
|
473
|
+
if (tbl !== "select" && tbl !== "where" && tbl !== "set") {
|
|
474
|
+
tables.add(tbl);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
sqlFiles.push({
|
|
479
|
+
file,
|
|
480
|
+
lines,
|
|
481
|
+
params: [...params],
|
|
482
|
+
tables: [...tables].sort(),
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
sqlFiles.sort((a, b) => a.file.localeCompare(b.file));
|
|
487
|
+
return sqlFiles;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// ---------------------------------------------------------------------------
|
|
491
|
+
// レイアウト・エレメント解析
|
|
492
|
+
// ---------------------------------------------------------------------------
|
|
493
|
+
function analyzeLayouts(appDir) {
|
|
494
|
+
const layoutDir = path.join(appDir, "View", "Layouts");
|
|
495
|
+
if (!fs.existsSync(layoutDir)) return [];
|
|
496
|
+
|
|
497
|
+
const layouts = [];
|
|
498
|
+
function walk(dir, prefix) {
|
|
499
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
500
|
+
for (const entry of entries) {
|
|
501
|
+
if (entry.isDirectory()) {
|
|
502
|
+
walk(path.join(dir, entry.name), prefix ? prefix + "/" + entry.name : entry.name);
|
|
503
|
+
} else if (entry.name.endsWith(".ctp")) {
|
|
504
|
+
layouts.push(prefix ? prefix + "/" + entry.name : entry.name);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
walk(layoutDir, "");
|
|
509
|
+
return layouts.sort();
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function analyzeElements(appDir) {
|
|
513
|
+
const elemDir = path.join(appDir, "View", "Elements");
|
|
514
|
+
if (!fs.existsSync(elemDir)) return [];
|
|
515
|
+
|
|
516
|
+
return fs
|
|
517
|
+
.readdirSync(elemDir)
|
|
518
|
+
.filter((f) => f.endsWith(".ctp"))
|
|
519
|
+
.sort();
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// ---------------------------------------------------------------------------
|
|
523
|
+
// フロントエンドアセット解析
|
|
524
|
+
// ---------------------------------------------------------------------------
|
|
525
|
+
const JS_LIBRARY_PATTERNS = [
|
|
526
|
+
{ pattern: /jquery-(\d+\.\d+\.\d+)/i, library: "jQuery" },
|
|
527
|
+
{ pattern: /jquery[-.]ui/i, library: "jQuery UI" },
|
|
528
|
+
{ pattern: /jquery[-.]cookie/i, library: "jQuery Cookie" },
|
|
529
|
+
{ pattern: /jquery[-.]datePicker/i, library: "jQuery DatePicker" },
|
|
530
|
+
{ pattern: /jquery[-.]datetimePicker/i, library: "jQuery DateTimePicker" },
|
|
531
|
+
{ pattern: /jquery[-.]fancybox/i, library: "FancyBox" },
|
|
532
|
+
{ pattern: /jquery[-.]tablefix/i, library: "jQuery TableFix" },
|
|
533
|
+
{ pattern: /highcharts/i, library: "Highcharts" },
|
|
534
|
+
];
|
|
535
|
+
|
|
536
|
+
function analyzeAssets(appDir) {
|
|
537
|
+
const jsDir = path.join(appDir, "webroot", "js");
|
|
538
|
+
const cssDir = path.join(appDir, "webroot", "css");
|
|
539
|
+
const result = { js: [], css: [] };
|
|
540
|
+
|
|
541
|
+
if (fs.existsSync(jsDir)) {
|
|
542
|
+
const jsFiles = fs.readdirSync(jsDir).filter((f) => f.endsWith(".js"));
|
|
543
|
+
for (const file of jsFiles) {
|
|
544
|
+
const filePath = path.join(jsDir, file);
|
|
545
|
+
const stat = fs.statSync(filePath);
|
|
546
|
+
const entry = { file, size: stat.size };
|
|
547
|
+
|
|
548
|
+
// ライブラリ検出
|
|
549
|
+
for (const { pattern, library } of JS_LIBRARY_PATTERNS) {
|
|
550
|
+
const m = file.match(pattern);
|
|
551
|
+
if (m) {
|
|
552
|
+
entry.library = library;
|
|
553
|
+
if (m[1]) entry.version = m[1];
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (!entry.library) {
|
|
559
|
+
entry.type = "custom";
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
result.js.push(entry);
|
|
563
|
+
}
|
|
564
|
+
result.js.sort((a, b) => a.file.localeCompare(b.file));
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (fs.existsSync(cssDir)) {
|
|
568
|
+
const cssFiles = fs.readdirSync(cssDir).filter((f) => f.endsWith(".css"));
|
|
569
|
+
for (const file of cssFiles) {
|
|
570
|
+
const filePath = path.join(cssDir, file);
|
|
571
|
+
const stat = fs.statSync(filePath);
|
|
572
|
+
const isLib = /jquery|fancybox|datepicker|datetimepicker/i.test(file) || file === "cake.generic.css";
|
|
573
|
+
result.css.push({
|
|
574
|
+
file,
|
|
575
|
+
size: stat.size,
|
|
576
|
+
type: isLib ? "library" : "custom",
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
result.css.sort((a, b) => a.file.localeCompare(b.file));
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return result;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// ---------------------------------------------------------------------------
|
|
586
|
+
// PermissionComponent 解析
|
|
587
|
+
// ---------------------------------------------------------------------------
|
|
588
|
+
function analyzePermissionComponent(appDir) {
|
|
589
|
+
const filePath = path.join(appDir, "Controller", "Component", "PermissionComponent.php");
|
|
590
|
+
if (!fs.existsSync(filePath)) return null;
|
|
591
|
+
|
|
592
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
593
|
+
const src = stripBlockComments(raw);
|
|
594
|
+
|
|
595
|
+
const methods = [];
|
|
596
|
+
const fnRe = /(?:public\s+)?function\s+(\w+)\s*\(/g;
|
|
597
|
+
let fm;
|
|
598
|
+
while ((fm = fnRe.exec(src)) !== null) {
|
|
599
|
+
const name = fm[1];
|
|
600
|
+
if (!name.startsWith("__")) methods.push(name);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return { file: "app/Controller/Component/PermissionComponent.php", methods };
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// ---------------------------------------------------------------------------
|
|
607
|
+
// Logic クラスメソッド解析: app/Model/Logic/*.php
|
|
608
|
+
// ---------------------------------------------------------------------------
|
|
609
|
+
function analyzeLogicClasses(appDir) {
|
|
610
|
+
const logicDir = path.join(appDir, "Model", "Logic");
|
|
611
|
+
if (!fs.existsSync(logicDir)) return [];
|
|
612
|
+
|
|
613
|
+
const files = fs.readdirSync(logicDir).filter((f) => f.endsWith(".php"));
|
|
614
|
+
const results = [];
|
|
615
|
+
|
|
616
|
+
for (const file of files) {
|
|
617
|
+
const filePath = path.join(logicDir, file);
|
|
618
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
619
|
+
const src = stripBlockComments(raw);
|
|
620
|
+
|
|
621
|
+
const classMatch = src.match(/class\s+(\w+)\s+extends\s+(\w+)/);
|
|
622
|
+
if (!classMatch) continue;
|
|
623
|
+
|
|
624
|
+
const methods = [];
|
|
625
|
+
const fnRe = /(public|protected|private)\s+function\s+(\w+)\s*\(([^)]*)\)/g;
|
|
626
|
+
let fm;
|
|
627
|
+
while ((fm = fnRe.exec(src)) !== null) {
|
|
628
|
+
if (fm[2].startsWith("__")) continue;
|
|
629
|
+
methods.push({ name: fm[2], visibility: fm[1], params: fm[3].trim() });
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
results.push({
|
|
633
|
+
className: classMatch[1],
|
|
634
|
+
extends: classMatch[2],
|
|
635
|
+
file: "app/Model/Logic/" + file,
|
|
636
|
+
methods,
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
results.sort((a, b) => a.className.localeCompare(b.className));
|
|
641
|
+
return results;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// ---------------------------------------------------------------------------
|
|
645
|
+
// TitlesGraphController アクション→Logic マッピング
|
|
646
|
+
// ---------------------------------------------------------------------------
|
|
647
|
+
function analyzeTitlesGraphMapping(appDir) {
|
|
648
|
+
const filePath = path.join(appDir, "Controller", "TitlesGraphController.php");
|
|
649
|
+
if (!fs.existsSync(filePath)) return [];
|
|
650
|
+
|
|
651
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
652
|
+
const src = stripBlockComments(raw);
|
|
653
|
+
|
|
654
|
+
const actionRe = /public\s+function\s+(\w+)\s*\(\)/g;
|
|
655
|
+
const results = [];
|
|
656
|
+
let am;
|
|
657
|
+
|
|
658
|
+
while ((am = actionRe.exec(src)) !== null) {
|
|
659
|
+
const actionName = am[1];
|
|
660
|
+
if (actionName.startsWith("__") || actionName === "beforeFilter") continue;
|
|
661
|
+
|
|
662
|
+
const bodyStart = am.index + am[0].length;
|
|
663
|
+
const nextFn = src.indexOf("public function", bodyStart + 1);
|
|
664
|
+
const body = src.slice(bodyStart, nextFn > 0 ? nextFn : undefined);
|
|
665
|
+
|
|
666
|
+
const logicRe = /\$this->(\w+Logic)->/g;
|
|
667
|
+
const logics = new Set();
|
|
668
|
+
let lm;
|
|
669
|
+
while ((lm = logicRe.exec(body)) !== null) {
|
|
670
|
+
logics.add(lm[1]);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
let outputType = "画面表示";
|
|
674
|
+
if (/OutputExcel|Excel/i.test(actionName)) outputType = "Excel";
|
|
675
|
+
else if (/OutputCsv|Csv/i.test(actionName)) outputType = "CSV";
|
|
676
|
+
else if (/ajax/i.test(actionName)) outputType = "JSON";
|
|
677
|
+
|
|
678
|
+
results.push({ action: actionName, logicClasses: [...logics], outputType });
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
return results;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// ---------------------------------------------------------------------------
|
|
685
|
+
// Composer 依存パッケージ解析
|
|
686
|
+
// ---------------------------------------------------------------------------
|
|
687
|
+
function analyzeComposerDeps(appDir) {
|
|
688
|
+
const rootDir = path.dirname(appDir);
|
|
689
|
+
const filePath = path.join(rootDir, "composer.json");
|
|
690
|
+
if (!fs.existsSync(filePath)) return { require: {}, requireDev: {} };
|
|
691
|
+
|
|
692
|
+
const json = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
693
|
+
return {
|
|
694
|
+
require: json.require || {},
|
|
695
|
+
requireDev: json["require-dev"] || {},
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// ---------------------------------------------------------------------------
|
|
700
|
+
// ACL 設定解析: app/Config/acl.php
|
|
701
|
+
// ---------------------------------------------------------------------------
|
|
702
|
+
function analyzeAcl(appDir) {
|
|
703
|
+
const filePath = path.join(appDir, "Config", "acl.php");
|
|
704
|
+
if (!fs.existsSync(filePath)) return null;
|
|
705
|
+
|
|
706
|
+
const raw = stripBlockComments(fs.readFileSync(filePath, "utf8"));
|
|
707
|
+
|
|
708
|
+
const aliases = [];
|
|
709
|
+
const aliasSection = raw.match(/\$config\['alias'\]\s*=\s*array\s*\(([\s\S]*?)\)\s*;/);
|
|
710
|
+
if (aliasSection) {
|
|
711
|
+
const re = /['"]([^'"]+)['"]\s*=>\s*['"]([^'"]+)['"]/g;
|
|
712
|
+
let m;
|
|
713
|
+
while ((m = re.exec(aliasSection[1])) !== null) {
|
|
714
|
+
aliases.push({ key: m[1], value: m[2] });
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const roles = [];
|
|
719
|
+
const rolesSection = raw.match(/\$config\['roles'\]\s*=\s*array\s*\(([\s\S]*?)\)\s*;/);
|
|
720
|
+
if (rolesSection) {
|
|
721
|
+
const re = /['"]([^'"]+)['"]\s*=>\s*(null|['"][^'"]*['"])/g;
|
|
722
|
+
let m;
|
|
723
|
+
while ((m = re.exec(rolesSection[1])) !== null) {
|
|
724
|
+
roles.push({ role: m[1], inherits: m[2] === "null" ? null : m[2].replace(/['"]/g, "") });
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const allowRules = [];
|
|
729
|
+
const denyRules = [];
|
|
730
|
+
const rulesSection = raw.match(/\$config\['rules'\]\s*=\s*array\s*\(([\s\S]*?)\)\s*;/);
|
|
731
|
+
if (rulesSection) {
|
|
732
|
+
const body = rulesSection[1];
|
|
733
|
+
const allowSec = body.match(/'allow'\s*=>\s*array\s*\(([\s\S]*?)\)/);
|
|
734
|
+
if (allowSec) {
|
|
735
|
+
const re = /['"]([^'"]*)['"]\s*=>\s*['"]([^'"]+)['"]/g;
|
|
736
|
+
let m;
|
|
737
|
+
while ((m = re.exec(allowSec[1])) !== null) {
|
|
738
|
+
allowRules.push({ resource: m[1], roles: m[2] });
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
const denySec = body.match(/'deny'\s*=>\s*array\s*\(([\s\S]*?)\)\s*,?\s*$/);
|
|
742
|
+
if (denySec) {
|
|
743
|
+
const re = /['"]([^'"]*)['"]\s*=>\s*['"]([^'"]+)['"]/g;
|
|
744
|
+
let m;
|
|
745
|
+
while ((m = re.exec(denySec[1])) !== null) {
|
|
746
|
+
denyRules.push({ resource: m[1], roles: m[2] });
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
return { aliases, roles, allow: allowRules, deny: denyRules };
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// ---------------------------------------------------------------------------
|
|
755
|
+
// Shell 実行フロー詳細解析
|
|
756
|
+
// ---------------------------------------------------------------------------
|
|
757
|
+
function analyzeShellDetails(appDir) {
|
|
758
|
+
const shellDir = path.join(appDir, "Console", "Command");
|
|
759
|
+
if (!fs.existsSync(shellDir)) return [];
|
|
760
|
+
|
|
761
|
+
const files = fs.readdirSync(shellDir).filter((f) => f.endsWith("Shell.php"));
|
|
762
|
+
const results = [];
|
|
763
|
+
|
|
764
|
+
for (const file of files) {
|
|
765
|
+
const filePath = path.join(shellDir, file);
|
|
766
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
767
|
+
const src = stripBlockComments(raw);
|
|
768
|
+
|
|
769
|
+
const classMatch = src.match(/class\s+(\w+)\s+extends\s+(\w+)/);
|
|
770
|
+
if (!classMatch) continue;
|
|
771
|
+
if (classMatch[1] === "AppShell") continue;
|
|
772
|
+
|
|
773
|
+
const hasMail = /CakeEmail/.test(raw);
|
|
774
|
+
const hasFileOps = /rename\s*\(|file_get_contents|fopen|unlink|file_put_contents/.test(raw);
|
|
775
|
+
const hasTransaction = /begin\s*\(\)|rollback\s*\(\)|commit\s*\(/.test(raw);
|
|
776
|
+
|
|
777
|
+
const flowSteps = [];
|
|
778
|
+
if (/getTarget|find\s*\(/.test(src)) flowSteps.push("対象データ取得");
|
|
779
|
+
if (/readdir|scandir|glob\(|file_get_contents/.test(src)) flowSteps.push("ファイル読込");
|
|
780
|
+
if (/import/i.test(src)) flowSteps.push("データインポート");
|
|
781
|
+
if (hasTransaction) flowSteps.push("トランザクション管理");
|
|
782
|
+
if (/createViewReports/.test(src)) flowSteps.push("レポート生成");
|
|
783
|
+
if (hasMail) flowSteps.push("メール通知");
|
|
784
|
+
if (/rename\s*\(/.test(src)) flowSteps.push("ファイルバックアップ");
|
|
785
|
+
if (/unlink\s*\(/.test(src)) flowSteps.push("ファイル削除");
|
|
786
|
+
|
|
787
|
+
results.push({
|
|
788
|
+
className: classMatch[1],
|
|
789
|
+
file: "app/Console/Command/" + file,
|
|
790
|
+
hasMail,
|
|
791
|
+
hasFileOps,
|
|
792
|
+
hasTransaction,
|
|
793
|
+
flowSteps,
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
results.sort((a, b) => a.className.localeCompare(b.className));
|
|
798
|
+
return results;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// ---------------------------------------------------------------------------
|
|
802
|
+
// メール通知仕様解析
|
|
803
|
+
// ---------------------------------------------------------------------------
|
|
804
|
+
function analyzeEmailNotifications(appDir) {
|
|
805
|
+
const result = { config: {}, usages: [] };
|
|
806
|
+
|
|
807
|
+
// email.php 設定
|
|
808
|
+
const emailConfigPath = path.join(appDir, "Config", "email.php");
|
|
809
|
+
if (fs.existsSync(emailConfigPath)) {
|
|
810
|
+
const raw = fs.readFileSync(emailConfigPath, "utf8");
|
|
811
|
+
const src = stripBlockComments(raw);
|
|
812
|
+
const defaultMatch = src.match(/\$default\s*=\s*array\s*\(([\s\S]*?)\)\s*;/);
|
|
813
|
+
if (defaultMatch) {
|
|
814
|
+
const body = defaultMatch[1];
|
|
815
|
+
const transport = body.match(/['"]transport['"]\s*=>\s*['"](\w+)['"]/);
|
|
816
|
+
const from = body.match(/['"]from['"]\s*=>\s*['"]([^'"]+)['"]/);
|
|
817
|
+
if (transport) result.config.transport = transport[1];
|
|
818
|
+
if (from) result.config.defaultFrom = from[1];
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// CakeEmail 使用箇所
|
|
823
|
+
const searchDirs = [
|
|
824
|
+
{ dir: "Console/Command", prefix: "Console/Command" },
|
|
825
|
+
{ dir: "Lib", prefix: "Lib" },
|
|
826
|
+
];
|
|
827
|
+
for (const { dir, prefix } of searchDirs) {
|
|
828
|
+
const dirPath = path.join(appDir, dir);
|
|
829
|
+
if (!fs.existsSync(dirPath)) continue;
|
|
830
|
+
const files = fs.readdirSync(dirPath).filter((f) => f.endsWith(".php"));
|
|
831
|
+
for (const file of files) {
|
|
832
|
+
const filePath = path.join(dirPath, file);
|
|
833
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
834
|
+
if (!raw.includes("CakeEmail")) continue;
|
|
835
|
+
|
|
836
|
+
const subjects = [];
|
|
837
|
+
const subjectStartRe = /->subject\s*\(/g;
|
|
838
|
+
let sm;
|
|
839
|
+
while ((sm = subjectStartRe.exec(raw)) !== null) {
|
|
840
|
+
// balanced-paren で引数全体を取得
|
|
841
|
+
const startIdx = sm.index + sm[0].length;
|
|
842
|
+
let depth = 1;
|
|
843
|
+
let i = startIdx;
|
|
844
|
+
while (i < raw.length && depth > 0) {
|
|
845
|
+
if (raw[i] === "(") depth++;
|
|
846
|
+
else if (raw[i] === ")") depth--;
|
|
847
|
+
i++;
|
|
848
|
+
}
|
|
849
|
+
let subj = raw.slice(startIdx, i - 1).trim();
|
|
850
|
+
subj = subj
|
|
851
|
+
.replace(/Configure::read\(['"]([^'"]+)['"]\)/g, "{$1}")
|
|
852
|
+
.replace(/\s*\.\s*/g, "")
|
|
853
|
+
.replace(/["']/g, "");
|
|
854
|
+
subjects.push(subj);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
const hasCc = /->cc\s*\(/.test(raw);
|
|
858
|
+
|
|
859
|
+
result.usages.push({
|
|
860
|
+
file: "app/" + prefix + "/" + file,
|
|
861
|
+
subjects: [...new Set(subjects)],
|
|
862
|
+
hasCc,
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
return result;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// ---------------------------------------------------------------------------
|
|
871
|
+
// テスト構成解析
|
|
872
|
+
// ---------------------------------------------------------------------------
|
|
873
|
+
function analyzeTestStructure(appDir) {
|
|
874
|
+
const testDir = path.join(appDir, "Test");
|
|
875
|
+
if (!fs.existsSync(testDir)) return null;
|
|
876
|
+
|
|
877
|
+
const result = { controllerTests: 0, modelTests: 0, fixtures: 0 };
|
|
878
|
+
|
|
879
|
+
const caseDir = path.join(testDir, "Case");
|
|
880
|
+
if (fs.existsSync(caseDir)) {
|
|
881
|
+
const ctrlDir = path.join(caseDir, "Controller");
|
|
882
|
+
const modelDir = path.join(caseDir, "Model");
|
|
883
|
+
if (fs.existsSync(ctrlDir)) {
|
|
884
|
+
result.controllerTests = fs.readdirSync(ctrlDir).filter((f) => f.endsWith("Test.php")).length;
|
|
885
|
+
}
|
|
886
|
+
if (fs.existsSync(modelDir)) {
|
|
887
|
+
result.modelTests = fs.readdirSync(modelDir).filter((f) => f.endsWith("Test.php")).length;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
const fixtureDir = path.join(testDir, "Fixture");
|
|
892
|
+
if (fs.existsSync(fixtureDir)) {
|
|
893
|
+
result.fixtures = fs.readdirSync(fixtureDir).filter((f) => f.endsWith("Fixture.php")).length;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
return result;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// ---------------------------------------------------------------------------
|
|
900
|
+
// メインエクスポート
|
|
901
|
+
// ---------------------------------------------------------------------------
|
|
902
|
+
export function analyzeExtras(appDir) {
|
|
903
|
+
const constants = analyzeConstants(appDir);
|
|
904
|
+
const bootstrap = analyzeBootstrap(appDir);
|
|
905
|
+
const appController = analyzeAppController(appDir);
|
|
906
|
+
const appModel = analyzeAppModel(appDir);
|
|
907
|
+
const helpers = analyzeHelpers(appDir);
|
|
908
|
+
const libraries = analyzeLibraries(appDir);
|
|
909
|
+
const behaviors = analyzeBehaviors(appDir);
|
|
910
|
+
const sqlFiles = analyzeSqlFiles(appDir);
|
|
911
|
+
const layouts = analyzeLayouts(appDir);
|
|
912
|
+
const elements = analyzeElements(appDir);
|
|
913
|
+
const assets = analyzeAssets(appDir);
|
|
914
|
+
const permissionComponent = analyzePermissionComponent(appDir);
|
|
915
|
+
const logicClasses = analyzeLogicClasses(appDir);
|
|
916
|
+
const titlesGraphMapping = analyzeTitlesGraphMapping(appDir);
|
|
917
|
+
const composerDeps = analyzeComposerDeps(appDir);
|
|
918
|
+
const acl = analyzeAcl(appDir);
|
|
919
|
+
const shellDetails = analyzeShellDetails(appDir);
|
|
920
|
+
const emailNotifications = analyzeEmailNotifications(appDir);
|
|
921
|
+
const testStructure = analyzeTestStructure(appDir);
|
|
922
|
+
|
|
923
|
+
return {
|
|
924
|
+
constants,
|
|
925
|
+
bootstrap,
|
|
926
|
+
appController,
|
|
927
|
+
appModel,
|
|
928
|
+
helpers,
|
|
929
|
+
libraries,
|
|
930
|
+
behaviors,
|
|
931
|
+
sqlFiles,
|
|
932
|
+
layouts,
|
|
933
|
+
elements,
|
|
934
|
+
assets,
|
|
935
|
+
permissionComponent,
|
|
936
|
+
logicClasses,
|
|
937
|
+
titlesGraphMapping,
|
|
938
|
+
composerDeps,
|
|
939
|
+
acl,
|
|
940
|
+
shellDetails,
|
|
941
|
+
emailNotifications,
|
|
942
|
+
testStructure,
|
|
943
|
+
};
|
|
944
|
+
}
|