indexwright 0.1.0
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/CHANGELOG.md +22 -0
- package/LICENSE +201 -0
- package/README.md +184 -0
- package/SPEC.md +386 -0
- package/dist/args.d.ts +26 -0
- package/dist/args.d.ts.map +1 -0
- package/dist/args.js +170 -0
- package/dist/args.js.map +1 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +96 -0
- package/dist/cli.js.map +1 -0
- package/dist/collections.d.ts +6 -0
- package/dist/collections.d.ts.map +1 -0
- package/dist/collections.js +25 -0
- package/dist/collections.js.map +1 -0
- package/dist/format/github.d.ts +13 -0
- package/dist/format/github.d.ts.map +1 -0
- package/dist/format/github.js +63 -0
- package/dist/format/github.js.map +1 -0
- package/dist/format/inline.d.ts +11 -0
- package/dist/format/inline.d.ts.map +1 -0
- package/dist/format/inline.js +15 -0
- package/dist/format/inline.js.map +1 -0
- package/dist/format/json.d.ts +7 -0
- package/dist/format/json.d.ts.map +1 -0
- package/dist/format/json.js +25 -0
- package/dist/format/json.js.map +1 -0
- package/dist/format/text.d.ts +3 -0
- package/dist/format/text.d.ts.map +1 -0
- package/dist/format/text.js +65 -0
- package/dist/format/text.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/key.d.ts +35 -0
- package/dist/key.d.ts.map +1 -0
- package/dist/key.js +76 -0
- package/dist/key.js.map +1 -0
- package/dist/lint.d.ts +22 -0
- package/dist/lint.d.ts.map +1 -0
- package/dist/lint.js +110 -0
- package/dist/lint.js.map +1 -0
- package/dist/parse.d.ts +11 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +88 -0
- package/dist/parse.js.map +1 -0
- package/dist/rules/explicit-name-field.d.ts +13 -0
- package/dist/rules/explicit-name-field.d.ts.map +1 -0
- package/dist/rules/explicit-name-field.js +35 -0
- package/dist/rules/explicit-name-field.js.map +1 -0
- package/dist/rules/field-order-variant.d.ts +10 -0
- package/dist/rules/field-order-variant.d.ts.map +1 -0
- package/dist/rules/field-order-variant.js +60 -0
- package/dist/rules/field-order-variant.js.map +1 -0
- package/dist/rules/index.d.ts +13 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +25 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/quota-headroom.d.ts +10 -0
- package/dist/rules/quota-headroom.d.ts.map +1 -0
- package/dist/rules/quota-headroom.js +40 -0
- package/dist/rules/quota-headroom.js.map +1 -0
- package/dist/rules/scope-mismatch.d.ts +10 -0
- package/dist/rules/scope-mismatch.d.ts.map +1 -0
- package/dist/rules/scope-mismatch.js +45 -0
- package/dist/rules/scope-mismatch.js.map +1 -0
- package/dist/types.d.ts +104 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +14 -0
- package/dist/types.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +11 -0
- package/dist/version.js.map +1 -0
- package/package.json +54 -0
package/dist/lint.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { compareStrings } from './collections.js';
|
|
3
|
+
import { analyse } from './key.js';
|
|
4
|
+
import { MalformedInputError, parseDocument } from './parse.js';
|
|
5
|
+
import { getRule, rules } from './rules/index.js';
|
|
6
|
+
import { RULE_IDS } from './types.js';
|
|
7
|
+
import { VERSION } from './version.js';
|
|
8
|
+
export const DEFAULT_QUOTA = 1000;
|
|
9
|
+
export const DEFAULT_QUOTA_THRESHOLD = 0.8;
|
|
10
|
+
/** Lint documents that are already parsed. The lowest layer; does no I/O. */
|
|
11
|
+
export function lintDocuments(inputs, options = {}) {
|
|
12
|
+
return assemble(inputs.map((input) => ({ file: input.file, document: input.document, error: null })), options);
|
|
13
|
+
}
|
|
14
|
+
/** Lint raw file contents. A file that does not parse becomes an error, not a thrown exception. */
|
|
15
|
+
export function lintTexts(inputs, options = {}) {
|
|
16
|
+
return assemble(inputs.map((input) => {
|
|
17
|
+
try {
|
|
18
|
+
return { file: input.file, document: parseDocument(input.text), error: null };
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
return { file: input.file, document: null, error: describe(error) };
|
|
22
|
+
}
|
|
23
|
+
}), options);
|
|
24
|
+
}
|
|
25
|
+
/** Lint files on disk. Unreadable files are reported the same way malformed ones are. */
|
|
26
|
+
export function lintFiles(paths, options = {}) {
|
|
27
|
+
return assemble(paths.map((file) => {
|
|
28
|
+
try {
|
|
29
|
+
return { file, document: parseDocument(readFileSync(file, 'utf8')), error: null };
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
return { file, document: null, error: describe(error) };
|
|
33
|
+
}
|
|
34
|
+
}), options);
|
|
35
|
+
}
|
|
36
|
+
function assemble(loaded, options) {
|
|
37
|
+
const selected = resolveRules(options.rules);
|
|
38
|
+
const ruleOptions = {
|
|
39
|
+
quota: options.quota ?? DEFAULT_QUOTA,
|
|
40
|
+
quotaThreshold: options.quotaThreshold ?? DEFAULT_QUOTA_THRESHOLD,
|
|
41
|
+
};
|
|
42
|
+
const findings = [];
|
|
43
|
+
const errors = [];
|
|
44
|
+
for (const entry of loaded) {
|
|
45
|
+
if (!entry.document) {
|
|
46
|
+
errors.push({ file: entry.file, message: entry.error ?? 'could not be read' });
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const context = {
|
|
50
|
+
file: entry.file,
|
|
51
|
+
document: entry.document,
|
|
52
|
+
indexes: analyse(entry.document),
|
|
53
|
+
options: ruleOptions,
|
|
54
|
+
};
|
|
55
|
+
for (const id of selected) {
|
|
56
|
+
findings.push(...getRule(id).check(context));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
findings.sort(compareFindings);
|
|
60
|
+
errors.sort((a, b) => compareStrings(a.file, b.file));
|
|
61
|
+
const byRule = {};
|
|
62
|
+
for (const id of selected)
|
|
63
|
+
byRule[id] = 0;
|
|
64
|
+
for (const finding of findings)
|
|
65
|
+
byRule[finding.rule] = (byRule[finding.rule] ?? 0) + 1;
|
|
66
|
+
return {
|
|
67
|
+
version: VERSION,
|
|
68
|
+
files: [...loaded.map((entry) => entry.file)].sort(compareStrings),
|
|
69
|
+
summary: { warnings: findings.length, errors: errors.length, byRule },
|
|
70
|
+
findings,
|
|
71
|
+
errors,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** Preserve the canonical rule order (SPEC §5) whatever order the caller asked in. */
|
|
75
|
+
function resolveRules(requested) {
|
|
76
|
+
if (!requested)
|
|
77
|
+
return rules.map((rule) => rule.id);
|
|
78
|
+
const wanted = new Set(requested);
|
|
79
|
+
return RULE_IDS.filter((id) => wanted.has(id));
|
|
80
|
+
}
|
|
81
|
+
const RULE_ORDER = new Map(RULE_IDS.map((id, position) => [id, position]));
|
|
82
|
+
function compareFindings(a, b) {
|
|
83
|
+
return (compareStrings(a.file, b.file) ||
|
|
84
|
+
(RULE_ORDER.get(a.rule) ?? 0) - (RULE_ORDER.get(b.rule) ?? 0) ||
|
|
85
|
+
compareKeys(a.key, b.key) ||
|
|
86
|
+
compareStrings(a.message, b.message));
|
|
87
|
+
}
|
|
88
|
+
/** A null key sorts first: it belongs to a file-wide finding, which has no index to sort by. */
|
|
89
|
+
function compareKeys(a, b) {
|
|
90
|
+
if (a === b)
|
|
91
|
+
return 0;
|
|
92
|
+
if (a === null)
|
|
93
|
+
return -1;
|
|
94
|
+
if (b === null)
|
|
95
|
+
return 1;
|
|
96
|
+
return compareStrings(a, b);
|
|
97
|
+
}
|
|
98
|
+
function describe(error) {
|
|
99
|
+
if (error instanceof MalformedInputError)
|
|
100
|
+
return error.message;
|
|
101
|
+
const code = error.code;
|
|
102
|
+
if (code === 'ENOENT')
|
|
103
|
+
return 'no such file';
|
|
104
|
+
if (code === 'EISDIR')
|
|
105
|
+
return 'is a directory';
|
|
106
|
+
if (code === 'EACCES')
|
|
107
|
+
return 'permission denied';
|
|
108
|
+
return error instanceof Error ? error.message : String(error);
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=lint.js.map
|
package/dist/lint.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lint.js","sourceRoot":"","sources":["../src/lint.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACnC,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAStC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,MAAM,CAAC,MAAM,aAAa,GAAG,IAAI,CAAC;AAClC,MAAM,CAAC,MAAM,uBAAuB,GAAG,GAAG,CAAC;AAiB3C,6EAA6E;AAC7E,MAAM,UAAU,aAAa,CAAC,MAAgC,EAAE,UAAuB,EAAE;IACvF,OAAO,QAAQ,CACb,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,EACpF,OAAO,CACR,CAAC;AACJ,CAAC;AAED,mGAAmG;AACnG,MAAM,UAAU,SAAS,CAAC,MAA4B,EAAE,UAAuB,EAAE;IAC/E,OAAO,QAAQ,CACb,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;QACnB,IAAI,CAAC;YACH,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,QAAQ,EAAE,aAAa,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;QAChF,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QACtE,CAAC;IACH,CAAC,CAAC,EACF,OAAO,CACR,CAAC;AACJ,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,SAAS,CAAC,KAAwB,EAAE,UAAuB,EAAE;IAC3E,OAAO,QAAQ,CACb,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QACjB,IAAI,CAAC;YACH,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,aAAa,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;QACpF,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1D,CAAC;IACH,CAAC,CAAC,EACF,OAAO,CACR,CAAC;AACJ,CAAC;AAQD,SAAS,QAAQ,CAAC,MAA6B,EAAE,OAAoB;IACnE,MAAM,QAAQ,GAAG,YAAY,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAC7C,MAAM,WAAW,GAAgB;QAC/B,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,aAAa;QACrC,cAAc,EAAE,OAAO,CAAC,cAAc,IAAI,uBAAuB;KAClE,CAAC;IAEF,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,MAAM,MAAM,GAAgB,EAAE,CAAC;IAE/B,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;YACpB,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,KAAK,IAAI,mBAAmB,EAAE,CAAC,CAAC;YAC/E,SAAS;QACX,CAAC;QACD,MAAM,OAAO,GAAG;YACd,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC;YAChC,OAAO,EAAE,WAAW;SACrB,CAAC;QACF,KAAK,MAAM,EAAE,IAAI,QAAQ,EAAE,CAAC;YAC1B,QAAQ,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;QAC/C,CAAC;IACH,CAAC;IAED,QAAQ,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;IAC/B,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IAEtD,MAAM,MAAM,GAA2B,EAAE,CAAC;IAC1C,KAAK,MAAM,EAAE,IAAI,QAAQ;QAAE,MAAM,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;IAC1C,KAAK,MAAM,OAAO,IAAI,QAAQ;QAAE,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;IAEvF,OAAO;QACL,OAAO,EAAE,OAAO;QAChB,KAAK,EAAE,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC;QAClE,OAAO,EAAE,EAAE,QAAQ,EAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE;QACrE,QAAQ;QACR,MAAM;KACP,CAAC;AACJ,CAAC;AAED,sFAAsF;AACtF,SAAS,YAAY,CAAC,SAAwC;IAC5D,IAAI,CAAC,SAAS;QAAE,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACpD,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC;IAClC,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;AACjD,CAAC;AAED,MAAM,UAAU,GAAG,IAAI,GAAG,CAAiB,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;AAE3F,SAAS,eAAe,CAAC,CAAU,EAAE,CAAU;IAC7C,OAAO,CACL,cAAc,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;QAC9B,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7D,WAAW,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC;QACzB,cAAc,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,CACrC,CAAC;AACJ,CAAC;AAED,gGAAgG;AAChG,SAAS,WAAW,CAAC,CAAgB,EAAE,CAAgB;IACrD,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IACtB,IAAI,CAAC,KAAK,IAAI;QAAE,OAAO,CAAC,CAAC,CAAC;IAC1B,IAAI,CAAC,KAAK,IAAI;QAAE,OAAO,CAAC,CAAC;IACzB,OAAO,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAC9B,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,IAAI,KAAK,YAAY,mBAAmB;QAAE,OAAO,KAAK,CAAC,OAAO,CAAC;IAC/D,MAAM,IAAI,GAAI,KAA+B,CAAC,IAAI,CAAC;IACnD,IAAI,IAAI,KAAK,QAAQ;QAAE,OAAO,cAAc,CAAC;IAC7C,IAAI,IAAI,KAAK,QAAQ;QAAE,OAAO,gBAAgB,CAAC;IAC/C,IAAI,IAAI,KAAK,QAAQ;QAAE,OAAO,mBAAmB,CAAC;IAClD,OAAO,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAChE,CAAC"}
|
package/dist/parse.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { IndexDocument } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* A file that is not a usable index declaration. Reported per file and mapped to exit code 2;
|
|
4
|
+
* it never aborts the analysis of the other files (SPEC §4).
|
|
5
|
+
*/
|
|
6
|
+
export declare class MalformedInputError extends Error {
|
|
7
|
+
readonly name = "MalformedInputError";
|
|
8
|
+
}
|
|
9
|
+
export declare function parseDocument(text: string): IndexDocument;
|
|
10
|
+
export declare function validateDocument(raw: unknown): IndexDocument;
|
|
11
|
+
//# sourceMappingURL=parse.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAkB,aAAa,EAAc,MAAM,YAAY,CAAC;AAE5E;;;GAGG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,SAAkB,IAAI,yBAAyB;CAChD;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,CAUzD;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,aAAa,CAa5D"}
|
package/dist/parse.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A file that is not a usable index declaration. Reported per file and mapped to exit code 2;
|
|
3
|
+
* it never aborts the analysis of the other files (SPEC §4).
|
|
4
|
+
*/
|
|
5
|
+
export class MalformedInputError extends Error {
|
|
6
|
+
name = 'MalformedInputError';
|
|
7
|
+
}
|
|
8
|
+
export function parseDocument(text) {
|
|
9
|
+
let raw;
|
|
10
|
+
try {
|
|
11
|
+
raw = JSON.parse(text);
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
// The parser quotes the offending source, which can carry newlines; findings are one line each.
|
|
15
|
+
const detail = error.message.replace(/\s+/g, ' ').trim();
|
|
16
|
+
throw new MalformedInputError(`invalid JSON: ${detail}`);
|
|
17
|
+
}
|
|
18
|
+
return validateDocument(raw);
|
|
19
|
+
}
|
|
20
|
+
export function validateDocument(raw) {
|
|
21
|
+
if (!isObject(raw)) {
|
|
22
|
+
throw new MalformedInputError('the top level must be a JSON object');
|
|
23
|
+
}
|
|
24
|
+
const indexes = raw['indexes'];
|
|
25
|
+
if (indexes === undefined) {
|
|
26
|
+
throw new MalformedInputError('missing "indexes"');
|
|
27
|
+
}
|
|
28
|
+
if (!Array.isArray(indexes)) {
|
|
29
|
+
throw new MalformedInputError('"indexes" must be an array');
|
|
30
|
+
}
|
|
31
|
+
const validated = indexes.map((entry, i) => validateIndex(entry, `indexes[${i}]`));
|
|
32
|
+
return { ...raw, indexes: validated };
|
|
33
|
+
}
|
|
34
|
+
function validateIndex(raw, path) {
|
|
35
|
+
if (!isObject(raw)) {
|
|
36
|
+
throw new MalformedInputError(`${path}: must be an object`);
|
|
37
|
+
}
|
|
38
|
+
const collectionGroup = requireString(raw['collectionGroup'], `${path}: "collectionGroup"`);
|
|
39
|
+
const queryScope = requireString(raw['queryScope'], `${path}: "queryScope"`);
|
|
40
|
+
const fields = raw['fields'];
|
|
41
|
+
if (!Array.isArray(fields)) {
|
|
42
|
+
throw new MalformedInputError(`${path}: "fields" must be an array`);
|
|
43
|
+
}
|
|
44
|
+
if (fields.length === 0) {
|
|
45
|
+
throw new MalformedInputError(`${path}: "fields" must not be empty`);
|
|
46
|
+
}
|
|
47
|
+
// A repeated fieldPath is odd but occurs in real exports, and refusing to analyse a file is the
|
|
48
|
+
// harshest thing a warn-only tool can do. It is carried through; R2 compares multisets, so the
|
|
49
|
+
// repetition does not make "the same fields" ambiguous (SPEC §4, §5).
|
|
50
|
+
const validatedFields = fields.map((entry, i) => validateField(entry, `${path}.fields[${i}]`));
|
|
51
|
+
return { ...raw, collectionGroup, queryScope, fields: validatedFields };
|
|
52
|
+
}
|
|
53
|
+
function validateField(raw, path) {
|
|
54
|
+
if (!isObject(raw)) {
|
|
55
|
+
throw new MalformedInputError(`${path}: must be an object`);
|
|
56
|
+
}
|
|
57
|
+
const fieldPath = requireString(raw['fieldPath'], `${path}: "fieldPath"`);
|
|
58
|
+
const configured = ['order', 'arrayConfig', 'vectorConfig'].filter((name) => raw[name] !== undefined);
|
|
59
|
+
if (configured.length === 0) {
|
|
60
|
+
throw new MalformedInputError(`${path}: needs one of "order", "arrayConfig", or "vectorConfig"`);
|
|
61
|
+
}
|
|
62
|
+
if (configured.length > 1) {
|
|
63
|
+
throw new MalformedInputError(`${path}: declares ${configured.map((name) => `"${name}"`).join(' and ')}; only one is allowed`);
|
|
64
|
+
}
|
|
65
|
+
// Values are not checked against an enumeration, so a direction added by a future Firebase
|
|
66
|
+
// release passes through instead of failing the run (SPEC §4).
|
|
67
|
+
if (raw['order'] !== undefined)
|
|
68
|
+
requireString(raw['order'], `${path}: "order"`);
|
|
69
|
+
if (raw['arrayConfig'] !== undefined)
|
|
70
|
+
requireString(raw['arrayConfig'], `${path}: "arrayConfig"`);
|
|
71
|
+
if (raw['vectorConfig'] !== undefined && !isObject(raw['vectorConfig'])) {
|
|
72
|
+
throw new MalformedInputError(`${path}: "vectorConfig" must be an object`);
|
|
73
|
+
}
|
|
74
|
+
return { ...raw, fieldPath };
|
|
75
|
+
}
|
|
76
|
+
function requireString(value, label) {
|
|
77
|
+
if (typeof value !== 'string') {
|
|
78
|
+
throw new MalformedInputError(value === undefined ? `${label} is missing` : `${label} must be a string`);
|
|
79
|
+
}
|
|
80
|
+
if (value === '') {
|
|
81
|
+
throw new MalformedInputError(`${label} must not be empty`);
|
|
82
|
+
}
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
function isObject(value) {
|
|
86
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=parse.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse.js","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Rule } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* R3 · explicit-name-field — a trailing `__name__` that restates what Firestore appends anyway.
|
|
4
|
+
*
|
|
5
|
+
* Firestore appends the document key implicitly; live exports render it explicitly. A redundant
|
|
6
|
+
* `__name__` in a hand-maintained file is therefore a signature of a value that round-tripped
|
|
7
|
+
* through an export. It does not change the resource, but it makes the file inconsistent and can
|
|
8
|
+
* mask genuine duplicates from naive text comparison.
|
|
9
|
+
*
|
|
10
|
+
* A `__name__` whose direction differs from the implicit default is meaningful and is not flagged.
|
|
11
|
+
*/
|
|
12
|
+
export declare const explicitNameField: Rule;
|
|
13
|
+
//# sourceMappingURL=explicit-name-field.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"explicit-name-field.d.ts","sourceRoot":"","sources":["../../src/rules/explicit-name-field.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAW,IAAI,EAAe,MAAM,aAAa,CAAC;AAE9D;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,EAAE,IAwB/B,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { uniqueSorted } from '../collections.js';
|
|
2
|
+
import { NAME_FIELD } from '../key.js';
|
|
3
|
+
/**
|
|
4
|
+
* R3 · explicit-name-field — a trailing `__name__` that restates what Firestore appends anyway.
|
|
5
|
+
*
|
|
6
|
+
* Firestore appends the document key implicitly; live exports render it explicitly. A redundant
|
|
7
|
+
* `__name__` in a hand-maintained file is therefore a signature of a value that round-tripped
|
|
8
|
+
* through an export. It does not change the resource, but it makes the file inconsistent and can
|
|
9
|
+
* mask genuine duplicates from naive text comparison.
|
|
10
|
+
*
|
|
11
|
+
* A `__name__` whose direction differs from the implicit default is meaningful and is not flagged.
|
|
12
|
+
*/
|
|
13
|
+
export const explicitNameField = {
|
|
14
|
+
id: 'explicit-name-field',
|
|
15
|
+
description: `a trailing ${NAME_FIELD} that restates the direction Firestore appends implicitly`,
|
|
16
|
+
check(context) {
|
|
17
|
+
const flagged = context.indexes.filter((index) => index.redundantNameDirection !== null);
|
|
18
|
+
// Two byte-identical declarations produce one key and would render as two identical lines.
|
|
19
|
+
const keys = uniqueSorted(flagged.map((index) => index.key));
|
|
20
|
+
return keys.map((key) => {
|
|
21
|
+
const index = flagged.find((candidate) => candidate.key === key);
|
|
22
|
+
const direction = index?.redundantNameDirection ?? 'ASCENDING';
|
|
23
|
+
return {
|
|
24
|
+
rule: 'explicit-name-field',
|
|
25
|
+
file: context.file,
|
|
26
|
+
key,
|
|
27
|
+
message: `the index ends with an explicit ${NAME_FIELD} (${direction}), which is the direction ` +
|
|
28
|
+
`Firestore appends implicitly. The entry names the same index either way; writing it ` +
|
|
29
|
+
`the shorter way keeps the file comparable by text.`,
|
|
30
|
+
related: [],
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
//# sourceMappingURL=explicit-name-field.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"explicit-name-field.js","sourceRoot":"","sources":["../../src/rules/explicit-name-field.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAGvC;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAS;IACrC,EAAE,EAAE,qBAAqB;IACzB,WAAW,EAAE,cAAc,UAAU,2DAA2D;IAEhG,KAAK,CAAC,OAAoB;QACxB,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,sBAAsB,KAAK,IAAI,CAAC,CAAC;QACzF,2FAA2F;QAC3F,MAAM,IAAI,GAAG,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;QAE7D,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAW,EAAE;YAC/B,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC;YACjE,MAAM,SAAS,GAAG,KAAK,EAAE,sBAAsB,IAAI,WAAW,CAAC;YAC/D,OAAO;gBACL,IAAI,EAAE,qBAAqB;gBAC3B,IAAI,EAAE,OAAO,CAAC,IAAI;gBAClB,GAAG;gBACH,OAAO,EACL,mCAAmC,UAAU,KAAK,SAAS,4BAA4B;oBACvF,sFAAsF;oBACtF,oDAAoD;gBACtD,OAAO,EAAE,EAAE;aACZ,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Rule } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* R2 · field-order-variant — indexes over the same field set, declared in different orders.
|
|
4
|
+
*
|
|
5
|
+
* Firestore treats a different field order as a different index, so each variant consumes write
|
|
6
|
+
* amplification, storage, and quota on its own. Distinct orderings can be genuinely required; the
|
|
7
|
+
* finding asks for that justification to be recorded, not for a variant to be removed.
|
|
8
|
+
*/
|
|
9
|
+
export declare const fieldOrderVariant: Rule;
|
|
10
|
+
//# sourceMappingURL=field-order-variant.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"field-order-variant.d.ts","sourceRoot":"","sources":["../../src/rules/field-order-variant.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAA0C,IAAI,EAAe,MAAM,aAAa,CAAC;AAK7F;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,EAAE,IAoC/B,CAAC"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { compareStrings, groupBy, uniqueSorted } from '../collections.js';
|
|
2
|
+
/** A byte no collection id or field path can contain, so grouping keys cannot collide. */
|
|
3
|
+
const SEPARATOR = '\u0000';
|
|
4
|
+
/**
|
|
5
|
+
* R2 · field-order-variant — indexes over the same field set, declared in different orders.
|
|
6
|
+
*
|
|
7
|
+
* Firestore treats a different field order as a different index, so each variant consumes write
|
|
8
|
+
* amplification, storage, and quota on its own. Distinct orderings can be genuinely required; the
|
|
9
|
+
* finding asks for that justification to be recorded, not for a variant to be removed.
|
|
10
|
+
*/
|
|
11
|
+
export const fieldOrderVariant = {
|
|
12
|
+
id: 'field-order-variant',
|
|
13
|
+
description: 'indexes over the same field set declared in different field orders',
|
|
14
|
+
check(context) {
|
|
15
|
+
const findings = [];
|
|
16
|
+
const groups = [...groupBy(context.indexes, fieldSetKey)].sort(([a], [b]) => compareStrings(a, b));
|
|
17
|
+
for (const [, indexes] of groups) {
|
|
18
|
+
if (indexes.length < 2)
|
|
19
|
+
continue;
|
|
20
|
+
const orderings = uniqueSorted(indexes.map(fieldSequence));
|
|
21
|
+
// Byte-identical declarations are not "different orders" and are not this rule's subject.
|
|
22
|
+
if (orderings.length < 2)
|
|
23
|
+
continue;
|
|
24
|
+
const first = indexes[0];
|
|
25
|
+
const keys = uniqueSorted(indexes.map((index) => index.key));
|
|
26
|
+
const fieldList = uniqueSorted(first.fields.map((field) => `${field.fieldPath} (${field.direction})`)).join(', ');
|
|
27
|
+
findings.push({
|
|
28
|
+
rule: 'field-order-variant',
|
|
29
|
+
file: context.file,
|
|
30
|
+
key: keys[0] ?? null,
|
|
31
|
+
message: `collectionGroup "${first.collectionGroup}" declares ${orderings.length} field orders ` +
|
|
32
|
+
`over the same ${first.queryScope}-scoped fields — ${fieldList}. Firestore stores each ` +
|
|
33
|
+
`order as a separate index; record which query needs which order.`,
|
|
34
|
+
related: keys.slice(1),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return findings;
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Every boundary is the separator, never `:` or `|`. Directions are not checked against an
|
|
42
|
+
* enumeration (§4), so a direction that itself contains `a:X|b` would otherwise make two different
|
|
43
|
+
* field multisets share one grouping key and fire this rule on indexes that do not share a field
|
|
44
|
+
* set at all.
|
|
45
|
+
*/
|
|
46
|
+
function pair(field) {
|
|
47
|
+
return `${field.fieldPath}${SEPARATOR}${field.direction}`;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Same collectionGroup, same queryScope, same multiset of field/direction pairs — order ignored.
|
|
51
|
+
* Sorting rather than de-duplicating keeps a repeated fieldPath (§4) from collapsing into one.
|
|
52
|
+
*/
|
|
53
|
+
function fieldSetKey(index) {
|
|
54
|
+
const pairs = index.fields.map(pair).sort(compareStrings);
|
|
55
|
+
return [index.collectionGroup, index.queryScope, ...pairs].join(SEPARATOR);
|
|
56
|
+
}
|
|
57
|
+
function fieldSequence(index) {
|
|
58
|
+
return index.fields.map(pair).join(SEPARATOR);
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=field-order-variant.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"field-order-variant.js","sourceRoot":"","sources":["../../src/rules/field-order-variant.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAG1E,0FAA0F;AAC1F,MAAM,SAAS,GAAG,QAAQ,CAAC;AAE3B;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAS;IACrC,EAAE,EAAE,qBAAqB;IACzB,WAAW,EAAE,oEAAoE;IAEjF,KAAK,CAAC,OAAoB;QACxB,MAAM,QAAQ,GAAc,EAAE,CAAC;QAC/B,MAAM,MAAM,GAAG,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAC1E,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,CACrB,CAAC;QAEF,KAAK,MAAM,CAAC,EAAE,OAAO,CAAC,IAAI,MAAM,EAAE,CAAC;YACjC,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;gBAAE,SAAS;YACjC,MAAM,SAAS,GAAG,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC;YAC3D,0FAA0F;YAC1F,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC;gBAAE,SAAS;YAEnC,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,CAAkB,CAAC;YAC1C,MAAM,IAAI,GAAG,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;YAC7D,MAAM,SAAS,GAAG,YAAY,CAC5B,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,SAAS,KAAK,KAAK,CAAC,SAAS,GAAG,CAAC,CACvE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAEb,QAAQ,CAAC,IAAI,CAAC;gBACZ,IAAI,EAAE,qBAAqB;gBAC3B,IAAI,EAAE,OAAO,CAAC,IAAI;gBAClB,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI;gBACpB,OAAO,EACL,oBAAoB,KAAK,CAAC,eAAe,cAAc,SAAS,CAAC,MAAM,gBAAgB;oBACvF,iBAAiB,KAAK,CAAC,UAAU,oBAAoB,SAAS,0BAA0B;oBACxF,kEAAkE;gBACpE,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;aACvB,CAAC,CAAC;QACL,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC;AAEF;;;;;GAKG;AACH,SAAS,IAAI,CAAC,KAAqB;IACjC,OAAO,GAAG,KAAK,CAAC,SAAS,GAAG,SAAS,GAAG,KAAK,CAAC,SAAS,EAAE,CAAC;AAC5D,CAAC;AAED;;;GAGG;AACH,SAAS,WAAW,CAAC,KAAoB;IACvC,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;IAC1D,OAAO,CAAC,KAAK,CAAC,eAAe,EAAE,KAAK,CAAC,UAAU,EAAE,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;AAC7E,CAAC;AAED,SAAS,aAAa,CAAC,KAAoB;IACzC,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;AAChD,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Rule, RuleId } from '../types.js';
|
|
2
|
+
import { explicitNameField } from './explicit-name-field.js';
|
|
3
|
+
import { fieldOrderVariant } from './field-order-variant.js';
|
|
4
|
+
import { quotaHeadroom } from './quota-headroom.js';
|
|
5
|
+
import { scopeMismatch } from './scope-mismatch.js';
|
|
6
|
+
/**
|
|
7
|
+
* Declaration order is R1 → R4 (SPEC §5), and it is also the sort order of findings within a file.
|
|
8
|
+
*/
|
|
9
|
+
export declare const rules: readonly Rule[];
|
|
10
|
+
export declare function getRule(id: RuleId): Rule;
|
|
11
|
+
export declare function isRuleId(value: string): value is RuleId;
|
|
12
|
+
export { explicitNameField, fieldOrderVariant, quotaHeadroom, scopeMismatch };
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rules/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD;;GAEG;AACH,eAAO,MAAM,KAAK,EAAE,SAAS,IAAI,EAKhC,CAAC;AAIF,wBAAgB,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAIxC;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,MAAM,CAEvD;AAED,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,aAAa,EAAE,aAAa,EAAE,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { explicitNameField } from './explicit-name-field.js';
|
|
2
|
+
import { fieldOrderVariant } from './field-order-variant.js';
|
|
3
|
+
import { quotaHeadroom } from './quota-headroom.js';
|
|
4
|
+
import { scopeMismatch } from './scope-mismatch.js';
|
|
5
|
+
/**
|
|
6
|
+
* Declaration order is R1 → R4 (SPEC §5), and it is also the sort order of findings within a file.
|
|
7
|
+
*/
|
|
8
|
+
export const rules = [
|
|
9
|
+
scopeMismatch,
|
|
10
|
+
fieldOrderVariant,
|
|
11
|
+
explicitNameField,
|
|
12
|
+
quotaHeadroom,
|
|
13
|
+
];
|
|
14
|
+
const byId = new Map(rules.map((rule) => [rule.id, rule]));
|
|
15
|
+
export function getRule(id) {
|
|
16
|
+
const rule = byId.get(id);
|
|
17
|
+
if (!rule)
|
|
18
|
+
throw new Error(`unknown rule: ${id}`);
|
|
19
|
+
return rule;
|
|
20
|
+
}
|
|
21
|
+
export function isRuleId(value) {
|
|
22
|
+
return byId.has(value);
|
|
23
|
+
}
|
|
24
|
+
export { explicitNameField, fieldOrderVariant, quotaHeadroom, scopeMismatch };
|
|
25
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/rules/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD;;GAEG;AACH,MAAM,CAAC,MAAM,KAAK,GAAoB;IACpC,aAAa;IACb,iBAAiB;IACjB,iBAAiB;IACjB,aAAa;CACd,CAAC;AAEF,MAAM,IAAI,GAAG,IAAI,GAAG,CAAe,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;AAEzE,MAAM,UAAU,OAAO,CAAC,EAAU;IAChC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC1B,IAAI,CAAC,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,iBAAiB,EAAE,EAAE,CAAC,CAAC;IAClD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,KAAa;IACpC,OAAO,IAAI,CAAC,GAAG,CAAC,KAAe,CAAC,CAAC;AACnC,CAAC;AAED,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,aAAa,EAAE,aAAa,EAAE,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Rule } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* R4 · quota-headroom — the file is approaching the per-database composite index limit.
|
|
4
|
+
*
|
|
5
|
+
* The limit is reached gradually and silently; the first symptom is a failed index creation at
|
|
6
|
+
* deploy time, which is a poor moment to discover it. Reporting headroom continuously makes the
|
|
7
|
+
* trend visible.
|
|
8
|
+
*/
|
|
9
|
+
export declare const quotaHeadroom: Rule;
|
|
10
|
+
//# sourceMappingURL=quota-headroom.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quota-headroom.d.ts","sourceRoot":"","sources":["../../src/rules/quota-headroom.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAW,IAAI,EAAe,MAAM,aAAa,CAAC;AAE9D;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,EAAE,IA+B3B,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* R4 · quota-headroom — the file is approaching the per-database composite index limit.
|
|
3
|
+
*
|
|
4
|
+
* The limit is reached gradually and silently; the first symptom is a failed index creation at
|
|
5
|
+
* deploy time, which is a poor moment to discover it. Reporting headroom continuously makes the
|
|
6
|
+
* trend visible.
|
|
7
|
+
*/
|
|
8
|
+
export const quotaHeadroom = {
|
|
9
|
+
id: 'quota-headroom',
|
|
10
|
+
description: 'the declared index count is close to the per-database limit',
|
|
11
|
+
check(context) {
|
|
12
|
+
const { quota, quotaThreshold } = context.options;
|
|
13
|
+
const count = context.document.indexes.length;
|
|
14
|
+
if (count <= quota * quotaThreshold)
|
|
15
|
+
return [];
|
|
16
|
+
const remaining = quota - count;
|
|
17
|
+
const headroom = remaining > 0
|
|
18
|
+
? `${remaining} left before the limit`
|
|
19
|
+
: remaining === 0
|
|
20
|
+
? 'the limit is exactly reached'
|
|
21
|
+
: `${-remaining} over the limit`;
|
|
22
|
+
const percent = formatPercent(quotaThreshold);
|
|
23
|
+
return [
|
|
24
|
+
{
|
|
25
|
+
rule: 'quota-headroom',
|
|
26
|
+
file: context.file,
|
|
27
|
+
// The subject is the file, not any one index (SPEC §5, R4).
|
|
28
|
+
key: null,
|
|
29
|
+
message: `${count} composite indexes declared, above ${percent} of the ${quota}-index limit ` +
|
|
30
|
+
`(${headroom}). Index creation fails once the limit is reached, at deploy time.`,
|
|
31
|
+
related: [],
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
function formatPercent(fraction) {
|
|
37
|
+
const percent = fraction * 100;
|
|
38
|
+
return `${Number.isInteger(percent) ? percent : percent.toFixed(1)}%`;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=quota-headroom.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quota-headroom.js","sourceRoot":"","sources":["../../src/rules/quota-headroom.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,aAAa,GAAS;IACjC,EAAE,EAAE,gBAAgB;IACpB,WAAW,EAAE,6DAA6D;IAE1E,KAAK,CAAC,OAAoB;QACxB,MAAM,EAAE,KAAK,EAAE,cAAc,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC;QAClD,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC;QAC9C,IAAI,KAAK,IAAI,KAAK,GAAG,cAAc;YAAE,OAAO,EAAE,CAAC;QAE/C,MAAM,SAAS,GAAG,KAAK,GAAG,KAAK,CAAC;QAChC,MAAM,QAAQ,GACZ,SAAS,GAAG,CAAC;YACX,CAAC,CAAC,GAAG,SAAS,wBAAwB;YACtC,CAAC,CAAC,SAAS,KAAK,CAAC;gBACf,CAAC,CAAC,8BAA8B;gBAChC,CAAC,CAAC,GAAG,CAAC,SAAS,iBAAiB,CAAC;QACvC,MAAM,OAAO,GAAG,aAAa,CAAC,cAAc,CAAC,CAAC;QAE9C,OAAO;YACL;gBACE,IAAI,EAAE,gBAAgB;gBACtB,IAAI,EAAE,OAAO,CAAC,IAAI;gBAClB,4DAA4D;gBAC5D,GAAG,EAAE,IAAI;gBACT,OAAO,EACL,GAAG,KAAK,sCAAsC,OAAO,WAAW,KAAK,eAAe;oBACpF,IAAI,QAAQ,oEAAoE;gBAClF,OAAO,EAAE,EAAE;aACZ;SACF,CAAC;IACJ,CAAC;CACF,CAAC;AAEF,SAAS,aAAa,CAAC,QAAgB;IACrC,MAAM,OAAO,GAAG,QAAQ,GAAG,GAAG,CAAC;IAC/B,OAAO,GAAG,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC;AACxE,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Rule } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* R1 · scope-mismatch — one `collectionGroup` declaring more than one `queryScope`.
|
|
4
|
+
*
|
|
5
|
+
* A `COLLECTION`-scoped index does not serve a collection-group query, and vice versa, so an
|
|
6
|
+
* odd-one-out is frequently a mistake that only surfaces as a production `FAILED_PRECONDITION`.
|
|
7
|
+
* Querying a collection both ways is legitimate, which is why this warns rather than fails.
|
|
8
|
+
*/
|
|
9
|
+
export declare const scopeMismatch: Rule;
|
|
10
|
+
//# sourceMappingURL=scope-mismatch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scope-mismatch.d.ts","sourceRoot":"","sources":["../../src/rules/scope-mismatch.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAA0B,IAAI,EAAe,MAAM,aAAa,CAAC;AAE7E;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,EAAE,IA6C3B,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { compareStrings, groupBy, uniqueSorted } from '../collections.js';
|
|
2
|
+
/**
|
|
3
|
+
* R1 · scope-mismatch — one `collectionGroup` declaring more than one `queryScope`.
|
|
4
|
+
*
|
|
5
|
+
* A `COLLECTION`-scoped index does not serve a collection-group query, and vice versa, so an
|
|
6
|
+
* odd-one-out is frequently a mistake that only surfaces as a production `FAILED_PRECONDITION`.
|
|
7
|
+
* Querying a collection both ways is legitimate, which is why this warns rather than fails.
|
|
8
|
+
*/
|
|
9
|
+
export const scopeMismatch = {
|
|
10
|
+
id: 'scope-mismatch',
|
|
11
|
+
description: 'one collectionGroup declares more than one queryScope',
|
|
12
|
+
check(context) {
|
|
13
|
+
const findings = [];
|
|
14
|
+
const byCollectionGroup = [...groupBy(context.indexes, (index) => index.collectionGroup)].sort(([a], [b]) => compareStrings(a, b));
|
|
15
|
+
for (const [collectionGroup, indexes] of byCollectionGroup) {
|
|
16
|
+
const byScope = groupBy(indexes, (index) => index.queryScope);
|
|
17
|
+
if (byScope.size < 2)
|
|
18
|
+
continue;
|
|
19
|
+
// Fewest indexes first; ties broken by scope name so the choice never depends on file order.
|
|
20
|
+
const ranked = [...byScope].sort(([aScope, a], [bScope, b]) => a.length - b.length || compareStrings(aScope, bScope));
|
|
21
|
+
const [flaggedScope, flaggedIndexes] = ranked[0];
|
|
22
|
+
const runnerUp = ranked[1];
|
|
23
|
+
const isStrictMinority = flaggedIndexes.length < runnerUp[1].length;
|
|
24
|
+
const census = [...byScope]
|
|
25
|
+
.sort(([a], [b]) => compareStrings(a, b))
|
|
26
|
+
.map(([scope, list]) => `${scope} (${list.length})`)
|
|
27
|
+
.join(', ');
|
|
28
|
+
const keys = uniqueSorted(flaggedIndexes.map((index) => index.key));
|
|
29
|
+
const verdict = isStrictMinority
|
|
30
|
+
? `${flaggedScope} is the minority`
|
|
31
|
+
: 'No scope is in the minority';
|
|
32
|
+
findings.push({
|
|
33
|
+
rule: 'scope-mismatch',
|
|
34
|
+
file: context.file,
|
|
35
|
+
key: keys[0] ?? null,
|
|
36
|
+
message: `collectionGroup "${collectionGroup}" declares more than one queryScope: ${census}. ` +
|
|
37
|
+
`${verdict}; confirm that each scope is intended, since an index of one scope does not ` +
|
|
38
|
+
`serve a query of the other.`,
|
|
39
|
+
related: keys.slice(1),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return findings;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=scope-mismatch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scope-mismatch.js","sourceRoot":"","sources":["../../src/rules/scope-mismatch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAG1E;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,aAAa,GAAS;IACjC,EAAE,EAAE,gBAAgB;IACpB,WAAW,EAAE,uDAAuD;IAEpE,KAAK,CAAC,OAAoB;QACxB,MAAM,QAAQ,GAAc,EAAE,CAAC;QAC/B,MAAM,iBAAiB,GAAG,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC,CAAC,IAAI,CAC5F,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,CACnC,CAAC;QAEF,KAAK,MAAM,CAAC,eAAe,EAAE,OAAO,CAAC,IAAI,iBAAiB,EAAE,CAAC;YAC3D,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;YAC9D,IAAI,OAAO,CAAC,IAAI,GAAG,CAAC;gBAAE,SAAS;YAE/B,6FAA6F;YAC7F,MAAM,MAAM,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,IAAI,CAC9B,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,IAAI,cAAc,CAAC,MAAM,EAAE,MAAM,CAAC,CACpF,CAAC;YACF,MAAM,CAAC,YAAY,EAAE,cAAc,CAAC,GAAG,MAAM,CAAC,CAAC,CAA8B,CAAC;YAC9E,MAAM,QAAQ,GAAG,MAAM,CAAC,CAAC,CAA8B,CAAC;YACxD,MAAM,gBAAgB,GAAG,cAAc,CAAC,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;YAEpE,MAAM,MAAM,GAAG,CAAC,GAAG,OAAO,CAAC;iBACxB,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;iBACxC,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,GAAG,KAAK,KAAK,IAAI,CAAC,MAAM,GAAG,CAAC;iBACnD,IAAI,CAAC,IAAI,CAAC,CAAC;YACd,MAAM,IAAI,GAAG,YAAY,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;YAEpE,MAAM,OAAO,GAAG,gBAAgB;gBAC9B,CAAC,CAAC,GAAG,YAAY,kBAAkB;gBACnC,CAAC,CAAC,6BAA6B,CAAC;YAClC,QAAQ,CAAC,IAAI,CAAC;gBACZ,IAAI,EAAE,gBAAgB;gBACtB,IAAI,EAAE,OAAO,CAAC,IAAI;gBAClB,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI;gBACpB,OAAO,EACL,oBAAoB,eAAe,wCAAwC,MAAM,IAAI;oBACrF,GAAG,OAAO,8EAA8E;oBACxF,6BAA6B;gBAC/B,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;aACvB,CAAC,CAAC;QACL,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
|