Skip to content

Linter 規則

linter 回報 emitter 會接受的錯誤。這些寫法都產出合法的 AsyncAPI 文件,但都不是作者要的那份文件。

規則在語意分析階段執行,不會執行 emitter。所以裝了 TypeSpec 編輯器擴充之後,規則在你打字時就會顯示。沒有設定任何 emitter 時,規則一樣會回報。

規則與診斷的差別

兩者都出現在 compiler 輸出裡,但不是同一種東西。

診斷lint 規則
何時執行emitter 執行時語意分析階段
何時啟用一律啟用只在你開啟時
嚴重度錯誤或警告只能是警告
代碼tsp-asyncapi/<code>tsp-asyncapi/<rule>

診斷一定會回報。規則要自己開,所以規則可以對不算錯的寫法提出警告。

開啟 linter

tspconfig.yaml 加一段 linter

yaml
emit:
  - "tsp-asyncapi"

linter:
  extends:
    - "tsp-asyncapi/recommended"

recommended 收錄會抓到錯誤的規則。一條規則要進去,條件是「你幾乎確定不是這個意思」。

只開啟單一規則:

yaml
linter:
  enable:
    "tsp-asyncapi/unused-security-scheme": true

要從繼承的規則集裡關掉某一條,要寫明理由:

yaml
linter:
  extends:
    - "tsp-asyncapi/recommended"
  disable:
    "tsp-asyncapi/channel-without-operation": "本服務描述它不提供的 channel。"

all 會啟用全部規則,包含不在 recommended 裡的那些。

規則

每一條規則都是警告。lint 規則無法是錯誤。

missing-service

recommended 內。

This program declares AsyncAPI content but no @service. The emitted document falls back to the title "AsyncAPI Document" and the version "0.0.0".

info.titleinfo.version 是必填欄位。沒有任何 namespace 標上 @service 時,emitter 用佔位值填這兩個欄位。文件是合法的,而那兩個值夠像真的,review 時很容易放過。

規則需要有 channel 才會回報。應用程式會宣告 channel,只放 @message model 的共用 library 不會。那種 library 本來就刻意沒有自己的 @service

typespec
// 會回報。
namespace Orders;

@message
model OrderCreated {
  id: string;
}

修法: 在描述這個應用程式的 namespace 上加 @service

channel-without-operation

recommended 內。

Channel '<id>' carries messages but no operation marked @send or @receive.

@send@receive 決定一個 operation 會不會進入 operations。message 要抵達 channel 則不需要這兩個 decorator。emitter 無論如何都會讀 channel 周圍那些 operation 的簽章。

所以少寫這兩個 decorator 的 channel,照樣帶著 message 被輸出。文件裡則沒有任何地方說明誰發送、誰接收。

typespec
// 會回報。publish 把 OrderCreated 帶到 channel 上,
// 但文件沒有描述任何流量。
@channel("orders.created")
interface OrderChannel {
  op publish(event: OrderCreated): void;
}

兩種情況下規則保持安靜。channel 沒有任何 message 時,由 channel-no-messages 負責。channel 只透過 @replyChannel 接收回覆時,它本來就不擁有 operation。

修法: 在這個 channel 的 operation 上加 @send@receive

operation-without-message

recommended 內。

Operation '<name>' names no @message model, so the emitted operation carries no messages field.

AsyncAPI 把「沒有 messages 欄位的 operation」讀成承載該 channel 的所有 message。空陣列的意思相反,所以 emitter 選擇省略欄位而不是輸出空陣列。

沒有被 @message 標記的 model 是 payload 或 channel 參數,不貢獻任何 message。只由這種 model 組成的 operation,因此宣稱了它 channel 上的每一個 message。

typespec
// 會回報。publish 沒有指名任何 message,於是它宣稱了 OrderCreated。
@channel("orders.{id}")
interface OrderChannel {
  @receive
  op consume(event: OrderCreated): void;

  @send
  op publish(id: string): void;
}

修法: 在這個 operation 承載的 model 上加 @message

server-protocol-mismatch

recommended 內。

This '<binding>' server binding names a protocol no server here speaks.

server 層的 binding 記錄在 namespace 上,所以該 namespace 宣告的每一個 @server 都會拿到它。binding 與其中任何一個 server 都對不上時,這條連線就被描述錯了。文件會用另一個通訊協定的設定去描述它。

typespec
// 會回報。文件說是 MQTT,設定卻是 Kafka 的。
@service(#{ title: "Orders" })
@server("prod", #{ host: "mqtt.example.com:1883", protocol: "mqtt" })
@kafkaServer(#{ schemaRegistryUrl: "https://registry.example.com" })
namespace Orders;

同一個通訊協定的加密傳輸會被接受。kafka-secure 屬於 Kafka,smf 屬於 Solace。

namespace 可以宣告多種通訊協定的 server。只要其中一個對得上,規則就不回報。binding 會送到全部的 server,而那正是原始碼要求的行為。

修法: 改掉 @serverprotocol,或移除這個 binding。

protobuf-content-type-undeclared

recommended 裡。只在 preview-features 指名 protobuf 時執行。

Message '<name>' declares the content type '<contentType>', but nothing gives it a Protobuf payload.

@contentType 說明傳輸中的位元組如何編碼,並不產生那些位元組。所以一個 message 可以指名 Protobuf 媒體型別,而它的 payload 仍然是從 TypeSpec model 產生的。這樣的文件等於叫消費端用 Protobuf 解碼,卻用 JSON Schema 描述同一批位元組。

typespec
// 會回報。content type 說是 Protobuf,payload 卻是 JSON Schema。
@Protobuf.package({ name: "com.example.orders" })
namespace Orders {
  @message
  @contentType("application/vnd.google.protobuf")
  model OrderPlaced {
    id: string;
  }
}

兩種寫法會給 message 一份 Protobuf payload,任一種都會讓這條規則安靜。@Protobuf.message 加上每個屬性的 @Protobuf.field,讓預覽功能算繪出 schema。@rawPayload 則帶著作者自己寫的文字。

規則讀媒體型別本身,忽略分號後面的內容,所以 ;version=3 這種參數不會遮住問題。application/vnd.google.protobufapplication/x-protobufapplication/protobufapplication/octet-stream+protobuf 都算。

修法: 加上 @Protobuf.message 與每個屬性的 @Protobuf.field,或用 @rawPayload 寫下 schema。

avro-content-type-undeclared

recommended 裡。只在 preview-features 指名 avro 時執行。

Message '<name>' declares the content type '<contentType>', but nothing gives it an Avro payload.

@contentType 說明傳輸中的位元組如何編碼,並不產生那些位元組。所以一個 message 可以指名 Avro 媒體型別,而它的 payload 仍然是從 TypeSpec model 產生的。這樣的文件等於叫消費端用 Avro 解碼,卻用 JSON Schema 描述同一批位元組。

typespec
// 會回報。content type 說是 Avro,payload 卻是 JSON Schema。
@Avro.avroNamespace("com.example.orders")
namespace Orders {
  @message
  @contentType("application/vnd.apache.avro")
  model OrderPlaced {
    id: string;
  }
}

兩種寫法會給 message 一份 Avro payload,任一種都會讓這條規則安靜。@Avro.avroRecord 讓預覽功能算繪出 schema。@rawPayload 則帶著作者自己寫的 schema。

規則讀媒體型別本身,忽略分號後面的內容,所以 ;version=1.9.0 這種參數不會遮住問題。application/vnd.apache.avroapplication/vnd.apache.avro+jsonapplication/vnd.apache.avro+yaml 都算。

修法: 加上 @Avro.avroRecord,或用 @rawPayload 寫下 schema。

unused-security-scheme

不在 recommended 內,要指名開啟。

Security scheme '<name>' is declared but no @useSecurity names it.

emitter 會把每一個 @securityScheme 寫進 components.securitySchemes,不管有沒有東西指名它。把 scheme 掛到 server 上的是 @useSecurity

這條規則不在 recommended 裡。「宣告了卻沒有人指名的 scheme」是一種真實的意圖。components.securitySchemes 是一份登錄表。文件可以先公布一種驗證方式,即使目前沒有 channel 要求它。

typespec
// 開啟這條規則時會回報。沒有任何地方要求 kafka-scram。
@service(#{ title: "Orders" })
@securityScheme("kafka-scram", #{ type: "scramSha512" })
@server("prod", #{ host: "kafka.example.com:9092", protocol: "kafka" })
namespace Orders;

修法: 在有宣告 server 的 namespace 上加 @useSecurity,或移除這個 scheme。