bk-schema-checker 0.1.0 → 0.1.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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/src/ts.js +117 -6
package/README.md CHANGED
@@ -14,7 +14,7 @@
14
14
 
15
15
  请求参数不看 mock 读了什么,而看 services 调用的参数类型:GET 的第二个参数是查询参数,POST、PUT、PATCH、DELETE 的第二个参数是请求体。路径参数来自 Django 的 `<str:system_id>` 这类转换器。
16
16
 
17
- 后台用 Python 标准库 `ast` 静态读取,包括字段继承、自定义字段子类、`SerializerMethodField` 的返回注解,以及能静态求值的 `choices`。前端 `tsconfig.json` 的 `paths` 用来展开 `@common/const` 这类别名;`export enum`、`export interface` 和 `` `${Enum}` `` 会展开成具体类型再比较。
17
+ 后台用 Python 标准库 `ast` 静态读取,包括字段继承、自定义字段子类、`SerializerMethodField` 的返回注解,以及能静态求值的 `choices`。前端 `tsconfig.json` 的 `paths` 用来展开 `@common/const` 这类别名;`export enum`、`export interface`(含 `extends`)、`A & { extra }` 交叉类型和 `` `${Enum}` `` 会展开成具体类型再比较。
18
18
 
19
19
  ## 环境要求
20
20
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bk-schema-checker",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "对照前端 services、Django REST 序列化器,以及可选的 bk-local-mock 响应,做接口协议检查。不启动 Django。",
5
5
  "type": "module",
6
6
  "author": "austinqli",
package/src/ts.js CHANGED
@@ -480,7 +480,10 @@ function parseTypeExport(source, cursor, mod) {
480
480
  if (interfaceLike || source[bodyAt] === "{") {
481
481
  const open = source.indexOf("{", bodyAt);
482
482
  const block = sliceBalanced(source, open);
483
- mod.types.set(ident.name, { kind: "source", source: source.slice(open, block.end + 1) });
483
+ const body = source.slice(open, block.end + 1);
484
+ const parents = interfaceLike ? parseInterfaceExtends(source, bodyAt, open) : [];
485
+ const composed = parents.length ? `${parents.join(" & ")} & ${body}` : body;
486
+ mod.types.set(ident.name, { kind: "source", source: composed });
484
487
  return block.end + 1;
485
488
  }
486
489
  if (source[bodyAt] === "=") {
@@ -492,6 +495,31 @@ function parseTypeExport(source, cursor, mod) {
492
495
  return skipStatement(source, cursor).end;
493
496
  }
494
497
 
498
+ function parseInterfaceExtends(source, start, open) {
499
+ const text = source.slice(start, open).trim();
500
+ if (!/^extends\b/.test(text)) {
501
+ return [];
502
+ }
503
+ const names = [];
504
+ let cursor = skipSpace(text, "extends".length);
505
+ while (cursor < text.length) {
506
+ const ident = readIdentifier(text, cursor);
507
+ if (!ident) {
508
+ break;
509
+ }
510
+ names.push(ident.name);
511
+ cursor = skipSpace(text, ident.end);
512
+ if (text[cursor] === "<") {
513
+ const generic = sliceAngles(text, cursor);
514
+ cursor = skipSpace(text, generic ? generic.end + 1 : cursor + 1);
515
+ }
516
+ if (text[cursor] === ",") {
517
+ cursor = skipSpace(text, cursor + 1);
518
+ }
519
+ }
520
+ return names;
521
+ }
522
+
495
523
  function parseEnumExport(source, cursor, mod) {
496
524
  const head = source.slice(cursor, cursor + 24);
497
525
  const keyword = /^export\s+const\s+enum\b/.test(head) ? "export const enum" : "export enum";
@@ -1747,6 +1775,17 @@ function skeleton(type) {
1747
1775
  value.fields = Object.fromEntries(type.props.map((prop) => [prop.name, skeleton(prop.type)]));
1748
1776
  return value;
1749
1777
  }
1778
+ if (type.kind === "intersection") {
1779
+ const value = new Partial("object");
1780
+ value.fields = {};
1781
+ for (const item of type.types) {
1782
+ const next = skeleton(item);
1783
+ if (next instanceof Partial && next.fields) {
1784
+ Object.assign(value.fields, next.fields);
1785
+ }
1786
+ }
1787
+ return value;
1788
+ }
1750
1789
  return new Partial();
1751
1790
  }
1752
1791
 
@@ -1764,7 +1803,7 @@ function parseTypeAt(source, cursor) {
1764
1803
  }
1765
1804
 
1766
1805
  function parseUnionType(source, cursor) {
1767
- let current = parsePrimaryType(source, cursor);
1806
+ let current = parseIntersectionType(source, cursor);
1768
1807
  const types = [current.type];
1769
1808
  let end = current.end;
1770
1809
  while (true) {
@@ -1772,13 +1811,29 @@ function parseUnionType(source, cursor) {
1772
1811
  if (source[next] !== "|") {
1773
1812
  break;
1774
1813
  }
1775
- current = parsePrimaryType(source, next + 1);
1814
+ current = parseIntersectionType(source, next + 1);
1776
1815
  types.push(current.type);
1777
1816
  end = current.end;
1778
1817
  }
1779
1818
  return { type: types.length === 1 ? types[0] : { kind: "union", types }, end };
1780
1819
  }
1781
1820
 
1821
+ function parseIntersectionType(source, cursor) {
1822
+ let current = parsePrimaryType(source, cursor);
1823
+ const types = [current.type];
1824
+ let end = current.end;
1825
+ while (true) {
1826
+ const next = skipSpace(source, end);
1827
+ if (source[next] !== "&") {
1828
+ break;
1829
+ }
1830
+ current = parsePrimaryType(source, next + 1);
1831
+ types.push(current.type);
1832
+ end = current.end;
1833
+ }
1834
+ return { type: types.length === 1 ? types[0] : { kind: "intersection", types }, end };
1835
+ }
1836
+
1782
1837
  function parsePrimaryType(source, cursor) {
1783
1838
  const start = skipSpace(source, cursor);
1784
1839
  if (source[start] === "{") {
@@ -2004,16 +2059,19 @@ function splitTypeProps(source) {
2004
2059
  export function resolveTsType(type, mod, index, seen = new Set()) {
2005
2060
  if (!type || type.kind !== "ref" && type.kind !== "index" && type.kind !== "parameters") {
2006
2061
  if (type?.kind === "array") {
2007
- return { kind: "array", item: resolveTsType(type.item, mod, index, seen) };
2062
+ return { kind: "array", item: resolveTsType(type.item, mod, index, new Set(seen)) };
2008
2063
  }
2009
2064
  if (type?.kind === "union") {
2010
- return { kind: "union", types: type.types.map((item) => resolveTsType(item, mod, index, seen)) };
2065
+ return { kind: "union", types: type.types.map((item) => resolveTsType(item, mod, index, new Set(seen))) };
2066
+ }
2067
+ if (type?.kind === "intersection") {
2068
+ return mergeResolvedTypes(type.types.map((item) => resolveTsType(item, mod, index, new Set(seen))));
2011
2069
  }
2012
2070
  if (type?.kind === "object") {
2013
2071
  return {
2014
2072
  kind: "object",
2015
2073
  open: Boolean(type.open),
2016
- props: type.props.map((prop) => ({ ...prop, type: resolveTsType(prop.type, mod, index, seen) })),
2074
+ props: type.props.map((prop) => ({ ...prop, type: resolveTsType(prop.type, mod, index, new Set(seen)) })),
2017
2075
  };
2018
2076
  }
2019
2077
  return type || { kind: "any" };
@@ -2044,6 +2102,59 @@ export function resolveTsType(type, mod, index, seen = new Set()) {
2044
2102
  return resolved;
2045
2103
  }
2046
2104
 
2105
+ function mergeResolvedTypes(types) {
2106
+ const parts = types.flatMap((type) => (type?.kind === "intersection" ? type.types : [type])).filter(Boolean);
2107
+ const unions = parts.filter((type) => type.kind === "union");
2108
+ if (unions.length) {
2109
+ const rest = parts.filter((type) => type.kind !== "union");
2110
+ return {
2111
+ kind: "union",
2112
+ types: unions[0].types.map((item) => mergeResolvedTypes([item, ...unions.slice(1), ...rest])),
2113
+ };
2114
+ }
2115
+ const objects = parts.filter((type) => type.kind === "object");
2116
+ if (objects.length) {
2117
+ return mergeObjectTypes(objects);
2118
+ }
2119
+ const concrete = parts.filter((type) => type.kind !== "any" && type.kind !== "null");
2120
+ return concrete[0] || parts[0] || { kind: "any" };
2121
+ }
2122
+
2123
+ function mergeObjectTypes(objects) {
2124
+ const props = [];
2125
+ let open = false;
2126
+ for (const object of objects) {
2127
+ open = open || Boolean(object.open);
2128
+ for (const prop of object.props || []) {
2129
+ const index = props.findIndex((item) => item.name === prop.name);
2130
+ if (index === -1) {
2131
+ props.push({ ...prop });
2132
+ continue;
2133
+ }
2134
+ const prev = props[index];
2135
+ props[index] = {
2136
+ name: prop.name,
2137
+ optional: Boolean(prev.optional && prop.optional),
2138
+ type: mergePropertyType(prev.type, prop.type),
2139
+ };
2140
+ }
2141
+ }
2142
+ return { kind: "object", props, open };
2143
+ }
2144
+
2145
+ function mergePropertyType(left, right) {
2146
+ if (!left || left.kind === "any") {
2147
+ return right;
2148
+ }
2149
+ if (!right || right.kind === "any") {
2150
+ return left;
2151
+ }
2152
+ if (left.kind === "object" && right.kind === "object") {
2153
+ return mergeObjectTypes([left, right]);
2154
+ }
2155
+ return right;
2156
+ }
2157
+
2047
2158
  function lookupType(mod, name, index, stack = new Set()) {
2048
2159
  if (!mod || stack.has(mod.file)) {
2049
2160
  return null;