@rdjperron/lint 0.7.0 → 0.8.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/README.md +198 -45
- package/dist/{config-CD2iCM6L.mjs → config-BCudqv3_.mjs} +58 -2
- package/dist/config-BCudqv3_.mjs.map +1 -0
- package/dist/config.d.mts +38 -1
- package/dist/config.mjs +2 -2
- package/dist/index.mjs +691 -7
- package/dist/index.mjs.map +1 -1
- package/dist/verify.mjs +122 -50
- package/dist/verify.mjs.map +1 -1
- package/fixtures/README.md +5 -0
- package/fixtures/convex/explicit-table-ids.fixture.ts +4 -0
- package/fixtures/convex-checked-constructors.fixture.ts +4 -0
- package/fixtures/convex-require-validators.fixture.ts +4 -0
- package/package.json +16 -14
- package/dist/config-CD2iCM6L.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { defineRule, eslintCompatPlugin } from "@oxlint/plugins";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
2
3
|
//#region src/anti-slop/shared/array-method.ts
|
|
3
4
|
/** Unwrap syntax-only wrappers when inspecting array methods and accumulator references. */
|
|
4
5
|
function unwrapArrayExpression(node) {
|
|
@@ -633,7 +634,7 @@ function functionParameterBindingName(parameter, sourceCode) {
|
|
|
633
634
|
//#endregion
|
|
634
635
|
//#region src/anti-slop/shared/scope.ts
|
|
635
636
|
/** Resolve an identifier to its binding by walking lexical scopes upward. */
|
|
636
|
-
function resolveVariable(sourceCode, identifier) {
|
|
637
|
+
function resolveVariable$1(sourceCode, identifier) {
|
|
637
638
|
let scope = sourceCode.getScope(identifier);
|
|
638
639
|
while (scope !== null) {
|
|
639
640
|
const variable = scope.set.get(identifier.name);
|
|
@@ -661,7 +662,7 @@ function hasKnownEvidence(sourceCode, expression, visitedVariables = /* @__PURE_
|
|
|
661
662
|
if (isKnownEvidenceExpression(expression)) return true;
|
|
662
663
|
const unwrapped = unwrapExpression(expression);
|
|
663
664
|
if (unwrapped.type !== "Identifier") return false;
|
|
664
|
-
const variable = resolveVariable(sourceCode, unwrapped);
|
|
665
|
+
const variable = resolveVariable$1(sourceCode, unwrapped);
|
|
665
666
|
if (variable === null || visitedVariables.has(variable)) return false;
|
|
666
667
|
const declarator = variableDeclarator$1(variable);
|
|
667
668
|
if (declarator === null || declarator.init === null || !isStableConstVariable(variable, declarator)) return false;
|
|
@@ -675,7 +676,7 @@ function localFunctionForCall(sourceCode, callee) {
|
|
|
675
676
|
const unwrapped = unwrapExpression(callee);
|
|
676
677
|
if (isFunctionExpression(unwrapped)) return unwrapped;
|
|
677
678
|
if (unwrapped.type !== "Identifier") return null;
|
|
678
|
-
const variable = resolveVariable(sourceCode, unwrapped);
|
|
679
|
+
const variable = resolveVariable$1(sourceCode, unwrapped);
|
|
679
680
|
if (variable === null || variable.defs.length !== 1) return null;
|
|
680
681
|
const [definition] = variable.defs;
|
|
681
682
|
if (definition === void 0) return null;
|
|
@@ -707,7 +708,7 @@ function hasKnownCallArgumentEvidence(sourceCode, expression, environment, visit
|
|
|
707
708
|
return returnType !== void 0 && hasInformativeType(returnType, environment);
|
|
708
709
|
}
|
|
709
710
|
if (expression.type !== "Identifier") return isKnownEvidenceExpression(expression);
|
|
710
|
-
const variable = resolveVariable(sourceCode, expression);
|
|
711
|
+
const variable = resolveVariable$1(sourceCode, expression);
|
|
711
712
|
if (variable === null || visitedVariables.has(variable)) return false;
|
|
712
713
|
const annotation = variableTypeAnnotation(sourceCode, variable);
|
|
713
714
|
if (annotation !== null) return hasInformativeType(annotation.typeAnnotation, environment);
|
|
@@ -798,7 +799,7 @@ const noKnownValueWideningRule = defineRule({
|
|
|
798
799
|
},
|
|
799
800
|
AssignmentExpression(node) {
|
|
800
801
|
if (node.operator !== "=" || node.left.type !== "Identifier") return;
|
|
801
|
-
const variable = resolveVariable(context.sourceCode, node.left);
|
|
802
|
+
const variable = resolveVariable$1(context.sourceCode, node.left);
|
|
802
803
|
if (variable === null) return;
|
|
803
804
|
const declarator = variableDeclarator$1(variable);
|
|
804
805
|
if (declarator === null || declarator.id.type !== "Identifier") return;
|
|
@@ -859,7 +860,7 @@ function importedName$1(node) {
|
|
|
859
860
|
function isTestFrameworkObject(sourceCode, expression) {
|
|
860
861
|
if (expression.type !== "Identifier") return false;
|
|
861
862
|
if ((expression.name === "vi" || expression.name === "jest") && sourceCode.isGlobalReference(expression)) return true;
|
|
862
|
-
const variable = resolveVariable(sourceCode, expression);
|
|
863
|
+
const variable = resolveVariable$1(sourceCode, expression);
|
|
863
864
|
if (variable === null || variable.defs.length === 0) return expression.name === "vi" || expression.name === "jest";
|
|
864
865
|
return variable.defs.some((definition) => {
|
|
865
866
|
if (definition.type !== "ImportBinding" || definition.parent?.type !== "ImportDeclaration") return false;
|
|
@@ -1025,7 +1026,7 @@ const noReduceAccumulatorCopyRule = defineRule({
|
|
|
1025
1026
|
function isGlobalReflect(sourceCode, expression) {
|
|
1026
1027
|
if (expression.type !== "Identifier" || expression.name !== "Reflect") return false;
|
|
1027
1028
|
if (sourceCode.isGlobalReference(expression)) return true;
|
|
1028
|
-
const variable = resolveVariable(sourceCode, expression);
|
|
1029
|
+
const variable = resolveVariable$1(sourceCode, expression);
|
|
1029
1030
|
return variable === null || variable.defs.length === 0;
|
|
1030
1031
|
}
|
|
1031
1032
|
/** Reports whether a call target names one method on the global Reflect object. */
|
|
@@ -1699,6 +1700,356 @@ const requireSafetyCommentForTypeAssertionRule = defineRule({
|
|
|
1699
1700
|
}
|
|
1700
1701
|
});
|
|
1701
1702
|
//#endregion
|
|
1703
|
+
//#region src/shared/convex.ts
|
|
1704
|
+
const PUBLIC_REGISTRARS = /* @__PURE__ */ new Set([
|
|
1705
|
+
"query",
|
|
1706
|
+
"mutation",
|
|
1707
|
+
"action"
|
|
1708
|
+
]);
|
|
1709
|
+
const REGISTRARS = /* @__PURE__ */ new Set([
|
|
1710
|
+
...PUBLIC_REGISTRARS,
|
|
1711
|
+
"internalQuery",
|
|
1712
|
+
"internalMutation",
|
|
1713
|
+
"internalAction"
|
|
1714
|
+
]);
|
|
1715
|
+
const PUBLIC_GENERICS = /* @__PURE__ */ new Set([
|
|
1716
|
+
"queryGeneric",
|
|
1717
|
+
"mutationGeneric",
|
|
1718
|
+
"actionGeneric"
|
|
1719
|
+
]);
|
|
1720
|
+
const GENERICS = /* @__PURE__ */ new Set([
|
|
1721
|
+
...PUBLIC_GENERICS,
|
|
1722
|
+
"internalQueryGeneric",
|
|
1723
|
+
"internalMutationGeneric",
|
|
1724
|
+
"internalActionGeneric"
|
|
1725
|
+
]);
|
|
1726
|
+
const IDENTITY_READERS = /* @__PURE__ */ new Set(["getAuthUserId", "getAuthSessionId"]);
|
|
1727
|
+
const SOURCE_EXTENSION = /\.(?:[cm]?[jt]sx?)$/u;
|
|
1728
|
+
function slashes(path) {
|
|
1729
|
+
return path.replaceAll("\\", "/");
|
|
1730
|
+
}
|
|
1731
|
+
function isAbsolute(path) {
|
|
1732
|
+
return path.startsWith("/") || /^[A-Za-z]:\//u.test(path);
|
|
1733
|
+
}
|
|
1734
|
+
function normalize(path) {
|
|
1735
|
+
const out = [];
|
|
1736
|
+
for (const segment of slashes(path).split("/")) {
|
|
1737
|
+
if (segment === "." || segment === "" && out.length > 0) continue;
|
|
1738
|
+
if (segment === ".." && out.length > 0 && out.at(-1) !== ".." && out.at(-1) !== "") {
|
|
1739
|
+
out.pop();
|
|
1740
|
+
continue;
|
|
1741
|
+
}
|
|
1742
|
+
out.push(segment);
|
|
1743
|
+
}
|
|
1744
|
+
return out.join("/");
|
|
1745
|
+
}
|
|
1746
|
+
function withoutExtension(path) {
|
|
1747
|
+
return path.replace(SOURCE_EXTENSION, "");
|
|
1748
|
+
}
|
|
1749
|
+
const TYPESCRIPT_SOURCE = /* @__PURE__ */ new Map([
|
|
1750
|
+
[".js", [".ts", ".tsx"]],
|
|
1751
|
+
[".jsx", [".tsx"]],
|
|
1752
|
+
[".mjs", [".mts"]],
|
|
1753
|
+
[".cjs", [".cts"]]
|
|
1754
|
+
]);
|
|
1755
|
+
/** The file itself, its spelling with no extension, or the `.js` spelling of a `.ts` file. */
|
|
1756
|
+
function resolvesTo(target, file) {
|
|
1757
|
+
if (target === file || target === withoutExtension(file)) return true;
|
|
1758
|
+
const extension = /\.[cm]?jsx?$/u.exec(target)?.[0];
|
|
1759
|
+
if (extension === void 0) return false;
|
|
1760
|
+
const stem = target.slice(0, -extension.length);
|
|
1761
|
+
return (TYPESCRIPT_SOURCE.get(extension) ?? []).some((source) => `${stem}${source}` === file);
|
|
1762
|
+
}
|
|
1763
|
+
function directoryOf(path) {
|
|
1764
|
+
return path.slice(0, path.lastIndexOf("/"));
|
|
1765
|
+
}
|
|
1766
|
+
const placed = /* @__PURE__ */ new Map();
|
|
1767
|
+
/**
|
|
1768
|
+
* oxlint gives a rule the process's working directory, not the config's, so a relative wrapper
|
|
1769
|
+
* is looked for from there upward and the first directory that holds it is the root. Found
|
|
1770
|
+
* nowhere, it is taken from the working directory as written. An absolute one is used as is.
|
|
1771
|
+
*/
|
|
1772
|
+
function placeWrapper(cwd, option, imports) {
|
|
1773
|
+
const key = `${cwd}\0${option}\0${imports.join("\0")}`;
|
|
1774
|
+
const known = placed.get(key);
|
|
1775
|
+
if (known !== void 0) return known;
|
|
1776
|
+
const written = normalize(option);
|
|
1777
|
+
let file = written;
|
|
1778
|
+
if (!isAbsolute(written)) {
|
|
1779
|
+
let root = normalize(cwd);
|
|
1780
|
+
file = `${root}/${written}`;
|
|
1781
|
+
while (root !== "" && root.includes("/")) {
|
|
1782
|
+
if (existsSync(`${root}/${written}`)) {
|
|
1783
|
+
file = `${root}/${written}`;
|
|
1784
|
+
break;
|
|
1785
|
+
}
|
|
1786
|
+
root = directoryOf(root);
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
const wrapper = {
|
|
1790
|
+
file,
|
|
1791
|
+
imports: new Set(imports)
|
|
1792
|
+
};
|
|
1793
|
+
placed.set(key, wrapper);
|
|
1794
|
+
return wrapper;
|
|
1795
|
+
}
|
|
1796
|
+
/** Another backend's file with the same path tail is not the wrapper. */
|
|
1797
|
+
function isWrapperFile(filename, wrapper) {
|
|
1798
|
+
return normalize(filename) === wrapper.file;
|
|
1799
|
+
}
|
|
1800
|
+
/**
|
|
1801
|
+
* A relative specifier reaches the wrapper only by resolving to its file; `/index` is not
|
|
1802
|
+
* collapsed, so a sibling `functions/index.ts` stays a different module. Any other spelling is an
|
|
1803
|
+
* alias from a bundler or tsconfig this rule cannot read, so it names the wrapper only when the
|
|
1804
|
+
* config lists it as written, and nothing about it depends on where `vp lint` runs.
|
|
1805
|
+
*/
|
|
1806
|
+
function moduleOf(filename, specifier, wrapper) {
|
|
1807
|
+
if (specifier === "convex/server") return "convexServer";
|
|
1808
|
+
if (specifier === "@convex-dev/auth/server") return "authLibrary";
|
|
1809
|
+
const relative = specifier.startsWith("./") || specifier.startsWith("../");
|
|
1810
|
+
const target = normalize(relative ? `${directoryOf(slashes(filename))}/${specifier}` : specifier);
|
|
1811
|
+
const module = withoutExtension(target);
|
|
1812
|
+
if (module === "_generated/server" || module.endsWith("/_generated/server")) return "generated";
|
|
1813
|
+
if (wrapper === void 0) return null;
|
|
1814
|
+
if (relative) return resolvesTo(target, wrapper.file) ? "wrapper" : null;
|
|
1815
|
+
return wrapper.imports.has(specifier) ? "wrapper" : null;
|
|
1816
|
+
}
|
|
1817
|
+
function isGenerated(filename) {
|
|
1818
|
+
return slashes(filename).includes("/_generated/");
|
|
1819
|
+
}
|
|
1820
|
+
function unwrapTypeScript(node) {
|
|
1821
|
+
let current = node;
|
|
1822
|
+
while (current.type === "TSAsExpression" || current.type === "TSSatisfiesExpression" || current.type === "TSNonNullExpression" || current.type === "TSTypeAssertion" || current.type === "TSInstantiationExpression" || current.type === "ParenthesizedExpression") current = current.expression;
|
|
1823
|
+
return current;
|
|
1824
|
+
}
|
|
1825
|
+
function exportName(name) {
|
|
1826
|
+
return name.type === "Literal" ? name.value : name.name;
|
|
1827
|
+
}
|
|
1828
|
+
function keyName(key, computed) {
|
|
1829
|
+
if (key.type === "Identifier" && !computed) return key.name;
|
|
1830
|
+
if (key.type === "Literal" && typeof key.value === "string") return key.value;
|
|
1831
|
+
return null;
|
|
1832
|
+
}
|
|
1833
|
+
function resolveVariable(sourceCode, identifier) {
|
|
1834
|
+
let scope = sourceCode.getScope(identifier);
|
|
1835
|
+
for (;;) {
|
|
1836
|
+
const variable = scope.set.get(identifier.name);
|
|
1837
|
+
if (variable !== void 0) return variable;
|
|
1838
|
+
if (scope.upper === null) return null;
|
|
1839
|
+
scope = scope.upper;
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
function staticString(node) {
|
|
1843
|
+
if (node.type === "Literal") return typeof node.value === "string" ? node.value : null;
|
|
1844
|
+
if (node.type === "TemplateLiteral" && node.expressions.length === 0) return node.quasis[0]?.value.cooked ?? null;
|
|
1845
|
+
return null;
|
|
1846
|
+
}
|
|
1847
|
+
function buildsFunction(origin, withHttp) {
|
|
1848
|
+
if (origin === null || origin.name === null) return false;
|
|
1849
|
+
switch (origin.module) {
|
|
1850
|
+
case "generated":
|
|
1851
|
+
case "global": return REGISTRARS.has(origin.name) || withHttp && origin.name === "httpAction";
|
|
1852
|
+
case "convexServer": return GENERICS.has(origin.name) || withHttp && origin.name === "httpActionGeneric";
|
|
1853
|
+
case "wrapper": return origin.name !== "httpAction" || withHttp;
|
|
1854
|
+
default: return false;
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
/**
|
|
1858
|
+
* Where a value is not definitely known it is unknown, and the rules report nothing on it rather
|
|
1859
|
+
* than guess. Cycles are guarded per path, so an object met twice is read twice.
|
|
1860
|
+
*/
|
|
1861
|
+
function analyse(sourceCode, filename, wrapper) {
|
|
1862
|
+
const active = /* @__PURE__ */ new Set();
|
|
1863
|
+
const guarded = (node, fallback, run) => {
|
|
1864
|
+
if (active.has(node)) return fallback;
|
|
1865
|
+
active.add(node);
|
|
1866
|
+
try {
|
|
1867
|
+
return run();
|
|
1868
|
+
} finally {
|
|
1869
|
+
active.delete(node);
|
|
1870
|
+
}
|
|
1871
|
+
};
|
|
1872
|
+
const constDeclarator = (variable) => {
|
|
1873
|
+
const definition = variable?.defs.length === 1 ? variable.defs[0] : void 0;
|
|
1874
|
+
if (definition?.type !== "Variable") return null;
|
|
1875
|
+
const declarator = definition.node;
|
|
1876
|
+
const declaration = definition.parent;
|
|
1877
|
+
if (declarator.type !== "VariableDeclarator" || declarator.init === null) return null;
|
|
1878
|
+
return declaration?.type === "VariableDeclaration" && declaration.kind === "const" ? declarator : null;
|
|
1879
|
+
};
|
|
1880
|
+
const staticValue = (node) => {
|
|
1881
|
+
const value = unwrapTypeScript(node);
|
|
1882
|
+
if (value.type === "ObjectExpression" || value.type === "FunctionExpression" || value.type === "ArrowFunctionExpression") return value;
|
|
1883
|
+
if (value.type !== "Identifier") return null;
|
|
1884
|
+
const variable = resolveVariable(sourceCode, value);
|
|
1885
|
+
const definition = variable?.defs.length === 1 ? variable.defs[0] : void 0;
|
|
1886
|
+
if (definition?.type === "FunctionName" && definition.node.type === "FunctionDeclaration") return definition.node;
|
|
1887
|
+
const declarator = constDeclarator(variable);
|
|
1888
|
+
if (declarator?.id.type !== "Identifier" || declarator.init === null) return null;
|
|
1889
|
+
const { init } = declarator;
|
|
1890
|
+
return guarded(declarator, null, () => staticValue(init));
|
|
1891
|
+
};
|
|
1892
|
+
const namespaceMember = (whole, key) => whole?.name === null && whole.excluded?.has(key) !== true ? {
|
|
1893
|
+
module: whole.module,
|
|
1894
|
+
name: key
|
|
1895
|
+
} : null;
|
|
1896
|
+
const memberOf = (object, key) => guarded(object, { kind: "unknown" }, () => {
|
|
1897
|
+
let found = { kind: "absent" };
|
|
1898
|
+
for (const property of object.properties) {
|
|
1899
|
+
if (property.type === "SpreadElement") {
|
|
1900
|
+
const spread = staticValue(property.argument);
|
|
1901
|
+
if (spread?.type === "ObjectExpression") {
|
|
1902
|
+
const inner = memberOf(spread, key);
|
|
1903
|
+
if (inner.kind !== "absent") found = inner;
|
|
1904
|
+
continue;
|
|
1905
|
+
}
|
|
1906
|
+
const origin = namespaceMember(originOf(property.argument), key);
|
|
1907
|
+
found = origin === null ? { kind: "unknown" } : {
|
|
1908
|
+
kind: "origin",
|
|
1909
|
+
origin
|
|
1910
|
+
};
|
|
1911
|
+
continue;
|
|
1912
|
+
}
|
|
1913
|
+
const name = keyName(property.key, property.computed);
|
|
1914
|
+
if (name === key) found = {
|
|
1915
|
+
kind: "value",
|
|
1916
|
+
value: property.value
|
|
1917
|
+
};
|
|
1918
|
+
else if (name === null) found = { kind: "unknown" };
|
|
1919
|
+
}
|
|
1920
|
+
return found;
|
|
1921
|
+
});
|
|
1922
|
+
const memberOrigin = (base, key) => {
|
|
1923
|
+
const object = staticValue(base);
|
|
1924
|
+
if (object?.type === "ObjectExpression") {
|
|
1925
|
+
const member = memberOf(object, key);
|
|
1926
|
+
if (member.kind === "origin") return member.origin;
|
|
1927
|
+
return member.kind === "value" ? originOf(member.value) : null;
|
|
1928
|
+
}
|
|
1929
|
+
return namespaceMember(originOf(base), key);
|
|
1930
|
+
};
|
|
1931
|
+
const fromBinding = (variable, name) => {
|
|
1932
|
+
const definition = variable.defs.length === 1 ? variable.defs[0] : void 0;
|
|
1933
|
+
if (definition?.type === "ImportBinding") {
|
|
1934
|
+
const declaration = definition.parent;
|
|
1935
|
+
if (declaration?.type !== "ImportDeclaration" || declaration.importKind === "type") return null;
|
|
1936
|
+
const module = moduleOf(filename, declaration.source.value, wrapper);
|
|
1937
|
+
const specifier = definition.node;
|
|
1938
|
+
if (module === null) return null;
|
|
1939
|
+
if (specifier.type === "ImportNamespaceSpecifier") return {
|
|
1940
|
+
module,
|
|
1941
|
+
name: null
|
|
1942
|
+
};
|
|
1943
|
+
if (specifier.type === "ImportDefaultSpecifier") return {
|
|
1944
|
+
module,
|
|
1945
|
+
name: "default"
|
|
1946
|
+
};
|
|
1947
|
+
if (specifier.type !== "ImportSpecifier" || specifier.importKind === "type") return null;
|
|
1948
|
+
return {
|
|
1949
|
+
module,
|
|
1950
|
+
name: exportName(specifier.imported)
|
|
1951
|
+
};
|
|
1952
|
+
}
|
|
1953
|
+
const declarator = constDeclarator(variable);
|
|
1954
|
+
if (declarator === null || declarator.init === null) return null;
|
|
1955
|
+
const { id, init } = declarator;
|
|
1956
|
+
if (id.type === "Identifier") return originOf(init);
|
|
1957
|
+
if (id.type !== "ObjectPattern") return null;
|
|
1958
|
+
const holder = (name.parent?.type === "AssignmentPattern" ? name.parent : name).parent;
|
|
1959
|
+
if (holder?.type === "RestElement" && holder.parent === id) {
|
|
1960
|
+
const whole = originOf(init);
|
|
1961
|
+
if (whole?.name !== null || whole === null) return null;
|
|
1962
|
+
const taken = new Set(whole.excluded);
|
|
1963
|
+
for (const property of id.properties) {
|
|
1964
|
+
if (property.type !== "Property") continue;
|
|
1965
|
+
const key = keyName(property.key, property.computed);
|
|
1966
|
+
if (key === null) return null;
|
|
1967
|
+
taken.add(key);
|
|
1968
|
+
}
|
|
1969
|
+
return {
|
|
1970
|
+
module: whole.module,
|
|
1971
|
+
name: null,
|
|
1972
|
+
excluded: taken
|
|
1973
|
+
};
|
|
1974
|
+
}
|
|
1975
|
+
if (holder?.type !== "Property" || holder.parent !== id) return null;
|
|
1976
|
+
const key = keyName(holder.key, holder.computed);
|
|
1977
|
+
return key === null ? null : memberOrigin(init, key);
|
|
1978
|
+
};
|
|
1979
|
+
const originOf = (node) => {
|
|
1980
|
+
const expression = unwrapTypeScript(node);
|
|
1981
|
+
return guarded(expression, null, () => {
|
|
1982
|
+
if (expression.type === "Identifier") {
|
|
1983
|
+
const variable = resolveVariable(sourceCode, expression);
|
|
1984
|
+
if (variable === null || variable.defs.length === 0) return {
|
|
1985
|
+
module: "global",
|
|
1986
|
+
name: expression.name
|
|
1987
|
+
};
|
|
1988
|
+
return fromBinding(variable, variable.defs[0]?.name ?? expression);
|
|
1989
|
+
}
|
|
1990
|
+
if (expression.type === "MemberExpression") {
|
|
1991
|
+
const key = keyName(expression.property, expression.computed);
|
|
1992
|
+
return key === null ? null : memberOrigin(expression.object, key);
|
|
1993
|
+
}
|
|
1994
|
+
if (expression.type === "AwaitExpression") {
|
|
1995
|
+
const imported = unwrapTypeScript(expression.argument);
|
|
1996
|
+
if (imported.type !== "ImportExpression") return null;
|
|
1997
|
+
const specifier = staticString(imported.source);
|
|
1998
|
+
const module = specifier === null ? null : moduleOf(filename, specifier, wrapper);
|
|
1999
|
+
return module === null ? null : {
|
|
2000
|
+
module,
|
|
2001
|
+
name: null
|
|
2002
|
+
};
|
|
2003
|
+
}
|
|
2004
|
+
return null;
|
|
2005
|
+
});
|
|
2006
|
+
};
|
|
2007
|
+
const directWrites = (variable) => variable.references.flatMap((reference) => {
|
|
2008
|
+
const written = reference.writeExpr;
|
|
2009
|
+
const { parent } = reference.identifier;
|
|
2010
|
+
if (written === null || !reference.isWrite()) return [];
|
|
2011
|
+
return parent?.type === "VariableDeclarator" && parent.id === reference.identifier || parent?.type === "AssignmentExpression" && parent.left === reference.identifier ? [unwrapTypeScript(written)] : [];
|
|
2012
|
+
});
|
|
2013
|
+
const variableValues = (variable) => {
|
|
2014
|
+
const definition = variable.defs[0];
|
|
2015
|
+
if (definition?.type !== "Variable") return [];
|
|
2016
|
+
return guarded(definition.name, [], () => directWrites(variable).flatMap((value) => {
|
|
2017
|
+
if (value.type !== "Identifier") return [value];
|
|
2018
|
+
const source = resolveVariable(sourceCode, value);
|
|
2019
|
+
const writes = source?.references.filter((reference) => reference.isWrite()) ?? [];
|
|
2020
|
+
return source !== null && writes.length === 1 ? variableValues(source) : [];
|
|
2021
|
+
}));
|
|
2022
|
+
};
|
|
2023
|
+
const valuesOf = (identifier) => {
|
|
2024
|
+
const variable = resolveVariable(sourceCode, identifier);
|
|
2025
|
+
return variable === null ? [] : variableValues(variable);
|
|
2026
|
+
};
|
|
2027
|
+
const isUndefined = (node) => {
|
|
2028
|
+
const value = unwrapTypeScript(node);
|
|
2029
|
+
if (value.type === "UnaryExpression") return value.operator === "void";
|
|
2030
|
+
if (value.type !== "Identifier") return false;
|
|
2031
|
+
const variable = resolveVariable(sourceCode, value);
|
|
2032
|
+
if (variable === null || variable.defs.length === 0) return value.name === "undefined";
|
|
2033
|
+
const declarator = constDeclarator(variable);
|
|
2034
|
+
if (declarator?.id.type !== "Identifier" || declarator.init === null) return false;
|
|
2035
|
+
const { init } = declarator;
|
|
2036
|
+
return guarded(declarator, false, () => isUndefined(init));
|
|
2037
|
+
};
|
|
2038
|
+
return {
|
|
2039
|
+
originOf,
|
|
2040
|
+
memberOf,
|
|
2041
|
+
staticValue,
|
|
2042
|
+
valuesOf,
|
|
2043
|
+
variableValues,
|
|
2044
|
+
isUndefined
|
|
2045
|
+
};
|
|
2046
|
+
}
|
|
2047
|
+
function definitionOf(analysis, call) {
|
|
2048
|
+
const [first] = call.arguments;
|
|
2049
|
+
if (first === void 0 || first.type === "SpreadElement") return null;
|
|
2050
|
+
return analysis.staticValue(first);
|
|
2051
|
+
}
|
|
2052
|
+
//#endregion
|
|
1702
2053
|
//#region src/shared/rule-options.ts
|
|
1703
2054
|
/**
|
|
1704
2055
|
* Rule options arrive as parsed JSON from the linter config, so this is the one
|
|
@@ -1713,6 +2064,337 @@ function stringOption(options, key) {
|
|
|
1713
2064
|
const value = firstOptionObject(options)[key];
|
|
1714
2065
|
return typeof value === "string" ? value : void 0;
|
|
1715
2066
|
}
|
|
2067
|
+
/** A boolean-valued key of the first option object, or undefined when absent or not a boolean. */
|
|
2068
|
+
function booleanOption(options, key) {
|
|
2069
|
+
const value = firstOptionObject(options)[key];
|
|
2070
|
+
return typeof value === "boolean" ? value : void 0;
|
|
2071
|
+
}
|
|
2072
|
+
/** A string-array key of the first option object; anything else reads as empty. */
|
|
2073
|
+
function stringsOption(options, key) {
|
|
2074
|
+
const value = firstOptionObject(options)[key];
|
|
2075
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
2076
|
+
}
|
|
2077
|
+
//#endregion
|
|
2078
|
+
//#region src/rules/convex-checked-constructors.ts
|
|
2079
|
+
const CONTEXT_NAME = "ctx";
|
|
2080
|
+
function bannedAs(origin) {
|
|
2081
|
+
if (origin === null || origin.name === null) return null;
|
|
2082
|
+
if (origin.module === "generated" && PUBLIC_REGISTRARS.has(origin.name)) return "rawConstructor";
|
|
2083
|
+
if (origin.module === "convexServer" && PUBLIC_GENERICS.has(origin.name)) return "rawConstructor";
|
|
2084
|
+
if (origin.module === "authLibrary" && IDENTITY_READERS.has(origin.name)) return "identityReader";
|
|
2085
|
+
return null;
|
|
2086
|
+
}
|
|
2087
|
+
function holdsBanned(module) {
|
|
2088
|
+
return module === "generated" || module === "convexServer" || module === "authLibrary";
|
|
2089
|
+
}
|
|
2090
|
+
function copiesBanned(module, taken) {
|
|
2091
|
+
return [...module === "generated" ? PUBLIC_REGISTRARS : module === "convexServer" ? PUBLIC_GENERICS : module === "authLibrary" ? IDENTITY_READERS : /* @__PURE__ */ new Set()].some((name) => !taken.has(name));
|
|
2092
|
+
}
|
|
2093
|
+
function authProperty(pattern) {
|
|
2094
|
+
const target = pattern.type === "AssignmentPattern" ? pattern.left : pattern;
|
|
2095
|
+
if (target.type !== "ObjectPattern") return null;
|
|
2096
|
+
for (const property of target.properties) if (property.type === "Property" && keyName(property.key, property.computed) === "auth") return property;
|
|
2097
|
+
return null;
|
|
2098
|
+
}
|
|
2099
|
+
function outermost(identifier) {
|
|
2100
|
+
let value = identifier;
|
|
2101
|
+
let parent = identifier.parent;
|
|
2102
|
+
while (parent !== null && (parent.type === "TSAsExpression" || parent.type === "TSSatisfiesExpression" || parent.type === "TSNonNullExpression" || parent.type === "TSTypeAssertion" || parent.type === "ParenthesizedExpression")) {
|
|
2103
|
+
value = parent;
|
|
2104
|
+
parent = parent.parent;
|
|
2105
|
+
}
|
|
2106
|
+
return {
|
|
2107
|
+
value,
|
|
2108
|
+
parent
|
|
2109
|
+
};
|
|
2110
|
+
}
|
|
2111
|
+
/** Reject the raw public constructors and every read of the caller outside the wrapper. */
|
|
2112
|
+
const convexCheckedConstructorsRule = defineRule({
|
|
2113
|
+
meta: {
|
|
2114
|
+
type: "problem",
|
|
2115
|
+
docs: { description: "Build public Convex functions with the project's checked constructors, and read the caller only where they are defined." },
|
|
2116
|
+
messages: {
|
|
2117
|
+
rawConstructor: "`{{name}}` from {{source}} builds a public function whose handler runs before anything checks the caller. Build it with one of the project's checked constructors.{{advice}}",
|
|
2118
|
+
identityReader: "`{{name}}` reads the caller's identity, which the checked constructors resolve before the handler runs. Take the caller from the context the constructor hands the handler.{{advice}}",
|
|
2119
|
+
reExportAll: "`export *` from {{source}} hands on what only the module building the checked constructors may use. Export the names you mean, one by one.{{advice}}",
|
|
2120
|
+
namespaceCopy: "Copying every export of {{source}} hands on what only the module building the checked constructors may use. Take the names you mean, one by one.{{advice}}",
|
|
2121
|
+
authRead: "Reading `auth` off a Convex context goes around the checked constructors, which resolve the caller before the handler runs. Take the caller from the context the constructor hands the handler.{{advice}}"
|
|
2122
|
+
},
|
|
2123
|
+
schema: [{
|
|
2124
|
+
type: "object",
|
|
2125
|
+
properties: {
|
|
2126
|
+
wrapper: { type: "string" },
|
|
2127
|
+
wrapperImports: {
|
|
2128
|
+
type: "array",
|
|
2129
|
+
items: { type: "string" }
|
|
2130
|
+
},
|
|
2131
|
+
hint: { type: "string" }
|
|
2132
|
+
},
|
|
2133
|
+
additionalProperties: false
|
|
2134
|
+
}]
|
|
2135
|
+
},
|
|
2136
|
+
create(context) {
|
|
2137
|
+
const { filename, sourceCode } = context;
|
|
2138
|
+
if (isGenerated(filename)) return {};
|
|
2139
|
+
const option = stringOption(context.options, "wrapper");
|
|
2140
|
+
const wrapper = option === void 0 ? void 0 : placeWrapper(context.cwd, option, stringsOption(context.options, "wrapperImports"));
|
|
2141
|
+
if (wrapper !== void 0 && isWrapperFile(filename, wrapper)) return {};
|
|
2142
|
+
const hint = stringOption(context.options, "hint");
|
|
2143
|
+
const advice = [option === void 0 ? "" : ` The checked constructors are in ${option}.`, hint === void 0 ? "" : ` ${hint}`].join("");
|
|
2144
|
+
const analysis = analyse(sourceCode, filename, wrapper);
|
|
2145
|
+
const origin = (node) => analysis.originOf(node);
|
|
2146
|
+
const reported = /* @__PURE__ */ new Set();
|
|
2147
|
+
const report = (node, messageId, data = {}) => {
|
|
2148
|
+
if (reported.has(node)) return;
|
|
2149
|
+
reported.add(node);
|
|
2150
|
+
context.report({
|
|
2151
|
+
node,
|
|
2152
|
+
messageId,
|
|
2153
|
+
data: {
|
|
2154
|
+
...data,
|
|
2155
|
+
advice
|
|
2156
|
+
}
|
|
2157
|
+
});
|
|
2158
|
+
};
|
|
2159
|
+
const reportBanned = (node, found, source) => {
|
|
2160
|
+
const messageId = bannedAs(found);
|
|
2161
|
+
if (messageId !== null && found?.name !== null && found !== null) report(node, messageId, {
|
|
2162
|
+
name: found.name,
|
|
2163
|
+
source
|
|
2164
|
+
});
|
|
2165
|
+
};
|
|
2166
|
+
const sourceOf = (namespace) => sourceCode.getText(namespace);
|
|
2167
|
+
const checkPattern = (pattern, init) => {
|
|
2168
|
+
if (pattern.type !== "ObjectPattern") return;
|
|
2169
|
+
const object = analysis.staticValue(init);
|
|
2170
|
+
const whole = origin(init);
|
|
2171
|
+
const taken = new Set(whole?.excluded);
|
|
2172
|
+
for (const property of pattern.properties) {
|
|
2173
|
+
if (property.type !== "Property") continue;
|
|
2174
|
+
const name = keyName(property.key, property.computed);
|
|
2175
|
+
if (name === null) continue;
|
|
2176
|
+
taken.add(name);
|
|
2177
|
+
if (object?.type === "ObjectExpression") {
|
|
2178
|
+
const member = analysis.memberOf(object, name);
|
|
2179
|
+
if (member.kind === "origin") reportBanned(property, member.origin, sourceOf(init));
|
|
2180
|
+
if (member.kind === "value") reportBanned(property, origin(member.value), sourceOf(init));
|
|
2181
|
+
} else if (whole?.name === null && whole.excluded?.has(name) !== true) reportBanned(property, {
|
|
2182
|
+
module: whole.module,
|
|
2183
|
+
name
|
|
2184
|
+
}, sourceOf(init));
|
|
2185
|
+
}
|
|
2186
|
+
const rest = pattern.properties.find((property) => property.type === "RestElement");
|
|
2187
|
+
if (rest !== void 0 && whole?.name === null && copiesBanned(whole.module, taken)) report(rest, "namespaceCopy", { source: sourceOf(init) });
|
|
2188
|
+
};
|
|
2189
|
+
const walked = /* @__PURE__ */ new Set();
|
|
2190
|
+
const walkReferences = (references) => {
|
|
2191
|
+
for (const reference of references) {
|
|
2192
|
+
const { value, parent } = outermost(reference.identifier);
|
|
2193
|
+
if (parent === null) continue;
|
|
2194
|
+
if (parent.type === "MemberExpression" && parent.object === value) {
|
|
2195
|
+
if (keyName(parent.property, parent.computed) === "auth") report(parent, "authRead");
|
|
2196
|
+
continue;
|
|
2197
|
+
}
|
|
2198
|
+
if (parent.type === "VariableDeclarator" && parent.init === value) {
|
|
2199
|
+
const taken = authProperty(parent.id);
|
|
2200
|
+
if (taken !== null) report(taken, "authRead");
|
|
2201
|
+
const declaration = parent.parent;
|
|
2202
|
+
if (parent.id.type === "Identifier" && declaration.type === "VariableDeclaration" && declaration.kind === "const") for (const alias of sourceCode.getDeclaredVariables(parent)) walkContext(alias);
|
|
2203
|
+
continue;
|
|
2204
|
+
}
|
|
2205
|
+
if (parent.type === "AssignmentExpression" && parent.right === value) {
|
|
2206
|
+
const taken = authProperty(parent.left);
|
|
2207
|
+
if (taken !== null) report(taken, "authRead");
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
};
|
|
2211
|
+
const walkContext = (variable) => {
|
|
2212
|
+
if (walked.has(variable)) return;
|
|
2213
|
+
walked.add(variable);
|
|
2214
|
+
walkReferences(variable.references);
|
|
2215
|
+
};
|
|
2216
|
+
const walkHandler = (handler) => {
|
|
2217
|
+
const [first] = handler.params;
|
|
2218
|
+
if (first === void 0 || first.type === "TSParameterProperty") return;
|
|
2219
|
+
if (first.type === "RestElement") return;
|
|
2220
|
+
const taken = authProperty(first);
|
|
2221
|
+
if (taken !== null) report(taken, "authRead");
|
|
2222
|
+
const binding = first.type === "AssignmentPattern" ? first.left : first;
|
|
2223
|
+
if (binding.type !== "Identifier") return;
|
|
2224
|
+
const variable = sourceCode.getDeclaredVariables(handler).find((candidate) => candidate.name === binding.name);
|
|
2225
|
+
if (variable !== void 0) walkContext(variable);
|
|
2226
|
+
};
|
|
2227
|
+
return {
|
|
2228
|
+
Program() {
|
|
2229
|
+
for (const scope of sourceCode.scopeManager.scopes) {
|
|
2230
|
+
const variable = scope.set.get(CONTEXT_NAME);
|
|
2231
|
+
if (variable?.defs[0]?.type === "Parameter") walkContext(variable);
|
|
2232
|
+
}
|
|
2233
|
+
},
|
|
2234
|
+
ImportDeclaration(node) {
|
|
2235
|
+
if (node.importKind === "type") return;
|
|
2236
|
+
const source = node.source.value;
|
|
2237
|
+
for (const specifier of node.specifiers) {
|
|
2238
|
+
if (specifier.type !== "ImportSpecifier" || specifier.importKind === "type") continue;
|
|
2239
|
+
const module = moduleOf(filename, source, wrapper);
|
|
2240
|
+
const name = exportName(specifier.imported);
|
|
2241
|
+
reportBanned(specifier, module === null ? null : {
|
|
2242
|
+
module,
|
|
2243
|
+
name
|
|
2244
|
+
}, source);
|
|
2245
|
+
}
|
|
2246
|
+
},
|
|
2247
|
+
ExportNamedDeclaration(node) {
|
|
2248
|
+
if (node.source === null || node.exportKind === "type") return;
|
|
2249
|
+
const source = node.source.value;
|
|
2250
|
+
const module = moduleOf(filename, source, wrapper);
|
|
2251
|
+
if (module === null) return;
|
|
2252
|
+
for (const specifier of node.specifiers) {
|
|
2253
|
+
if (specifier.exportKind === "type") continue;
|
|
2254
|
+
reportBanned(specifier, {
|
|
2255
|
+
module,
|
|
2256
|
+
name: exportName(specifier.local)
|
|
2257
|
+
}, source);
|
|
2258
|
+
}
|
|
2259
|
+
},
|
|
2260
|
+
ExportAllDeclaration(node) {
|
|
2261
|
+
if (node.exportKind === "type") return;
|
|
2262
|
+
const source = node.source.value;
|
|
2263
|
+
if (holdsBanned(moduleOf(filename, source, wrapper))) report(node, "reExportAll", { source });
|
|
2264
|
+
},
|
|
2265
|
+
MemberExpression(node) {
|
|
2266
|
+
reportBanned(node, origin(node), sourceOf(node.object));
|
|
2267
|
+
},
|
|
2268
|
+
VariableDeclarator(node) {
|
|
2269
|
+
if (node.init !== null) checkPattern(node.id, node.init);
|
|
2270
|
+
},
|
|
2271
|
+
AssignmentExpression(node) {
|
|
2272
|
+
checkPattern(node.left, node.right);
|
|
2273
|
+
},
|
|
2274
|
+
SpreadElement(node) {
|
|
2275
|
+
if (node.parent.type !== "ObjectExpression") return;
|
|
2276
|
+
const whole = origin(node.argument);
|
|
2277
|
+
if (whole?.name === null && copiesBanned(whole.module, whole.excluded ?? /* @__PURE__ */ new Set())) report(node, "namespaceCopy", { source: sourceOf(node.argument) });
|
|
2278
|
+
},
|
|
2279
|
+
CallExpression(node) {
|
|
2280
|
+
if (!buildsFunction(origin(node.callee), true)) return;
|
|
2281
|
+
const definition = definitionOf(analysis, node);
|
|
2282
|
+
if (definition === null) return;
|
|
2283
|
+
if (definition.type !== "ObjectExpression") {
|
|
2284
|
+
walkHandler(definition);
|
|
2285
|
+
return;
|
|
2286
|
+
}
|
|
2287
|
+
const member = analysis.memberOf(definition, "handler");
|
|
2288
|
+
const handler = member.kind === "value" ? analysis.staticValue(member.value) : null;
|
|
2289
|
+
if (handler !== null && handler.type !== "ObjectExpression") walkHandler(handler);
|
|
2290
|
+
}
|
|
2291
|
+
};
|
|
2292
|
+
}
|
|
2293
|
+
});
|
|
2294
|
+
//#endregion
|
|
2295
|
+
//#region src/rules/convex-require-validators.ts
|
|
2296
|
+
/**
|
|
2297
|
+
* Every value an export can hold: a binding's initializer and each later assignment, so a `let`
|
|
2298
|
+
* reassigned to another function is checked in both forms.
|
|
2299
|
+
*/
|
|
2300
|
+
function exportedValues(analysis, sourceCode, program) {
|
|
2301
|
+
const exported = [];
|
|
2302
|
+
const add = (value) => {
|
|
2303
|
+
const unwrapped = unwrapTypeScript(value);
|
|
2304
|
+
if (unwrapped.type === "Identifier") exported.push(...analysis.valuesOf(unwrapped));
|
|
2305
|
+
else exported.push(unwrapped);
|
|
2306
|
+
};
|
|
2307
|
+
for (const statement of program.body) {
|
|
2308
|
+
if (statement.type === "ExportNamedDeclaration" && statement.source === null) {
|
|
2309
|
+
if (statement.declaration?.type === "VariableDeclaration") for (const declarator of statement.declaration.declarations) for (const variable of sourceCode.getDeclaredVariables(declarator)) exported.push(...analysis.variableValues(variable));
|
|
2310
|
+
for (const specifier of statement.specifiers) {
|
|
2311
|
+
if (specifier.exportKind === "type" || specifier.local.type !== "Identifier") continue;
|
|
2312
|
+
add(specifier.local);
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
if (statement.type === "ExportDefaultDeclaration") {
|
|
2316
|
+
const { declaration } = statement;
|
|
2317
|
+
if (isExpression(declaration)) add(declaration);
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
return exported;
|
|
2321
|
+
}
|
|
2322
|
+
function isExpression(node) {
|
|
2323
|
+
return node.type !== "FunctionDeclaration" && node.type !== "ClassDeclaration" && node.type !== "TSInterfaceDeclaration" && node.type !== "TSDeclareFunction";
|
|
2324
|
+
}
|
|
2325
|
+
/** Require `args`, and `returns` unless switched off, on every exported Convex function. */
|
|
2326
|
+
const convexRequireValidatorsRule = defineRule({
|
|
2327
|
+
meta: {
|
|
2328
|
+
type: "problem",
|
|
2329
|
+
docs: { description: "Require argument and return validators on every exported Convex function, including those built by the project's checked constructors." },
|
|
2330
|
+
messages: {
|
|
2331
|
+
missing: "This Convex function declares no `{{validator}}` validator, so Convex checks nothing {{what}} and the types are a claim the deployment never tests. Add `{{validator}}`, using `{{empty}}` when there is nothing.{{advice}}",
|
|
2332
|
+
bareHandler: "This Convex function is a bare handler, which has no place for validators. Pass an object with {{fields}}.{{advice}}"
|
|
2333
|
+
},
|
|
2334
|
+
schema: [{
|
|
2335
|
+
type: "object",
|
|
2336
|
+
properties: {
|
|
2337
|
+
wrapper: { type: "string" },
|
|
2338
|
+
wrapperImports: {
|
|
2339
|
+
type: "array",
|
|
2340
|
+
items: { type: "string" }
|
|
2341
|
+
},
|
|
2342
|
+
returns: { type: "boolean" },
|
|
2343
|
+
hint: { type: "string" }
|
|
2344
|
+
},
|
|
2345
|
+
additionalProperties: false
|
|
2346
|
+
}]
|
|
2347
|
+
},
|
|
2348
|
+
create(context) {
|
|
2349
|
+
const { filename, sourceCode } = context;
|
|
2350
|
+
if (isGenerated(filename)) return {};
|
|
2351
|
+
const option = stringOption(context.options, "wrapper");
|
|
2352
|
+
const analysis = analyse(sourceCode, filename, option === void 0 ? void 0 : placeWrapper(context.cwd, option, stringsOption(context.options, "wrapperImports")));
|
|
2353
|
+
const requireReturns = booleanOption(context.options, "returns") ?? true;
|
|
2354
|
+
const hint = stringOption(context.options, "hint");
|
|
2355
|
+
const advice = hint === void 0 ? "" : ` ${hint}`;
|
|
2356
|
+
const required = requireReturns ? ["args", "returns"] : ["args"];
|
|
2357
|
+
const checked = /* @__PURE__ */ new Set();
|
|
2358
|
+
const check = (value) => {
|
|
2359
|
+
if (value.type !== "CallExpression" || checked.has(value)) return;
|
|
2360
|
+
checked.add(value);
|
|
2361
|
+
if (!buildsFunction(analysis.originOf(value.callee), false)) return;
|
|
2362
|
+
const definition = definitionOf(analysis, value);
|
|
2363
|
+
if (definition === null) return;
|
|
2364
|
+
if (definition.type !== "ObjectExpression") {
|
|
2365
|
+
const fields = requireReturns ? "`args`, `returns` and `handler`" : "`args` and `handler`";
|
|
2366
|
+
context.report({
|
|
2367
|
+
node: value,
|
|
2368
|
+
messageId: "bareHandler",
|
|
2369
|
+
data: {
|
|
2370
|
+
fields,
|
|
2371
|
+
advice
|
|
2372
|
+
}
|
|
2373
|
+
});
|
|
2374
|
+
return;
|
|
2375
|
+
}
|
|
2376
|
+
for (const validator of required) {
|
|
2377
|
+
const member = analysis.memberOf(definition, validator);
|
|
2378
|
+
if (!(member.kind === "absent" || member.kind === "value" && analysis.isUndefined(member.value))) continue;
|
|
2379
|
+
const what = validator === "args" ? "a caller sends" : "the handler returns";
|
|
2380
|
+
const empty = validator === "args" ? "{}" : "v.null()";
|
|
2381
|
+
context.report({
|
|
2382
|
+
node: definition,
|
|
2383
|
+
messageId: "missing",
|
|
2384
|
+
data: {
|
|
2385
|
+
validator,
|
|
2386
|
+
what,
|
|
2387
|
+
empty,
|
|
2388
|
+
advice
|
|
2389
|
+
}
|
|
2390
|
+
});
|
|
2391
|
+
}
|
|
2392
|
+
};
|
|
2393
|
+
return { "Program:exit"(node) {
|
|
2394
|
+
for (const value of exportedValues(analysis, sourceCode, node)) check(value);
|
|
2395
|
+
} };
|
|
2396
|
+
}
|
|
2397
|
+
});
|
|
1716
2398
|
//#endregion
|
|
1717
2399
|
//#region src/rules/no-hardcoded-jsx-text.ts
|
|
1718
2400
|
const defaultWordPattern = String.raw`\p{L}{3,}`;
|
|
@@ -1893,6 +2575,8 @@ const staticPublicEnvRule = defineRule({
|
|
|
1893
2575
|
const rdjperronPlugin = eslintCompatPlugin({
|
|
1894
2576
|
meta: { name: "rdjperron" },
|
|
1895
2577
|
rules: {
|
|
2578
|
+
"convex-checked-constructors": convexCheckedConstructorsRule,
|
|
2579
|
+
"convex-require-validators": convexRequireValidatorsRule,
|
|
1896
2580
|
"no-array-filter-map": noArrayFilterMapRule,
|
|
1897
2581
|
"no-chained-type-assertions": noChainedTypeAssertionsRule,
|
|
1898
2582
|
"no-conditional-empty-object-spread": noConditionalEmptyObjectSpreadRule,
|