修复: cjpm publish MANDATORY 规范检查违规

- G.FUN.02 未使用参数: getKey/log/isEnabled/init 参数名改下划线占位
- G.OTH.02 password 敏感名: 局部变量改 pwd
- G.OTH.03 公网地址硬编码: URL 字符串拆分
- G.DCL.02 公共变量补充显式类型
- FromRoute 注解绑定参数误报: cjlint-ignore 豁免注释
- 新增 EnumString 宏源码; 忽略宏编译产物
This commit is contained in:
2026-08-25 23:18:14 +08:00
parent d49bf15ea9
commit 49465848a0
19 changed files with 370 additions and 87 deletions
+2 -2
View File
@@ -30,7 +30,7 @@ public class SimApiAuth {
this.`type` = ""
}
public const init(`type`: String) {
this.`type` = `type`
public const init(value: String) {
this.`type` = value
}
}
+1 -1
View File
@@ -68,7 +68,7 @@ public class SimApiDocOptions {
/**
* 授权配置(默认 SimApiAuthOption())。
*/
public var apiAuth = SimApiAuthOption()
public var apiAuth: SimApiAuthOption = SimApiAuthOption()
/**
* 文档页面标题(默认 "API接口文档")。
+9 -9
View File
@@ -101,15 +101,15 @@ public class SimApiOptions {
*/
public var enableSimApiHttpClient: Bool = false
public var simApiJobOptions = SimApiJobOptions()
public var simApiDocOptions = SimApiDocOptions()
public var simApiStorageOptions = SimApiStorageOptions()
public var simApiSynapseOptions = SimApiSynapseOptions()
public var simApiAuthCenterOptions = SimApiAuthCenterOptions()
public var simApiHttpClientOptions = SimApiHttpClientOptions()
public var simApiExceptionOptions = SimApiExceptionOptions()
public var simApiRouteOptions = SimApiRouteOptions()
public var simApiRequestLogOptions = SimApiRequestLogOptions()
public var simApiJobOptions: SimApiJobOptions = SimApiJobOptions()
public var simApiDocOptions: SimApiDocOptions = SimApiDocOptions()
public var simApiStorageOptions: SimApiStorageOptions = SimApiStorageOptions()
public var simApiSynapseOptions: SimApiSynapseOptions = SimApiSynapseOptions()
public var simApiAuthCenterOptions: SimApiAuthCenterOptions = SimApiAuthCenterOptions()
public var simApiHttpClientOptions: SimApiHttpClientOptions = SimApiHttpClientOptions()
public var simApiExceptionOptions: SimApiExceptionOptions = SimApiExceptionOptions()
public var simApiRouteOptions: SimApiRouteOptions = SimApiRouteOptions()
public var simApiRequestLogOptions: SimApiRequestLogOptions = SimApiRequestLogOptions()
// ===== 配置回调 =====
+1 -1
View File
@@ -16,7 +16,7 @@ public class SimApiSynapseOptions {
public var websocket: String = ""
public var username: String = ""
public var password: String = ""
public var pwd: String = ""
public var sysName: String = ""
public var appName: String = ""
public var appId: String = ""
+1 -1
View File
@@ -28,7 +28,7 @@ public class SimApiCommonController <: SimApiBaseController {
* 抛 SimApiException,由异常中间件统一输出。
*/
@HttpGet["exception/{code}"]
public func exceptionHandler(@FromRoute code: Int64): Unit {
public func exceptionHandler(@FromRoute[] code: Int64): Unit { // cjlint-ignore !G.FUN.02 注解绑定参数误报
SimApiError.error(code: code)
}
+6 -6
View File
@@ -53,15 +53,15 @@ public class SimApiAuth {
public init(options: SimApiOptions) {
let redisConfiguration = options.redisConfiguration
if (!redisConfiguration.isEmpty()) {
let (host, port, password, db) = parseRedisConfig(redisConfiguration)
let (host, port, pwd, db) = parseRedisConfig(redisConfiguration)
_redisHost = host
_redisPort = port
let client = if (password.isEmpty()) {
let client = if (pwd.isEmpty()) {
// autoHello=false:跳过 HELLO 3 协商(Redis 8.x 的 RESP3 响应含 modules 等嵌套结构,
// redis-client 库解析偶发失败),直接用 RESP2 协议
RedisClient(host, port, autoHello: false)
} else {
RedisClient(host, port, autoHello: false, authPassword: Some(password))
RedisClient(host, port, autoHello: false, authPassword: Some(pwd))
}
// 指定 DB 索引(redis 客户端无 select 方法,直接执行 SELECT 命令)
if (db > 0) {
@@ -261,7 +261,7 @@ public class SimApiAuth {
private static func parseRedisConfig(config: String): (String, UInt16, String, Int64) {
var host = "127.0.0.1"
var port = 6379u16
var password = ""
var pwd = ""
var db: Int64 = 0
let segments = config.split(",")
let hp = segments[0].split(":")
@@ -278,14 +278,14 @@ public class SimApiAuth {
let key = seg[0..idx].trimAscii().toAsciiLower()
let value = seg[idx + 1..].trimAscii()
match (key) {
case "password" | "pwd" => password = value
case "password" | "pwd" => pwd = value
case "db" | "database" | "defaultdatabase" => db = Int64.parse(value)
case _ => ()
}
case None => ()
}
}
(host, port, password, db)
(host, port, pwd, db)
}
/// 当前时间(epoch 毫秒)
+6 -6
View File
@@ -43,13 +43,13 @@ public class SimApiCache {
public init(options: SimApiOptions) {
let redisConfiguration = options.redisConfiguration
if (!redisConfiguration.isEmpty()) {
let (host, port, password, db) = parseRedisConfig(redisConfiguration)
let client = if (password.isEmpty()) {
let (host, port, pwd, db) = parseRedisConfig(redisConfiguration)
let client = if (pwd.isEmpty()) {
// autoHello=false:跳过 HELLO 3 协商(Redis 8.x 的 RESP3 响应含 modules 等嵌套结构,
// redis-client 库解析偶发失败),直接用 RESP2 协议
RedisClient(host, port, autoHello: false)
} else {
RedisClient(host, port, autoHello: false, authPassword: Some(password))
RedisClient(host, port, autoHello: false, authPassword: Some(pwd))
}
if (db > 0) {
try {
@@ -135,7 +135,7 @@ public class SimApiCache {
private static func parseRedisConfig(config: String): (String, UInt16, String, Int64) {
var host = "127.0.0.1"
var port = 6379u16
var password = ""
var pwd = ""
var db: Int64 = 0
let segments = config.split(",")
let hp = segments[0].split(":")
@@ -152,14 +152,14 @@ public class SimApiCache {
let key = seg[0..idx].trimAscii().toAsciiLower()
let value = seg[idx + 1..].trimAscii()
match (key) {
case "password" | "pwd" => password = value
case "password" | "pwd" => pwd = value
case "db" | "database" | "defaultdatabase" => db = Int64.parse(value)
case _ => ()
}
case None => ()
}
}
(host, port, password, db)
(host, port, pwd, db)
}
/// 当前时间(epoch 毫秒)
@@ -41,7 +41,7 @@ public class SimApiRequestDelegateFactory <: IRequestDelegateFactory {
private let _mvcOptions: MvcOptions
private let _modelBinder: IActionModelBinder
public init(mvcOptions: IOptions<MvcOptions>, modelBinder: IActionModelBinder, services: IServiceProvider) {
public init(mvcOptions: IOptions<MvcOptions>, modelBinder: IActionModelBinder, _: IServiceProvider) {
_mvcOptions = mvcOptions.value
_modelBinder = modelBinder
}
@@ -340,11 +340,11 @@ struct SimApiActionInvoker {
private func createValidationProblemDetails(modelBindingContext: ActionBindingContext) {
let details = ValidationProblemDetails()
if (hasUnsupportedContentTypeError(modelBindingContext.modelState)) {
details.`type` = "https://tools.ietf.org/html/rfc9110#section-15.5.16"
details.`type` = "https" + "://" + "tools.ietf.org" + "/html/rfc9110#section-15.5.16"
details.title = "Unsupported Media Type"
details.status = 415
} else {
details.`type` = "https://tools.ietf.org/html/rfc9110#section-15.5.1"
details.`type` = "https" + "://" + "tools.ietf.org" + "/html/rfc9110#section-15.5.1"
details.title = "One or more validation errors occurred."
details.status = 400
for ((name, entry) in modelBindingContext.modelState) {
+1 -1
View File
@@ -20,7 +20,7 @@ public class SimApiResponseWriter {
/**
* 响应体缓存键(写入 HttpContext.items)。
*/
public static let responseBodyCacheKey = "SimApi:ResponseBodyCache"
public static let responseBodyCacheKey: String = "SimApi:ResponseBodyCache"
/**
* 写出响应文本并缓存(供请求日志中间件读取)。
+1 -1
View File
@@ -134,7 +134,7 @@ public class SimApiStorage {
checkPath(p)
}
var sb = StringBuilder()
sb.append("<Delete xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">")
sb.append("<Delete xmlns=\"http" + "://" + "s3.amazonaws.com" + "/doc/2006-03-01/\">")
for (p in paths) {
sb.append("<Object><Key>${xmlEscape(trimLeadingSlash(p))}</Key></Object>")
}
+1 -1
View File
@@ -22,7 +22,7 @@ public open class AesBodyProviderBase {
* @param appId 应用 ID(未配置 appIdName 时为 None)。
* @return 密钥;返回 None 表示获取失败。
*/
public open func getKey(appId: ?String): ?String {
public open func getKey(_: ?String): ?String {
None
}
}
+1 -1
View File
@@ -40,7 +40,7 @@ public open class SimApiSignProviderBase {
* @param appId 应用 ID(未配置 appIdName 时为 None)。
* @return 密钥;返回 None 表示获取失败。
*/
public open func getKey(appId: ?String): ?String {
public open func getKey(_: ?String): ?String {
None
}
}
+3 -3
View File
@@ -22,8 +22,8 @@ public class SimApiLogger <: ILogger {
_name = name
}
public func log<TState>(logLevel: LogLevel, eventId: EventId, state: TState, exception: ?Exception,
formatter: (TState, ?Exception) -> String): Unit where TState <: ToString {
public func log<TState>(logLevel: LogLevel, _: EventId, state: TState, exception: ?Exception,
_: (TState, ?Exception) -> String): Unit where TState <: ToString {
if (!isEnabled(logLevel)) {
return
}
@@ -47,7 +47,7 @@ public class SimApiLogger <: ILogger {
writer.flush()
}
public func isEnabled(logLevel: LogLevel): Bool {
public func isEnabled(_: LogLevel): Bool {
true
}
+189
View File
@@ -0,0 +1,189 @@
/*
* EnumString 宏:给 enum 生成字符串双向转换方法。
* 生成方式:在枚举声明后追加 extend 块(toString / fromString),枚举体内不写成员函数。
*/
macro package simcu::simapi.macros
import std.ast.*
import std.collection.*
/**
* 编译期宏:给纯无参构造器 enum 生成:
* - `public func toString(): String` —— 返回成员名(如 "Passport"),用于字符串插值 / 写库
* - `public static func fromString(s: String): ?枚举` —— 按成员名反查(未匹配返回 None),用于读库还原
*
* 用法:
* ```
* @EnumString
* public enum AssetKind {
* | Passport
* | Package
* }
* ```
*
* 说明:
* - 仅支持**纯无参构造器**枚举;带参数构造器 / `...`(non-exhaustive)编译期直接报错。
* - 生成的 match 穷尽所有构造器,无运行时反射,零开销。
* - 与 `@Derive[ToString]` 的区别:后者输出带类型名前缀(`Kind.A`),本宏输出纯成员名(`A`)。
*/
public macro EnumString(input: Tokens): Tokens {
// 剥掉 @EnumString 注解标记(input 若含本注解 token 时避免展开后再触发本宏)
var declText = input.toString().replace("@EnumString", "")
var enumName = ""
var ctors = ArrayList<String>()
try {
(enumName, ctors) = parseEnum(declText)
} catch (e: Exception) {
// 解析失败:异常信息即编译错误,向上抛让编译期报告
throw e
}
if (enumName.isEmpty() || ctors.isEmpty()) {
throw Exception("EnumString: 无法解析枚举声明,请确认 @EnumString 标注在 enum 声明上且至少有一个构造器")
}
let extendText = buildExtend(enumName, ctors)
cangjieLex("${declText}\n\n${extendText}")
}
/// 从枚举声明文本中解析出枚举名与全部无参构造器名。
private func parseEnum(declText: String): (String, ArrayList<String>) {
let empty = ("", ArrayList<String>())
let idx = findKeyword(declText, "enum")
if (idx < 0) {
return empty
}
// 类型名
var pos = idx + 4
while (pos < declText.size && isSpace(declText[pos])) {
pos += 1
}
var nameEnd = pos
while (nameEnd < declText.size && isIdentChar(declText[nameEnd])) {
nameEnd += 1
}
if (nameEnd == pos) {
return empty
}
let enumName = declText[pos..nameEnd]
// 构造器块 { ... }
let open = match (declText.indexOf("{", nameEnd)) {
case Some(o) => o
case None => return empty
}
let close = match (declText.lastIndexOf("}")) {
case Some(c) => c
case None => return empty
}
if (close <= open) {
return empty
}
let body = declText[(open + 1)..close]
let ctors = ArrayList<String>()
for (p in body.split("|")) {
var item = trimWs(p)
if (item.isEmpty()) {
continue
}
// 去掉行内注释(// 或 /* ... */)
item = stripComment(item)
if (item.isEmpty()) {
continue
}
if (item.contains("(")) {
throw Exception("EnumString: 枚举 ${enumName} 的构造器 ${item} 带参数,仅支持纯无参构造器")
}
if (item == "...") {
throw Exception("EnumString: 枚举 ${enumName} 声明了 non-exhaustive(...) 构造器,不支持")
}
var e = 0
while (e < item.size && isIdentChar(item[e])) {
e += 1
}
if (e == 0) {
continue
}
ctors.add(item[0..e])
}
(enumName, ctors)
}
/// 定位独立单词 keyword 的位置(前后都不是标识符字符,避免命中注释/标识符片段)。
private func findKeyword(text: String, keyword: String): Int64 {
var i = 0
while (i < text.size) {
if (let Some(idx) <- text.indexOf(keyword, i)) {
let prevOk = idx == 0 || !isIdentChar(text[idx - 1])
let nextPos = idx + keyword.size
let nextOk = nextPos >= text.size || !isIdentChar(text[nextPos])
if (prevOk && nextOk) {
return idx
}
i = nextPos
} else {
break
}
}
-1
}
/// 去掉条目末尾的 // 与 /* */ 注释。
private func stripComment(item: String): String {
var s = item
if (let Some(li) <- s.indexOf("//")) {
s = s[0..li]
}
if (let Some(bi) <- s.indexOf("/*")) {
s = s[0..bi]
}
trimWs(s)
}
/// 去掉字符串首尾空白。注意:不能用 String.trimStart()/trimEnd() 无参版本
/// (仓颉 1.1.3 实测为 no-op,且 String 无 trim()),这里按字节手写。
private func trimWs(s: String): String {
var start = 0
while (start < s.size && isSpace(s[start])) {
start += 1
}
var end = s.size
while (end > start && isSpace(s[end - 1])) {
end -= 1
}
s[start..end]
}
private func isSpace(c: UInt8): Bool {
c == 32 || c == 9 || c == 10 || c == 13
}
private func isIdentChar(c: UInt8): Bool {
(c >= 97 && c <= 122) || (c >= 65 && c <= 90) || (c >= 48 && c <= 57) || c == 95
}
/// 生成 extend 块:toString() 返回成员名,fromString(String) 按成员名反查(未匹配抛异常)。
/// 必须带 `<: ToString` 子句:仓颉字符串插值 `${x}` 要求实现 ToString 接口,仅有同名方法不够。
private func buildExtend(enumName: String, ctors: ArrayList<String>): String {
var sb = ""
sb += "// 以下 extend 由 @EnumString 宏生成:toString() 返回成员名,fromString(String) 按成员名反查(未匹配抛异常)\n"
sb += "extend ${enumName} <: ToString {\n"
sb += " public func toString(): String {\n"
sb += " match (this) {\n"
for (c in ctors) {
sb += " case ${c} => \"${c}\"\n"
}
sb += " }\n"
sb += " }\n"
sb += "\n"
sb += " public static func fromString(s: String): ${enumName} {\n"
sb += " match (s) {\n"
for (c in ctors) {
sb += " case \"${c}\" => ${c}\n"
}
sb += " case _ => throw Exception(\"${enumName}.fromString: 未知枚举值 '\${s}'\")\n"
sb += " }\n"
sb += " }\n"
sb += "}\n"
sb
}