feat(state): 静态数据流补类型,调用点按「动作名|局部类名」分桶

1) gen_action_kt 加纯静态局部数据流:实参是变量时,按「形参 → 调用点」和「函数体内最近一次赋值」回溯,深度 2、多来源冲突即放弃;新增 --audit 用运行时真值反向校验推断规则。

2) 调用点改为按「动作名|局部类名」索引。此前按名字索引,同名不同类(Init 有 1 字段版与 18 字段版)互相串味,导致推断出与运行时真值冲突 13 处;分桶后冲突降到 6 处(其中 3 处是运行时未定类型)。

3) analyze_action_trace 修 [object Object] 被当成 List<String> 的误判;客户端每次上报的是本页累计缓冲,页面刷新会覆盖成小文件,改为并入 action_trace_merged.json,只增不减。

结果:已定类型字段 361 → 722(runtime 158 / callsite 196 / dataflow 368),无调用点字段 12,未收录模型类型 37,compileKotlinJs 通过。
This commit is contained in:
TermiNexus committed 2026-10-21 20:33:53 +08:00
1 parent a0466b6544
commit b77e11ad30
8 files changed
+4452 -1950

No files matched your search

File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -1 +1 @@
{"at":"2026-10-21T12:19:33.563Z","count":5426,"trace":[{"s":"SetLocations(captchaLocations=[])"},{"s":"[object Object]"},{"s":"SocialNetworkAvailability(socialNetworkAvailability={facebook=false, google=false, apple=true, vkontakte=false})"},{"s":"SocialNetworkClientIds(socialNetworkClientIds={facebook=558265767576435, google=156495950351-9vvpk8ubf4udrdbrvsma33n01gqm484d.apps.googleusercontent.com, apple=com.3dtank.mobile.production, vkontakte=53596836})"},{"s":"SetInviteNeed(inviteNeed=false)"},{"s":"Init(disableRegistrationMessage=null, disableShopMessage=null, homeScreenMessage=null)"},{"s":"SetPasswordRestrictions(minPasswordLength=8, maxPasswordLength=20, passwordNeedLower=true, passwordNeedUpper=true, passwordNeedDigit=true)"},{"s":"[object Object]"},{"s":"[object Object]"},{"s":"GoTo(screen=[object Object], history=false)"},{"s":"[object Object]"},{"s":"Configure(modes=[MatchmakingModeRank [matchmakingMode = QUICK_BATTLE rank = 1 ], MatchmakingModeRank [matchmakingMode = DM_ONLY rank = 1 ], MatchmakingModeRank [matchmakingMode = TDM_ONLY rank = 1 ], MatchmakingModeRank [matchmakingMode = CTF_ONLY rank = 6 ], MatchmakingModeRank [matchmakingMode = CP_ONLY rank = 2 ], MatchmakingModeRank [matchmakingMode = SGE_ONLY rank = 3 ], MatchmakingModeRank [matchmakingMode = AS_ONLY rank = 7 ], MatchmakingModeRank [matchmakingMode = RUGBY_ONLY rank = 5 ], MatchmakingModeRank [matchmakingMode = TJR_ONLY rank = 4 ], MatchmakingModeRank [matchmakingMode = JGR_ONLY rank = 7 ], MatchmakingModeRank [matchmakingMode = HOLIDAY rank = 8 ]], disabledModes=[JGR_ONLY, DM_ONLY], holidayEnabled=true, holidayTitle=节日模式, holidayIcon=[object Object], eventBigTile=[object Object], holidayDescription=专为当前游戏内庆祝活动创建的特殊模式。 仅在有限的时间内可用。, minRankForProBattle=9, maxUsersInGroup=2, groupsInEventMode=false, eventEndDate=[object Object], borderColorForEventButton=FFCC00)"},{"s":"InitProBattleRewardLimit(proLimit=ProBattleRewardLimitData [crystalFundLimit = 60200 crystalFundValue = 0 xpFundLimit = 36400 xpFundValue = 242 ])"},{"s":"LimitReached(isLimitReached=false)"},{"s":"Loaded(users={3074457345817168740=ClanNotifierData [clanId = 0 clanTag = null userId = 3074457345817168740 ]})"},{"s":"SetLocations(captchaLocations=[])"},{"s":"[object Object]"},{"s":"SetUserId(userId=3074457345817168740)"},{"s":"SetPremiumRemainingSeconds(seconds=0)"},{"s":"SetDisabledWithoutPremiumLocations(locations=[])"},{"s":"SetRankNames(rankNames=[新兵, 二等兵, 一等兵, 下士, 中士, 上士, 三级军士长, 二级军士长, 一级军士长, 军士长, 五级准尉, 四级准尉, 三级准尉, 二级准尉, 一级准尉, 特级准尉, 少尉, 中尉, 上尉, 少校, 中校, 上校, 准将, 少将, 中将, 上将, 元帅, 陆军元帅, 统帅, 大元帅, 传奇])"},{"s":"UpdateHasClan(hasClan=false)"},{"s":"UpdateTimeToJoinRestriction(restrictionTimeJoinClanInSec=0)"},{"s":"AddIncomingClan(clanIds=[])"},{"s":"AddNewIncomingClan(clanIds=[])"},{"s":"AddUsersToNewAccepted(clanId=-2144223137, userIds=[])"},{"s":"AddNewIncoming(newIncoming=[])"},{"s":"UpdateClanLicense(hasLicense=false)"},{"s":"AddUsersToAccepted(usersId=[])"},{"s":"SetLimits(acceptedLimit=300, incomingLimit=50)"},{"s":"AddUsersToIncoming(usersId=[])"},{"s":"AddUsersToAccepted(usersId=[3074457345778698355, 3074457345814924878, 3074457345810102728, 3074457345814619490, 3074457345797743185])"},{"s":"AddUsersToNewAccepted(usersId=[])"},{"s":"AddUsersToNewIncoming(usersId=[])"},{"s":"Loaded(users={3074457345817168740=ClanNotifierData [clanId = 0 clanTag = null userId = 3074457345817168740 ]})"},{"s":"UpdateOnlineStatus(status={3074457345817168740=OnlineNotifierData [online = false AbstractNotifier [userId = 3074457345817168740 ]]})"},{"s":"UpdatePremiumStatus(premiums={3074457345817168740=PremiumNotifierData [hasPremium = false AbstractNotifier [userId = 3074457345817168740 ]]})"},{"s":"UpdateRanks(ranks={3074457345817168740=15})"},{"s":"UpdateUids(uids={3074457345817168740=WYT2WYT})"},{"s":"InitProBattleRewardLimit(proLimit=ProBattleRewardLimitData [crystalFundLimit = 60200 crystalFundValue = 0 xpFundLimit = 36400 xpFundValue = 242 ])"},{"s":"LimitReached(isLimitReached=false)"},{"s":"SetMountedItemsProperties(properties=[ROTATING_WEAPON, HOVER_CHASSIS])"},{"s":"Configure(config=ChatCC [admin = false antifloodEnabled = true bufferSize = 60 channels = [ChannelSidAndName [name = 默认 sid = 默认 ]] chatEnabled = true chatModeratorLevel = NONE hashTags = [HashTagData [tag = help text = Help url = https://en.tankiwiki.com/Technical_support ], HashTagData [tag = rules text = Rules url = http://tankionline.com/en/rules/ ], HashTagData [tag = plans text = Developers’ missions and plans url = http://en.tankiwiki.com/Development_Plans ], HashTagData [tag = ranks text = Ranks url = https://en.tankiwiki.com/Ranks_and_Experience ], HashTagData [tag = clans text = Clans url = http://en.tankiforum.com/index.php?showforum=42 ], HashTagDataLine truncated
{"at":"2026-10-21T12:33:52.214Z","count":934,"trace":[{"s":"SetLocations(captchaLocations=[])"},{"s":"[object Object]"},{"s":"SocialNetworkAvailability(socialNetworkAvailability={facebook=false, google=false, apple=true, vkontakte=false})"},{"s":"SocialNetworkClientIds(socialNetworkClientIds={facebook=558265767576435, google=156495950351-9vvpk8ubf4udrdbrvsma33n01gqm484d.apps.googleusercontent.com, apple=com.3dtank.mobile.production, vkontakte=53596836})"},{"s":"SetInviteNeed(inviteNeed=false)"},{"s":"Init(disableRegistrationMessage=null, disableShopMessage=null, homeScreenMessage=null)"},{"s":"SetPasswordRestrictions(minPasswordLength=8, maxPasswordLength=20, passwordNeedLower=true, passwordNeedUpper=true, passwordNeedDigit=true)"},{"s":"[object Object]"},{"s":"[object Object]"},{"s":"GoTo(screen=[object Object], history=false)"},{"s":"[object Object]"},{"s":"Configure(modes=[MatchmakingModeRank [matchmakingMode = QUICK_BATTLE rank = 1 ], MatchmakingModeRank [matchmakingMode = DM_ONLY rank = 1 ], MatchmakingModeRank [matchmakingMode = TDM_ONLY rank = 1 ], MatchmakingModeRank [matchmakingMode = CTF_ONLY rank = 6 ], MatchmakingModeRank [matchmakingMode = CP_ONLY rank = 2 ], MatchmakingModeRank [matchmakingMode = SGE_ONLY rank = 3 ], MatchmakingModeRank [matchmakingMode = AS_ONLY rank = 7 ], MatchmakingModeRank [matchmakingMode = RUGBY_ONLY rank = 5 ], MatchmakingModeRank [matchmakingMode = TJR_ONLY rank = 4 ], MatchmakingModeRank [matchmakingMode = JGR_ONLY rank = 7 ], MatchmakingModeRank [matchmakingMode = HOLIDAY rank = 8 ]], disabledModes=[JGR_ONLY, DM_ONLY], holidayEnabled=true, holidayTitle=节日模式, holidayIcon=[object Object], eventBigTile=[object Object], holidayDescription=专为当前游戏内庆祝活动创建的特殊模式。 仅在有限的时间内可用。, minRankForProBattle=9, maxUsersInGroup=2, groupsInEventMode=false, eventEndDate=[object Object], borderColorForEventButton=FFCC00)"},{"s":"InitProBattleRewardLimit(proLimit=ProBattleRewardLimitData [crystalFundLimit = 60200 crystalFundValue = 0 xpFundLimit = 36400 xpFundValue = 566 ])"},{"s":"LimitReached(isLimitReached=false)"},{"s":"Loaded(users={3074457345817168740=ClanNotifierData [clanId = 0 clanTag = null userId = 3074457345817168740 ]})"},{"s":"SetLocations(captchaLocations=[])"},{"s":"[object Object]"},{"s":"SetUserId(userId=3074457345817168740)"},{"s":"SetPremiumRemainingSeconds(seconds=0)"},{"s":"SetDisabledWithoutPremiumLocations(locations=[])"},{"s":"SetRankNames(rankNames=[新兵, 二等兵, 一等兵, 下士, 中士, 上士, 三级军士长, 二级军士长, 一级军士长, 军士长, 五级准尉, 四级准尉, 三级准尉, 二级准尉, 一级准尉, 特级准尉, 少尉, 中尉, 上尉, 少校, 中校, 上校, 准将, 少将, 中将, 上将, 元帅, 陆军元帅, 统帅, 大元帅, 传奇])"},{"s":"UpdateHasClan(hasClan=false)"},{"s":"UpdateTimeToJoinRestriction(restrictionTimeJoinClanInSec=0)"},{"s":"AddIncomingClan(clanIds=[])"},{"s":"AddNewIncomingClan(clanIds=[])"},{"s":"AddUsersToNewAccepted(clanId=-2144183555, userIds=[])"},{"s":"AddNewIncoming(newIncoming=[])"},{"s":"UpdateClanLicense(hasLicense=false)"},{"s":"AddUsersToAccepted(usersId=[])"},{"s":"SetLimits(acceptedLimit=300, incomingLimit=50)"},{"s":"AddUsersToIncoming(usersId=[])"},{"s":"AddUsersToAccepted(usersId=[3074457345778698355, 3074457345814924878, 3074457345810102728, 3074457345814619490, 3074457345797743185])"},{"s":"AddUsersToNewAccepted(usersId=[])"},{"s":"AddUsersToNewIncoming(usersId=[])"},{"s":"Loaded(users={3074457345817168740=ClanNotifierData [clanId = 0 clanTag = null userId = 3074457345817168740 ]})"},{"s":"UpdateOnlineStatus(status={3074457345817168740=OnlineNotifierData [online = false AbstractNotifier [userId = 3074457345817168740 ]]})"},{"s":"UpdatePremiumStatus(premiums={3074457345817168740=PremiumNotifierData [hasPremium = false AbstractNotifier [userId = 3074457345817168740 ]]})"},{"s":"UpdateRanks(ranks={3074457345817168740=15})"},{"s":"UpdateUids(uids={3074457345817168740=WYT2WYT})"},{"s":"InitProBattleRewardLimit(proLimit=ProBattleRewardLimitData [crystalFundLimit = 60200 crystalFundValue = 0 xpFundLimit = 36400 xpFundValue = 566 ])"},{"s":"LimitReached(isLimitReached=false)"},{"s":"[object Object]"},{"s":"[object Object]"},{"s":"UnsubscribeUpdateUserData(usersId=[])"},{"s":"SetMountedItemsProperties(properties=[ROTATING_WEAPON, HOVER_CHASSIS])"},{"s":"SetUserPropertiesAction(user=UserPropertiesCC [bazookaGearScore = 110 canUseGroup = true crystals = 972 crystalsRating = 0 droneGearScore = 371 eventPassEndTime = 0 gearScore = 3842 goldsTakenRating = 0 hasSpectatorPermissions = false hullGearScore = 1586 id = 3074457345817168740 partnerUid = null rank = 15 rankBounds = RankBounds [lowBound = 156000 topBound = 192000 ] registrationTimestamp = 0 resistanceGearScore = 25 score = 169847 scoreRating = 0 uid = WYT2WYT userProfileUrl = http://ratings.3dtank.com/user/ userRating = 0 weaponGearScore = 1750 ])"},{"s":"[object Object]"},{"sLine truncated
+1
View File
@@ -0,0 +1 @@
{"updatedAt":"2026-10-21T12:31:30.471Z","count":5673,"trace":[{"s":"SetLocations(captchaLocations=[])"},{"s":"[object Object]"},{"s":"SocialNetworkAvailability(socialNetworkAvailability={facebook=false, google=false, apple=true, vkontakte=false})"},{"s":"SocialNetworkClientIds(socialNetworkClientIds={facebook=558265767576435, google=156495950351-9vvpk8ubf4udrdbrvsma33n01gqm484d.apps.googleusercontent.com, apple=com.3dtank.mobile.production, vkontakte=53596836})"},{"s":"SetInviteNeed(inviteNeed=false)"},{"s":"Init(disableRegistrationMessage=null, disableShopMessage=null, homeScreenMessage=null)"},{"s":"SetPasswordRestrictions(minPasswordLength=8, maxPasswordLength=20, passwordNeedLower=true, passwordNeedUpper=true, passwordNeedDigit=true)"},{"s":"[object Object]"},{"s":"[object Object]"},{"s":"GoTo(screen=[object Object], history=false)"},{"s":"[object Object]"},{"s":"Configure(modes=[MatchmakingModeRank [matchmakingMode = QUICK_BATTLE rank = 1 ], MatchmakingModeRank [matchmakingMode = DM_ONLY rank = 1 ], MatchmakingModeRank [matchmakingMode = TDM_ONLY rank = 1 ], MatchmakingModeRank [matchmakingMode = CTF_ONLY rank = 6 ], MatchmakingModeRank [matchmakingMode = CP_ONLY rank = 2 ], MatchmakingModeRank [matchmakingMode = SGE_ONLY rank = 3 ], MatchmakingModeRank [matchmakingMode = AS_ONLY rank = 7 ], MatchmakingModeRank [matchmakingMode = RUGBY_ONLY rank = 5 ], MatchmakingModeRank [matchmakingMode = TJR_ONLY rank = 4 ], MatchmakingModeRank [matchmakingMode = JGR_ONLY rank = 7 ], MatchmakingModeRank [matchmakingMode = HOLIDAY rank = 8 ]], disabledModes=[JGR_ONLY, DM_ONLY], holidayEnabled=true, holidayTitle=节日模式, holidayIcon=[object Object], eventBigTile=[object Object], holidayDescription=专为当前游戏内庆祝活动创建的特殊模式。 仅在有限的时间内可用。, minRankForProBattle=9, maxUsersInGroup=2, groupsInEventMode=false, eventEndDate=[object Object], borderColorForEventButton=FFCC00)"},{"s":"InitProBattleRewardLimit(proLimit=ProBattleRewardLimitData [crystalFundLimit = 60200 crystalFundValue = 0 xpFundLimit = 36400 xpFundValue = 242 ])"},{"s":"LimitReached(isLimitReached=false)"},{"s":"Loaded(users={3074457345817168740=ClanNotifierData [clanId = 0 clanTag = null userId = 3074457345817168740 ]})"},{"s":"SetLocations(captchaLocations=[])"},{"s":"[object Object]"},{"s":"SetUserId(userId=3074457345817168740)"},{"s":"SetPremiumRemainingSeconds(seconds=0)"},{"s":"SetDisabledWithoutPremiumLocations(locations=[])"},{"s":"SetRankNames(rankNames=[新兵, 二等兵, 一等兵, 下士, 中士, 上士, 三级军士长, 二级军士长, 一级军士长, 军士长, 五级准尉, 四级准尉, 三级准尉, 二级准尉, 一级准尉, 特级准尉, 少尉, 中尉, 上尉, 少校, 中校, 上校, 准将, 少将, 中将, 上将, 元帅, 陆军元帅, 统帅, 大元帅, 传奇])"},{"s":"UpdateHasClan(hasClan=false)"},{"s":"UpdateTimeToJoinRestriction(restrictionTimeJoinClanInSec=0)"},{"s":"AddIncomingClan(clanIds=[])"},{"s":"AddNewIncomingClan(clanIds=[])"},{"s":"AddUsersToNewAccepted(clanId=-2144223137, userIds=[])"},{"s":"AddNewIncoming(newIncoming=[])"},{"s":"UpdateClanLicense(hasLicense=false)"},{"s":"AddUsersToAccepted(usersId=[])"},{"s":"SetLimits(acceptedLimit=300, incomingLimit=50)"},{"s":"AddUsersToIncoming(usersId=[])"},{"s":"AddUsersToAccepted(usersId=[3074457345778698355, 3074457345814924878, 3074457345810102728, 3074457345814619490, 3074457345797743185])"},{"s":"AddUsersToNewAccepted(usersId=[])"},{"s":"AddUsersToNewIncoming(usersId=[])"},{"s":"Loaded(users={3074457345817168740=ClanNotifierData [clanId = 0 clanTag = null userId = 3074457345817168740 ]})"},{"s":"UpdateOnlineStatus(status={3074457345817168740=OnlineNotifierData [online = false AbstractNotifier [userId = 3074457345817168740 ]]})"},{"s":"UpdatePremiumStatus(premiums={3074457345817168740=PremiumNotifierData [hasPremium = false AbstractNotifier [userId = 3074457345817168740 ]]})"},{"s":"UpdateRanks(ranks={3074457345817168740=15})"},{"s":"UpdateUids(uids={3074457345817168740=WYT2WYT})"},{"s":"InitProBattleRewardLimit(proLimit=ProBattleRewardLimitData [crystalFundLimit = 60200 crystalFundValue = 0 xpFundLimit = 36400 xpFundValue = 242 ])"},{"s":"LimitReached(isLimitReached=false)"},{"s":"SetMountedItemsProperties(properties=[ROTATING_WEAPON, HOVER_CHASSIS])"},{"s":"Configure(config=ChatCC [admin = false antifloodEnabled = true bufferSize = 60 channels = [ChannelSidAndName [name = 默认 sid = 默认 ]] chatEnabled = true chatModeratorLevel = NONE hashTags = [HashTagData [tag = help text = Help url = https://en.tankiwiki.com/Technical_support ], HashTagData [tag = rules text = Rules url = http://tankionline.com/en/rules/ ], HashTagData [tag = plans text = Developers’ missions and plans url = http://en.tankiwiki.com/Development_Plans ], HashTagData [tag = ranks text = Ranks url = https://en.tankiwiki.com/Ranks_and_Experience ], HashTagData [tag = clans text = Clans url = http://en.tankiforum.com/index.php?showforum=42 ], HashLine truncated
+22 -14
View File
@@ -1,11 +1,11 @@
{
"generatedAt": "2026-10-21T12:13:34.495Z",
"generatedAt": "2026-10-21T12:31:31.068Z",
"stats": {
"Rows": 4304,
"Parsed": 4129,
"Skipped": 175,
"Rows": 5673,
"Parsed": 5490,
"Skipped": 183,
"Actions": 135,
"Unresolved": 14
"Unresolved": 22
},
"Types": {
"SetLocations": {
@@ -48,7 +48,7 @@
"passwordNeedDigit": "Boolean"
},
"GoTo": {
"screen": "List<String>",
"screen": "Any?",
"history": "Boolean"
},
"Configure": {
@@ -56,13 +56,13 @@
"disabledModes": "List<String>",
"holidayEnabled": "Boolean",
"holidayTitle": "String",
"holidayIcon": "List<String>",
"eventBigTile": "List<String>",
"holidayIcon": "Any?",
"eventBigTile": "Any?",
"holidayDescription": "String",
"minRankForProBattle": "Int",
"maxUsersInGroup": "Int",
"groupsInEventMode": "Boolean",
"eventEndDate": "List<String>",
"eventEndDate": "Any?",
"borderColorForEventButton": "String",
"config": "String"
},
@@ -243,7 +243,7 @@
"params": "String"
},
"InitNotificationSound": {
"sound": "List<String>"
"sound": "Any?"
},
"SetRankIndexToMinGS": {
"rankIndexToMinGS": "Map<String, Int>"
@@ -274,7 +274,7 @@
},
"SetDrone": {
"itemId": "Long",
"resource": "List<String>",
"resource": "Any?",
"emissionColors": "EmissionColors"
},
"SetQuestCategoriesStatistic": {
@@ -328,7 +328,7 @@
"score": "Int"
},
"UnsubscribeUpdateUserData": {
"usersId": "List<Any?>"
"usersId": "Any?"
},
"RemoveBattle": {
"battleId": "Long"
@@ -375,7 +375,7 @@
},
"SetTankResistance": {
"user": "Long",
"tankResistance": "List<String>"
"tankResistance": "Any?"
},
"SetTankInfo": {
"user": "Long",
@@ -511,6 +511,10 @@
"Init.disableShopMessage = null",
"Init.homeScreenMessage = null",
"Init.customBattleName = null",
"GoTo.screen = Any?",
"Configure.holidayIcon = Any?",
"Configure.eventBigTile = Any?",
"Configure.eventEndDate = Any?",
"AddUsersToAccepted.usersId = List<Any?>|List<Long>",
"UpdateUids.uids = Map<String, String>|Map<String, Any>|Map<String, Int>|Map<String, Long>",
"SetAttachedCreatorTag.creatorTag = null",
@@ -518,8 +522,12 @@
"SetActiveContract.contractId = null",
"RemoveItemNotification.excludeType = null",
"SetEventPassShopItemId.shopItemId = null",
"InitNotificationSound.sound = Any?",
"SetDrone.resource = Any?",
"UnsubscribeUpdateUserData.usersId = List<Any?>|List<Long>|List<Int>",
"SetTankResistance.tankResistance = List<String>|List<Any?>",
"AddMessage.message = BattleChatUserMessage|BattleChatTeamMessage",
"GarageDevicesLoadedAction.devices = List<GarageDeviceObject>|List<Any?>",
"QuestsChangeCategory.previousScreen = List<String>|QuestCategory"
"QuestsChangeCategory.previousScreen = Any?|QuestCategory"
]
}
+15 -1
View File
@@ -17,7 +17,19 @@ const path = require('path');
const PROJECT = path.resolve(__dirname, '..');
const Trace = JSON.parse(fs.readFileSync(path.join(PROJECT, 'names', 'action_trace.json'), 'utf8'));
const Rows = Trace.trace || [];
// 客户端每次上报的是「本页累计缓冲」,页面刷新会覆盖成更小的文件;
// 这里把历史并进 action_trace_merged.json,保证只增不减。
const MergedPath = path.join(PROJECT, 'names', 'action_trace_merged.json');
let Merged = [];
try { Merged = JSON.parse(fs.readFileSync(MergedPath, 'utf8')).trace || []; } catch (E) { Merged = []; }
const SeenText = new Set(Merged.map((R) => R.s));
const Rows = Merged.slice();
for (const R of Trace.trace || []) {
if (SeenText.has(R.s)) continue;
SeenText.add(R.s);
Rows.push(R);
}
fs.writeFileSync(MergedPath, JSON.stringify({ updatedAt: new Date().toISOString(), count: Rows.length, trace: Rows }), 'utf8');
// --- 值解析 -------------------------------------------------------------
function SplitTop(Text, Sep) {
@@ -42,6 +54,8 @@ function ParseValue(V) {
return Math.abs(N) > 2147483647 ? 'Long' : 'Int';
}
if (/^-?\d+\.\d+(E-?\d+)?$/.test(S) || /^-?\d+E-?\d+$/i.test(S)) return 'Double';
// 注意 `[object Object]` 也满足 startsWith('[') && endsWith(']'),它不是列表
if (/^\[object [A-Za-z]+\]$/.test(S)) return 'Any?';
if (S.startsWith('[') && S.endsWith(']')) {
const Inner = S.slice(1, -1).trim();
if (!Inner) return 'List<Any?>';
+210 -8
View File
@@ -23,6 +23,9 @@ const path = require('path');
const PROJECT = path.resolve(__dirname, '..');
const Argv = process.argv.slice(2);
const Arg = (N, D) => { const I = Argv.indexOf('--' + N); return I >= 0 && Argv[I + 1] ? Argv[I + 1] : D; };
// --audit:对「已有运行时真值」的字段也跑一遍推断,输出冲突,用来校验推断规则是否可靠
const Audit = Argv.indexOf('--audit') >= 0;
const Conflicts = [];
// TankIC/3DTank/tools → TankIC/TankiOffline/...
const OUT = path.resolve(Arg('out', path.join(PROJECT, '..', 'TankiOffline', 'app', 'src', 'jsMain', 'kotlin', 'App', 'State', 'Inventory.kt')));
@@ -111,10 +114,188 @@ function CheckType(T, Source, Self) {
? { Ok: true, Type: 'Map<' + P[0].Type.replace(/\?$/, '') + ', ' + P[1].Type.replace(/\?$/, '') + '>' + (/\?$/.test(T) ? '?' : '') }
: { Ok: false, Bad: T };
}
if (Source === 'callsite' && Names.has(S) && S !== Self) return { Ok: true, Type: T };
if ((Source === 'callsite' || Source === 'dataflow') && Names.has(S) && S !== Self) return { Ok: true, Type: T };
return { Ok: false, Bad: T };
}
// ---- 纯静态局部数据流:实参是变量时,回溯形参/局部赋值 ----
// 只做两种最可靠的形状,且深度受限、冲突即放弃(不猜):
// ① 标识符是某个函数的形参 → 找该函数的调用点,推断对应位置的实参类型
// ② 标识符最近一次被赋值 → 推断赋值右侧表达式
const SrcDir = path.join(PROJECT, 'src');
const SrcCache = new Map();
function SrcOf(Mod) {
if (!SrcCache.has(Mod)) {
let Text = '';
try { Text = fs.readFileSync(path.join(SrcDir, 'module_' + String(Mod).replace(/^module_/, '') + '.js'), 'utf8'); } catch (E) { Text = ''; }
SrcCache.set(Mod, Text);
}
return SrcCache.get(Mod);
}
// 模块内所有 `function Name(params) {`(带花括号配对得到的函数体范围)
const FnIndexCache = new Map();
const BraceCache = new Map();
function BracePairs(Text) {
if (BraceCache.has(Text)) return BraceCache.get(Text);
const Pairs = new Map();
const Stack = [];
let I = 0;
while (I < Text.length) {
const C = Text[I];
if (C === '"' || C === "'" || C === '`') { I = SkipString(Text, I); continue; }
if (C === '/' && Text[I + 1] === '/') { const N = Text.indexOf('\n', I); if (N < 0) break; I = N; continue; }
if (C === '/' && Text[I + 1] === '*') { const N = Text.indexOf('*/', I + 2); if (N < 0) break; I = N + 2; continue; }
if (C === '{') Stack.push(I);
else if (C === '}') { const O = Stack.pop(); if (O !== undefined) Pairs.set(O, I); }
I++;
}
BraceCache.set(Text, Pairs);
return Pairs;
}
function SkipString(Text, I) {
const Q = Text[I];
I++;
while (I < Text.length) {
if (Text[I] === '\\') { I += 2; continue; }
if (Text[I] === Q) return I + 1;
I++;
}
return I;
}
function FnIndex(Mod) {
if (!FnIndexCache.has(Mod)) {
const Text = SrcOf(Mod);
const Pairs = BracePairs(Text);
const List = [];
for (const M of Text.matchAll(/(?<![\w$.])function\s+([A-Za-z0-9_$]+)\s*\(([^)]*)\)\s*\{/g)) {
const Open = M.index + M[0].length - 1;
const End = Pairs.get(Open);
if (End === undefined) continue;
List.push({
Name: M[1],
Params: M[2].trim() ? M[2].split(',').map((S) => S.trim()) : [],
Start: M.index,
End,
});
}
FnIndexCache.set(Mod, List);
}
return FnIndexCache.get(Mod);
}
// 包住 Off、且形参表里含 Param 的最内层函数
function EnclosingFn(Mod, Off, Param) {
let Best = null;
for (const F of FnIndex(Mod)) {
if (F.Start > Off || F.End < Off) continue;
if (F.Params.indexOf(Param) < 0) continue;
if (!Best || F.Start > Best.Start) Best = F;
}
return Best;
}
const FnCallsCache = new Map();
function FnCalls(Mod, Name) {
const Key = Mod + '|' + Name;
if (FnCallsCache.has(Key)) return FnCallsCache.get(Key);
const Text = SrcOf(Mod);
const Out = [];
const Re = new RegExp('(?<![\\w$.])' + Name.replace(/[$]/g, '\\$') + '\\s*\\(', 'g');
let M;
while ((M = Re.exec(Text)) && Out.length < 40) {
if (/function\s+$/.test(Text.slice(Math.max(0, M.index - 10), M.index))) continue;
const End = MatchParen(Text, Text.indexOf('(', M.index));
if (End < 0) continue;
Out.push({ Args: SplitArgs(Text.slice(Text.indexOf('(', M.index) + 1, End)), At: M.index });
}
FnCallsCache.set(Key, Out);
return Out;
}
function MatchParen(Text, Open) {
let Depth = 0;
for (let I = Open; I < Text.length; I++) {
const C = Text[I];
if (C === '(') Depth++;
else if (C === ')') { Depth--; if (Depth === 0) return I; }
}
return -1;
}
function SplitArgs(Text) {
const Out = [];
let Depth = 0;
let Cur = '';
for (const C of Text) {
if (C === '(' || C === '[' || C === '{') Depth++;
else if (C === ')' || C === ']' || C === '}') Depth--;
if (C === ',' && Depth === 0) { Out.push(Cur.trim()); Cur = ''; continue; }
Cur += C;
}
if (Cur.trim()) Out.push(Cur.trim());
return Out;
}
// [From, To) 范围内最近一次 `X = <expr>`
function LastAssign(Text, Name, From, To) {
const Re = new RegExp('(?<![\\w$.])' + Name.replace(/[$]/g, '\\$') + '\\s*=\\s*([^;\\n]{1,80})', 'g');
let Best = null;
let M;
while ((M = Re.exec(Text)) && M.index < To) {
if (M.index >= From) Best = M[1];
}
if (!Best) return null;
const E = Best.trim().replace(/[,)]+$/, '').trim();
return /^[A-Za-z0-9_$"]/.test(E) || /^!/.test(E) ? E : null;
}
// 多来源类型合一:全同 → 该类型;Int/Long 混合 → Long;否则放弃
function Unify(List) {
const S = new Set(List.filter(Boolean));
if (!S.size) return null;
if (S.size === 1) return [...S][0];
if (S.size === 2 && S.has('Int') && S.has('Long')) return 'Long';
return null;
}
const FlowSeen = new Set();
function InferDeep(Expr, Mod, Off, Depth) {
const E = String(Expr || '').trim();
if (!E) return null;
const Base = InferType(E);
if (Base) return Base;
if (Depth <= 0 || !Mod) return null;
const Text = SrcOf(Mod);
if (!Text) return null;
// a.b.c 形式:用最后一段查协议字段表
const Chain = /^(?:[A-Za-z_$][\w$]*\.)+([A-Za-z0-9_$]+)$/.exec(E);
if (Chain) {
const P = PropType.get(Chain[1]);
return P ? KotlinOf(P) : null;
}
if (!/^[A-Za-z_$][\w$]*$/.test(E)) return null;
const Guard = Mod + '|' + Off + '|' + E + '|' + Depth;
if (FlowSeen.has(Guard)) return null;
FlowSeen.add(Guard);
try {
// ① 形参 → 调用点(按调用点自身的作用域继续回溯)
const Fn = EnclosingFn(Mod, Off, E);
if (Fn) {
const Idx = Fn.Params.indexOf(E);
const Types = [];
for (const Site of FnCalls(Mod, Fn.Name)) {
if (Site.Args.length <= Idx) continue;
Types.push(InferDeep(Site.Args[Idx], Mod, Site.At, Depth - 1));
}
const U = Unify(Types);
if (U) return U;
}
// ② 所在函数体内的最近一次赋值
const Assign = LastAssign(Text, E, Fn ? Fn.Start : 0, Off);
if (Assign && Assign !== E) return InferDeep(Assign, Mod, Off, Depth - 1);
} finally {
FlowSeen.delete(Guard);
}
return null;
}
const Emitted = [];
const Seen = new Set();
let TypedFields = 0;
@@ -122,25 +303,42 @@ let TotalFields = 0;
for (const R of Inventory.Rows) {
if (Seen.has(R.Name)) continue;
Seen.add(R.Name);
const Sites = Calls[R.Name] || [];
// 调用点按「动作名|局部类名」索引:同名不同类(如 Init 的 1 字段版/18 字段版)不能混用
const Sites = Calls[R.Name + '|' + R.Local] || [];
const Fields = R.ToStringFields.length ? R.ToStringFields : R.Params.map((P, I) => 'field' + (I + 1));
const Lines = [];
Fields.forEach((F, I) => {
TotalFields++;
// ① 运行时打点(真值)→ ② 协议字段表/字面量反推 → ③ Any?
// ① 运行时打点(真值)→ ② 调用点/数据流反推 → ③ Any?
const Rt = RuntimeTypes[R.Name] && RuntimeTypes[R.Name][F];
let Type = Rt || null;
let Source = Rt ? 'runtime' : '';
let Inferred = null;
let InferredSource = '';
const Notes = [];
if (!Type) {
let NullSeen = false;
{
const Deep = [];
for (const S of Sites) {
const A = S.Args[I];
if (A === undefined) continue;
if (A.trim() === 'null') NullSeen = true;
const T = InferType(A);
if (T) { if (!Type) { Type = T; Source = 'callsite'; } }
else if (Notes.length < 2) Notes.push(A.slice(0, 40));
if (T) { if (!Inferred) { Inferred = T; InferredSource = 'callsite'; } }
else {
if (Notes.length < 2) Notes.push(A.slice(0, 40));
const D = InferDeep(A, S.Module, S.At, 2);
if (D) Deep.push(D);
}
}
if (!Inferred) {
const U = Unify(Deep);
if (U) { Inferred = U; InferredSource = 'dataflow'; }
}
}
// 协议字段表的 opt<> 只说明「线上可选」,动作字段本身没观察到 null 就当非空
if (Inferred && !NullSeen) Inferred = Inferred.replace(/\?$/, '');
if (Audit && Rt && Inferred && Rt !== Inferred) Conflicts.push(R.Name + '.' + F + ' 运行时=' + Rt + ' 推断=' + Inferred + '(' + InferredSource + ')');
let Type = Rt || Inferred;
let Source = Rt ? 'runtime' : InferredSource;
let Note = '';
if (Type) {
const C = CheckType(Type, Source, R.Name);
@@ -172,4 +370,8 @@ fs.mkdirSync(path.dirname(OUT), { recursive: true });
fs.writeFileSync(OUT, Head.join('\n') + Emitted.join('\n\n') + '\n', 'utf8');
console.log('生成 ' + Emitted.length + ' 个 data class,字段 ' + TotalFields + ',已定类型 ' + TypedFields +
'(' + ((TypedFields * 100 / Math.max(1, TotalFields)).toFixed(1)) + '%)');
if (Audit) {
console.log('推断 vs 运行时真值冲突 ' + Conflicts.length + ' 处');
for (const C of Conflicts.slice(0, 25)) console.log(' ' + C);
}
console.log(' 输出: ' + OUT);
+20 -16
View File
@@ -59,15 +59,18 @@ const ExportToLocal = new Map();
for (const M of Src10032.matchAll(/t\.\$_\$\.([A-Za-z0-9_$]+) = ([A-Za-z0-9_$]+)/g)) ExportToLocal.set(M[1], M[2]);
const LocalToName = new Map();
for (const R of Inventory.Rows) if (!LocalToName.has(R.Local)) LocalToName.set(R.Local, R.Name);
const KeyToName = new Map();
for (const [K, L] of ExportToLocal) if (LocalToName.has(L)) KeyToName.set(K, LocalToName.get(L));
// 键必须带局部类名:不同模块里存在同名但字段完全不同的动作类(如 Init 有 1 字段版和 18 字段版)
const KeyToClass = new Map();
for (const [K, L] of ExportToLocal) if (LocalToName.has(L)) KeyToClass.set(K, { Name: LocalToName.get(L), Local: L });
// 扫全包调用点
const Calls = new Map(); // 动作名 → [{Module, Args}]
const Calls = new Map(); // "动作名|局部类名" → [{Module, At, Args}]
// 在 Text 里收集 `<Local>(…)` 的构造点(Kotlin/JS 的 data class 用 new 调用)
function CollectCalls(Text, Local, Name, Module) {
const Re = new RegExp('(?<![\\w$.])new ' + Local.replace(/[$]/g, '\\$') + '\\s*\\(', 'g');
// 在 Text 里收集 `new <NewName>(…)` 的构造点(Kotlin/JS 的 data class 用 new 调用)
// KeyLocal = 该类在 module_10032 里的局部类名(跨模块时 NewName 是别名,不能拿来做键)
function CollectCalls(Text, NewName, Name, KeyLocal, Module) {
const Key = Name + '|' + KeyLocal;
const Re = new RegExp('(?<![\\w$.])new ' + NewName.replace(/[$]/g, '\\$') + '\\s*\\(', 'g');
let M;
while ((M = Re.exec(Text))) {
const Open = Text.indexOf('(', M.index);
@@ -79,35 +82,36 @@ function CollectCalls(Text, Local, Name, Module) {
}
if (End < 0) continue;
const Args = SplitArgs(Text.slice(Open + 1, End));
const List = Calls.get(Name) || [];
List.push({ Module, Args });
Calls.set(Name, List);
const List = Calls.get(Key) || [];
// At = new 关键字在文件里的偏移,供 gen_action_kt 做局部数据流回溯
List.push({ Module, At: M.index, Args });
Calls.set(Key, List);
}
}
// ① module_10032 内部:动作之间互相 new(这些类大多不导出,外部扫不到)
for (const [Local, Name] of LocalToName) CollectCalls(Src10032, Local, Name, 'module_10032');
for (const [Local, Name] of LocalToName) CollectCalls(Src10032, Local, Name, Local, 'module_10032');
// ② 其它模块:经由 `别名 = dep.$_$.<导出键>` 引入后 new
for (const F of fs.readdirSync(SRC).filter((N) => /^module_\d+\.js$/.test(N))) {
if (F === 'module_10032.js') continue;
const Text = fs.readFileSync(path.join(SRC, F), 'utf8');
const Deps = FindDeps(Text);
const Alias = new Map(); // 局部别名 → 动作名
const Alias = new Map(); // 局部别名 → {Name, Local}
for (const M of Text.matchAll(/(?<![A-Za-z0-9_$.])([A-Za-z_$][\w$]*) = ([A-Za-z_$][\w$]*)\.\$_\$\.([A-Za-z0-9_$]+)[,;)]/g)) {
if (Deps[M[2]] !== '10032') continue;
const Name = KeyToName.get(M[3]);
if (Name && !Alias.has(M[1])) Alias.set(M[1], Name);
const Cls = KeyToClass.get(M[3]);
if (Cls && !Alias.has(M[1])) Alias.set(M[1], Cls);
}
if (!Alias.size) continue;
for (const [Local, Name] of Alias) CollectCalls(Text, Local, Name, F.replace('.js', ''));
for (const [Local, Cls] of Alias) CollectCalls(Text, Local, Cls.Name, Cls.Local, F.replace('.js', ''));
}
const Named = Inventory.Rows.filter((R) => Calls.has(R.Name));
const Named = Inventory.Rows.filter((R) => Calls.has(R.Name + '|' + R.Local));
console.log('有调用点的动作/状态类 ' + Named.length + ' / ' + Inventory.Rows.length);
console.log('总调用点 ' + [...Calls.values()].reduce((A, B) => A + B.length, 0));
for (const R of Named.slice(0, Limit)) {
const List = Calls.get(R.Name);
const List = Calls.get(R.Name + '|' + R.Local);
const Sample = List.slice(0, 3).map((C) => C.Module + ': ' + C.Args.join(' , '));
console.log(' ' + R.Name + '(' + R.Params.join(',') + ') ×' + List.length);
for (const S of Sample) console.log(' ' + S.slice(0, 140));
File diff suppressed because it is too large. Load diff