oxlint-plugin-react-doctor 0.9.1-dev.6904ff1 → 0.9.1-dev.846c2df
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/dist/index.d.ts +4145 -3137
- package/dist/index.js +2051 -12
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -4,6 +4,8 @@ import { parseSync, visitorKeys } from "oxc-parser";
|
|
|
4
4
|
import * as fs from "node:fs";
|
|
5
5
|
import { readFileSync } from "node:fs";
|
|
6
6
|
import { analyze } from "eslint-scope";
|
|
7
|
+
import { visit } from "@shaderfrog/glsl-parser/ast/index.js";
|
|
8
|
+
import { parse } from "@shaderfrog/glsl-parser/index.js";
|
|
7
9
|
//#region src/plugin/utils/is-node-of-type.ts
|
|
8
10
|
const isNodeOfType = (node, type) => node !== null && typeof node === "object" && "type" in node && node.type === type;
|
|
9
11
|
//#endregion
|
|
@@ -22875,10 +22877,10 @@ const hasBroaderDeclaredDependency = (declaredKey, declaredKeys) => {
|
|
|
22875
22877
|
for (const otherDeclaredKey of declaredKeys) if (otherDeclaredKey !== declaredKey && declaredKey.startsWith(`${otherDeclaredKey}.`)) return true;
|
|
22876
22878
|
return false;
|
|
22877
22879
|
};
|
|
22878
|
-
const getMemberRootIdentifier$
|
|
22880
|
+
const getMemberRootIdentifier$2 = (node) => {
|
|
22879
22881
|
const stripped = unwrapExpression$3(node);
|
|
22880
22882
|
if (isNodeOfType(stripped, "Identifier")) return stripped;
|
|
22881
|
-
if (isNodeOfType(stripped, "MemberExpression")) return getMemberRootIdentifier$
|
|
22883
|
+
if (isNodeOfType(stripped, "MemberExpression")) return getMemberRootIdentifier$2(stripped.object);
|
|
22882
22884
|
return null;
|
|
22883
22885
|
};
|
|
22884
22886
|
const hasComputedMemberExpression = (node) => {
|
|
@@ -22915,7 +22917,7 @@ const resolveIdentitySourceKeysFromExpression = (expression, scopes, visitedSymb
|
|
|
22915
22917
|
if (isNodeOfType(stripped, "MemberExpression")) {
|
|
22916
22918
|
if (hasComputedMemberExpression(stripped)) return null;
|
|
22917
22919
|
const sourceKey = stringifyMemberChain(stripped);
|
|
22918
|
-
const rootIdentifier = getMemberRootIdentifier$
|
|
22920
|
+
const rootIdentifier = getMemberRootIdentifier$2(stripped);
|
|
22919
22921
|
const rootSymbol = rootIdentifier ? scopes.symbolFor(rootIdentifier) : null;
|
|
22920
22922
|
if (!sourceKey || !rootSymbol) return null;
|
|
22921
22923
|
if (isOutsideAllFunctions(rootSymbol)) return /* @__PURE__ */ new Set();
|
|
@@ -23009,7 +23011,7 @@ const resolveDerivedExpressionSourceKeys = (expression, scopes, visitedSymbolIds
|
|
|
23009
23011
|
if (isNodeOfType(candidate, "MemberExpression")) {
|
|
23010
23012
|
if (hasComputedMemberExpression(candidate)) return null;
|
|
23011
23013
|
const sourceKey = stringifyMemberChain(candidate);
|
|
23012
|
-
const rootIdentifier = getMemberRootIdentifier$
|
|
23014
|
+
const rootIdentifier = getMemberRootIdentifier$2(candidate);
|
|
23013
23015
|
const rootSymbol = rootIdentifier ? scopes.symbolFor(rootIdentifier) : null;
|
|
23014
23016
|
if (!sourceKey || !rootSymbol) return null;
|
|
23015
23017
|
if (isOutsideAllFunctions(rootSymbol) || symbolHasStableValue(rootSymbol, scopes)) return /* @__PURE__ */ new Set();
|
|
@@ -23096,14 +23098,14 @@ const isUseCallbackResultDep = (node, scopes) => {
|
|
|
23096
23098
|
return Boolean(initializer && isNodeOfType(initializer, "CallExpression") && getHookName(initializer.callee, scopes) === "useCallback");
|
|
23097
23099
|
};
|
|
23098
23100
|
const isExtraReactiveDepAllowed = (node, scopes) => {
|
|
23099
|
-
const rootIdentifier = getMemberRootIdentifier$
|
|
23101
|
+
const rootIdentifier = getMemberRootIdentifier$2(node);
|
|
23100
23102
|
if (!rootIdentifier) return false;
|
|
23101
23103
|
const symbol = scopes.symbolFor(rootIdentifier);
|
|
23102
23104
|
if (!symbol || isOutsideAllFunctions(symbol)) return false;
|
|
23103
23105
|
return !isUseCallbackResultDep(node, scopes);
|
|
23104
23106
|
};
|
|
23105
23107
|
const getRootSymbol = (node, scopes) => {
|
|
23106
|
-
const rootIdentifier = getMemberRootIdentifier$
|
|
23108
|
+
const rootIdentifier = getMemberRootIdentifier$2(node);
|
|
23107
23109
|
return rootIdentifier ? scopes.symbolFor(rootIdentifier) : null;
|
|
23108
23110
|
};
|
|
23109
23111
|
const getDeclaredDepSymbolSource = (node) => {
|
|
@@ -23392,7 +23394,7 @@ const findRefCurrentInCleanup = (callback, scopes) => {
|
|
|
23392
23394
|
if (isNodeOfType(node, "MemberExpression")) {
|
|
23393
23395
|
const candidateName = getRefCurrentNameFromMemberExpression(node);
|
|
23394
23396
|
if (candidateName) {
|
|
23395
|
-
const rootIdentifier = getMemberRootIdentifier$
|
|
23397
|
+
const rootIdentifier = getMemberRootIdentifier$2(node);
|
|
23396
23398
|
const symbol = rootIdentifier ? scopes.symbolFor(rootIdentifier) : null;
|
|
23397
23399
|
const callbackScope = scopes.ownScopeFor(callback) ?? scopes.scopeFor(callback);
|
|
23398
23400
|
if (!symbol || !isDescendantScope(symbol.scope, callbackScope)) {
|
|
@@ -23465,7 +23467,7 @@ const hasRefCurrentAssignment = (callback, refCurrentName) => {
|
|
|
23465
23467
|
return didAssignRefCurrent;
|
|
23466
23468
|
};
|
|
23467
23469
|
const isOuterFunctionScopeDep = (node, callback, scopes) => {
|
|
23468
|
-
const rootIdentifier = getMemberRootIdentifier$
|
|
23470
|
+
const rootIdentifier = getMemberRootIdentifier$2(node);
|
|
23469
23471
|
if (!rootIdentifier) return false;
|
|
23470
23472
|
const symbol = scopes.symbolFor(rootIdentifier);
|
|
23471
23473
|
if (!symbol || isOutsideAllFunctions(symbol)) return false;
|
|
@@ -98683,6 +98685,20 @@ const THREE_INTERPOLATION_FACTOR_ARGUMENT_BY_METHOD = new Map([
|
|
|
98683
98685
|
["slerp", 1],
|
|
98684
98686
|
["slerpQuaternions", 2]
|
|
98685
98687
|
]);
|
|
98688
|
+
const GLSL_POSITION_COMPONENT_BIT_BY_ALIAS = new Map([
|
|
98689
|
+
["x", 1],
|
|
98690
|
+
["r", 1],
|
|
98691
|
+
["s", 1],
|
|
98692
|
+
["y", 2],
|
|
98693
|
+
["g", 2],
|
|
98694
|
+
["t", 2],
|
|
98695
|
+
["z", 4],
|
|
98696
|
+
["b", 4],
|
|
98697
|
+
["p", 4],
|
|
98698
|
+
["w", 8],
|
|
98699
|
+
["a", 8],
|
|
98700
|
+
["q", 8]
|
|
98701
|
+
]);
|
|
98686
98702
|
const THREE_POSTPROCESSING_PASS_DISPOSAL_RELEASES = new Map([
|
|
98687
98703
|
["RenderPixelatedPass", 147],
|
|
98688
98704
|
["OutputPass", 153],
|
|
@@ -98690,6 +98706,73 @@ const THREE_POSTPROCESSING_PASS_DISPOSAL_RELEASES = new Map([
|
|
|
98690
98706
|
["RenderTransitionPass", 164],
|
|
98691
98707
|
["FXAAPass", 177]
|
|
98692
98708
|
]);
|
|
98709
|
+
const THREE_RENDERER_MANAGED_SHADER_UNIFORM_NAMES = new Set([
|
|
98710
|
+
"ambientLightColor",
|
|
98711
|
+
"bindMatrix",
|
|
98712
|
+
"bindMatrixInverse",
|
|
98713
|
+
"boneTexture",
|
|
98714
|
+
"boneTextureSize",
|
|
98715
|
+
"cameraPosition",
|
|
98716
|
+
"clippingPlanes",
|
|
98717
|
+
"directionalLights",
|
|
98718
|
+
"directionalLightShadows",
|
|
98719
|
+
"directionalShadowMap",
|
|
98720
|
+
"directionalShadowMatrix",
|
|
98721
|
+
"fogColor",
|
|
98722
|
+
"fogDensity",
|
|
98723
|
+
"fogFar",
|
|
98724
|
+
"fogNear",
|
|
98725
|
+
"hemisphereLights",
|
|
98726
|
+
"isOrthographic",
|
|
98727
|
+
"lightProbe",
|
|
98728
|
+
"logDepthBufFC",
|
|
98729
|
+
"ltc_1",
|
|
98730
|
+
"ltc_2",
|
|
98731
|
+
"modelMatrix",
|
|
98732
|
+
"modelViewMatrix",
|
|
98733
|
+
"morphTargetBaseInfluence",
|
|
98734
|
+
"morphTargetInfluences",
|
|
98735
|
+
"morphTargetsTexture",
|
|
98736
|
+
"morphTargetsTextureSize",
|
|
98737
|
+
"normalMatrix",
|
|
98738
|
+
"pointLights",
|
|
98739
|
+
"pointLightShadows",
|
|
98740
|
+
"pointShadowMap",
|
|
98741
|
+
"pointShadowMatrix",
|
|
98742
|
+
"probesMax",
|
|
98743
|
+
"probesMin",
|
|
98744
|
+
"probesResolution",
|
|
98745
|
+
"probesSH",
|
|
98746
|
+
"projectionMatrix",
|
|
98747
|
+
"receiveShadow",
|
|
98748
|
+
"rectAreaLights",
|
|
98749
|
+
"spotLightMap",
|
|
98750
|
+
"spotLightMatrix",
|
|
98751
|
+
"spotLights",
|
|
98752
|
+
"spotLightShadows",
|
|
98753
|
+
"spotShadowMap",
|
|
98754
|
+
"toneMappingExposure",
|
|
98755
|
+
"transmissionSamplerMap",
|
|
98756
|
+
"transmissionSamplerSize",
|
|
98757
|
+
"viewMatrix"
|
|
98758
|
+
]);
|
|
98759
|
+
const THREE_SHADER_MATERIAL_INJECTED_FRAGMENT_NAMES = new Set([
|
|
98760
|
+
"cameraPosition",
|
|
98761
|
+
"isOrthographic",
|
|
98762
|
+
"viewMatrix"
|
|
98763
|
+
]);
|
|
98764
|
+
const THREE_SHADER_MATERIAL_INJECTED_VERTEX_NAMES = new Set([
|
|
98765
|
+
"cameraPosition",
|
|
98766
|
+
"isOrthographic",
|
|
98767
|
+
"modelMatrix",
|
|
98768
|
+
"modelViewMatrix",
|
|
98769
|
+
"normal",
|
|
98770
|
+
"normalMatrix",
|
|
98771
|
+
"position",
|
|
98772
|
+
"projectionMatrix",
|
|
98773
|
+
"uv",
|
|
98774
|
+
"viewMatrix"
|
|
98775
|
+
]);
|
|
98693
98776
|
//#endregion
|
|
98694
98777
|
//#region src/plugin/rules/r3f/r3f-limit-shadowed-point-lights.ts
|
|
98695
98778
|
const resolvesToTrue = (expression, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
@@ -121827,9 +121910,9 @@ const getJsxMemberRootIdentifier = (node) => {
|
|
|
121827
121910
|
return null;
|
|
121828
121911
|
};
|
|
121829
121912
|
const getJsxMemberPropertyName = (node) => isNodeOfType(node.property, "JSXIdentifier") ? node.property.name : null;
|
|
121830
|
-
const getMemberRootIdentifier = (node) => {
|
|
121913
|
+
const getMemberRootIdentifier$1 = (node) => {
|
|
121831
121914
|
if (isNodeOfType(node.object, "Identifier")) return node.object;
|
|
121832
|
-
if (isNodeOfType(node.object, "MemberExpression")) return getMemberRootIdentifier(node.object);
|
|
121915
|
+
if (isNodeOfType(node.object, "MemberExpression")) return getMemberRootIdentifier$1(node.object);
|
|
121833
121916
|
return null;
|
|
121834
121917
|
};
|
|
121835
121918
|
const getMemberPropertyName = (node) => isNodeOfType(node.property, "Identifier") ? node.property.name : null;
|
|
@@ -121934,7 +122017,7 @@ const tanstackStartMissingScripts = defineRule({
|
|
|
121934
122017
|
return didCollectAlias;
|
|
121935
122018
|
}
|
|
121936
122019
|
if (!isNodeOfType(node.init, "MemberExpression")) return false;
|
|
121937
|
-
const rootIdentifier = getMemberRootIdentifier(node.init);
|
|
122020
|
+
const rootIdentifier = getMemberRootIdentifier$1(node.init);
|
|
121938
122021
|
const rootDeclaration = rootIdentifier ? getBindingDeclaration(rootIdentifier, context) : null;
|
|
121939
122022
|
const propertyName = getMemberPropertyName(node.init);
|
|
121940
122023
|
if (!rootDeclaration || !tanstackRouterNamespaceDeclarations.has(rootDeclaration)) return false;
|
|
@@ -121968,7 +122051,7 @@ const tanstackStartMissingScripts = defineRule({
|
|
|
121968
122051
|
return factoryDeclaration ? rootRouteFactoryDeclarations.has(factoryDeclaration) : TANSTACK_ROOT_ROUTE_FACTORY_NAMES.has(callee.name);
|
|
121969
122052
|
}
|
|
121970
122053
|
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
121971
|
-
const rootIdentifier = getMemberRootIdentifier(callee);
|
|
122054
|
+
const rootIdentifier = getMemberRootIdentifier$1(callee);
|
|
121972
122055
|
const rootDeclaration = rootIdentifier ? getBindingDeclaration(rootIdentifier, context) : null;
|
|
121973
122056
|
const propertyName = getMemberPropertyName(callee);
|
|
121974
122057
|
return Boolean(rootDeclaration && tanstackRouterNamespaceDeclarations.has(rootDeclaration) && propertyName && TANSTACK_ROOT_ROUTE_FACTORY_NAMES.has(propertyName));
|
|
@@ -122791,6 +122874,47 @@ const threeNoCloneInAnimationLoop = defineRule({
|
|
|
122791
122874
|
}
|
|
122792
122875
|
});
|
|
122793
122876
|
//#endregion
|
|
122877
|
+
//#region src/plugin/rules/r3f/utils/visit-three-animation-loop-enabled-property-writes.ts
|
|
122878
|
+
const visitThreeAnimationLoopEnabledPropertyWrites = (call, scopes, propertyNames, analyzedCallbacks, visitor) => {
|
|
122879
|
+
const callback = resolveThreeAnimationLoopCallback(call, scopes);
|
|
122880
|
+
if (!callback || analyzedCallbacks.has(callback)) return;
|
|
122881
|
+
analyzedCallbacks.add(callback);
|
|
122882
|
+
walkFunctionExecution(callback, scopes, (candidate, isConditionallyExecuted) => {
|
|
122883
|
+
if (isConditionallyExecuted || !isNodeOfType(candidate, "AssignmentExpression") || candidate.operator !== "=" || !isNodeOfType(candidate.left, "MemberExpression") || !isNodeOfType(candidate.right, "Literal") || candidate.right.value !== true) return;
|
|
122884
|
+
const propertyName = getStaticPropertyName(candidate.left);
|
|
122885
|
+
if (!propertyName || !propertyNames.has(propertyName)) return;
|
|
122886
|
+
const constructorName = getThreeConstructorName(candidate.left.object, scopes);
|
|
122887
|
+
if (!constructorName) return;
|
|
122888
|
+
visitor({
|
|
122889
|
+
constructorName,
|
|
122890
|
+
node: candidate,
|
|
122891
|
+
propertyName
|
|
122892
|
+
});
|
|
122893
|
+
});
|
|
122894
|
+
};
|
|
122895
|
+
//#endregion
|
|
122896
|
+
//#region src/plugin/rules/r3f/three-no-material-recompile-in-animation-loop.ts
|
|
122897
|
+
const MATERIAL_RECOMPILE_PROPERTY_NAMES = new Set(["needsUpdate"]);
|
|
122898
|
+
const threeNoMaterialRecompileInAnimationLoop = defineRule({
|
|
122899
|
+
id: "three-no-material-recompile-in-animation-loop",
|
|
122900
|
+
title: "Material recompiled inside animation loop",
|
|
122901
|
+
category: "Performance",
|
|
122902
|
+
severity: "error",
|
|
122903
|
+
recommendation: "Change material defines or features outside the animation loop and set needsUpdate only when the compiled program must change",
|
|
122904
|
+
create: (context) => {
|
|
122905
|
+
const analyzedCallbacks = /* @__PURE__ */ new Set();
|
|
122906
|
+
return { CallExpression(node) {
|
|
122907
|
+
visitThreeAnimationLoopEnabledPropertyWrites(node, context.scopes, MATERIAL_RECOMPILE_PROPERTY_NAMES, analyzedCallbacks, ({ constructorName, node: assignment }) => {
|
|
122908
|
+
if (!constructorName.endsWith("Material")) return;
|
|
122909
|
+
context.report({
|
|
122910
|
+
node: assignment,
|
|
122911
|
+
message: "Setting material.needsUpdate every frame forces Three.js to reconsider and often rebuild the GPU program. Set it only after a shader-affecting material change"
|
|
122912
|
+
});
|
|
122913
|
+
});
|
|
122914
|
+
} };
|
|
122915
|
+
}
|
|
122916
|
+
});
|
|
122917
|
+
//#endregion
|
|
122794
122918
|
//#region src/plugin/rules/r3f/three-no-new-in-animation-loop.ts
|
|
122795
122919
|
const threeNoNewInAnimationLoop = defineRule({
|
|
122796
122920
|
id: "three-no-new-in-animation-loop",
|
|
@@ -122840,6 +122964,28 @@ const threeNoObjectConstructionInRender = defineRule({
|
|
|
122840
122964
|
} })
|
|
122841
122965
|
});
|
|
122842
122966
|
//#endregion
|
|
122967
|
+
//#region src/plugin/rules/r3f/three-no-redundant-uniforms-need-update.ts
|
|
122968
|
+
const UNIFORM_UPDATE_PROPERTY_NAMES = new Set(["uniformsNeedUpdate"]);
|
|
122969
|
+
const threeNoRedundantUniformsNeedUpdate = defineRule({
|
|
122970
|
+
id: "three-no-redundant-uniforms-need-update",
|
|
122971
|
+
title: "Redundant uniformsNeedUpdate inside animation loop",
|
|
122972
|
+
category: "Performance",
|
|
122973
|
+
severity: "warn",
|
|
122974
|
+
recommendation: "Mutate ShaderMaterial uniform values directly; Three.js uploads custom uniforms during rendering",
|
|
122975
|
+
create: (context) => {
|
|
122976
|
+
const analyzedCallbacks = /* @__PURE__ */ new Set();
|
|
122977
|
+
return { CallExpression(node) {
|
|
122978
|
+
visitThreeAnimationLoopEnabledPropertyWrites(node, context.scopes, UNIFORM_UPDATE_PROPERTY_NAMES, analyzedCallbacks, ({ constructorName, node: assignment }) => {
|
|
122979
|
+
if (constructorName !== "ShaderMaterial" && constructorName !== "RawShaderMaterial") return;
|
|
122980
|
+
context.report({
|
|
122981
|
+
node: assignment,
|
|
122982
|
+
message: "Shader material custom uniforms are refreshed during rendering, so setting uniformsNeedUpdate on every animation frame is redundant"
|
|
122983
|
+
});
|
|
122984
|
+
});
|
|
122985
|
+
} };
|
|
122986
|
+
}
|
|
122987
|
+
});
|
|
122988
|
+
//#endregion
|
|
122843
122989
|
//#region src/plugin/rules/r3f/three-no-state-in-animation-loop.ts
|
|
122844
122990
|
const threeNoStateInAnimationLoop = defineRule({
|
|
122845
122991
|
id: "three-no-state-in-animation-loop",
|
|
@@ -122894,6 +123040,631 @@ const threeNoStateInPointerMove = defineRule({
|
|
|
122894
123040
|
}
|
|
122895
123041
|
});
|
|
122896
123042
|
//#endregion
|
|
123043
|
+
//#region src/plugin/rules/r3f/three-on-before-compile-require-program-cache-key.ts
|
|
123044
|
+
const SHADER_PROGRAM_PROPERTY_NAMES = new Set([
|
|
123045
|
+
"defines",
|
|
123046
|
+
"fragmentShader",
|
|
123047
|
+
"vertexShader"
|
|
123048
|
+
]);
|
|
123049
|
+
const MATERIAL_PROGRAM_OPTION_NAMES = ["customProgramCacheKey", "onBeforeCompile"];
|
|
123050
|
+
const isScopeWithin$1 = (candidateScope, ancestorScope) => {
|
|
123051
|
+
let currentScope = candidateScope;
|
|
123052
|
+
while (currentScope) {
|
|
123053
|
+
if (currentScope === ancestorScope) return true;
|
|
123054
|
+
currentScope = currentScope.parent;
|
|
123055
|
+
}
|
|
123056
|
+
return false;
|
|
123057
|
+
};
|
|
123058
|
+
const getMemberRootIdentifier = (expression) => {
|
|
123059
|
+
let candidate = expression;
|
|
123060
|
+
while (isNodeOfType(candidate, "MemberExpression")) candidate = candidate.object;
|
|
123061
|
+
return isNodeOfType(candidate, "Identifier") ? candidate : null;
|
|
123062
|
+
};
|
|
123063
|
+
const getShaderProgramPropertyName = (member, shaderParameterSymbol, context) => {
|
|
123064
|
+
const rootIdentifier = getMemberRootIdentifier(member);
|
|
123065
|
+
if (!rootIdentifier || context.scopes.symbolFor(rootIdentifier)?.id !== shaderParameterSymbol.id) return null;
|
|
123066
|
+
let candidate = member;
|
|
123067
|
+
let propertyName = null;
|
|
123068
|
+
while (isNodeOfType(candidate, "MemberExpression")) {
|
|
123069
|
+
propertyName = getStaticPropertyName(candidate);
|
|
123070
|
+
if (isNodeOfType(candidate.object, "Identifier")) break;
|
|
123071
|
+
candidate = candidate.object;
|
|
123072
|
+
}
|
|
123073
|
+
return propertyName && SHADER_PROGRAM_PROPERTY_NAMES.has(propertyName) ? propertyName : null;
|
|
123074
|
+
};
|
|
123075
|
+
const expressionDependsOnMutableCapture = (expression, callbackScope, context) => {
|
|
123076
|
+
const visitedLocalSymbolIds = /* @__PURE__ */ new Set();
|
|
123077
|
+
const inspectExpression = (expressionToInspect) => {
|
|
123078
|
+
let dependsOnMutableCapture = false;
|
|
123079
|
+
walkAst(expressionToInspect, (candidate) => {
|
|
123080
|
+
if (dependsOnMutableCapture || candidate !== expressionToInspect && isFunctionLike$1(candidate)) return false;
|
|
123081
|
+
if (isNodeOfType(candidate, "ThisExpression")) {
|
|
123082
|
+
dependsOnMutableCapture = true;
|
|
123083
|
+
return false;
|
|
123084
|
+
}
|
|
123085
|
+
if (isNodeOfType(candidate, "MemberExpression")) {
|
|
123086
|
+
const rootIdentifier = getMemberRootIdentifier(candidate);
|
|
123087
|
+
const symbol = rootIdentifier ? context.scopes.symbolFor(rootIdentifier) : null;
|
|
123088
|
+
if (symbol && !isScopeWithin$1(symbol.scope, callbackScope) && symbol.kind !== "import" && symbol.kind !== "function" && symbol.kind !== "class") {
|
|
123089
|
+
dependsOnMutableCapture = true;
|
|
123090
|
+
return false;
|
|
123091
|
+
}
|
|
123092
|
+
}
|
|
123093
|
+
if (!isNodeOfType(candidate, "Identifier")) return;
|
|
123094
|
+
const symbol = context.scopes.symbolFor(candidate);
|
|
123095
|
+
const hasMutableConstInitializer = symbol?.kind === "const" && Boolean(symbol.initializer && (isNodeOfType(symbol.initializer, "ArrayExpression") || isNodeOfType(symbol.initializer, "ObjectExpression") || isNodeOfType(symbol.initializer, "NewExpression")));
|
|
123096
|
+
if (symbol && !isScopeWithin$1(symbol.scope, callbackScope) && (symbol.kind === "let" || symbol.kind === "var" || symbol.kind === "parameter" || hasMutableConstInitializer)) {
|
|
123097
|
+
dependsOnMutableCapture = true;
|
|
123098
|
+
return false;
|
|
123099
|
+
}
|
|
123100
|
+
if (!symbol || !isScopeWithin$1(symbol.scope, callbackScope) || visitedLocalSymbolIds.has(symbol.id)) return;
|
|
123101
|
+
const initializer = getDirectUnreassignedInitializer(symbol);
|
|
123102
|
+
if (!initializer || isFunctionLike$1(initializer) || isNodeOfType(initializer, "ArrayExpression") || isNodeOfType(initializer, "ObjectExpression") || isNodeOfType(initializer, "NewExpression")) return;
|
|
123103
|
+
visitedLocalSymbolIds.add(symbol.id);
|
|
123104
|
+
if (inspectExpression(initializer)) {
|
|
123105
|
+
dependsOnMutableCapture = true;
|
|
123106
|
+
return false;
|
|
123107
|
+
}
|
|
123108
|
+
});
|
|
123109
|
+
return dependsOnMutableCapture;
|
|
123110
|
+
};
|
|
123111
|
+
return inspectExpression(expression);
|
|
123112
|
+
};
|
|
123113
|
+
const callbackHasVariantDependentPatch = (callback, context) => {
|
|
123114
|
+
const callbackScope = context.scopes.ownScopeFor(callback);
|
|
123115
|
+
if (!callbackScope || !isFunctionLike$1(callback)) return false;
|
|
123116
|
+
const shaderParameter = callback.params[0];
|
|
123117
|
+
if (!shaderParameter || !isNodeOfType(shaderParameter, "Identifier")) return false;
|
|
123118
|
+
const shaderParameterSymbol = context.scopes.symbolFor(shaderParameter);
|
|
123119
|
+
if (!shaderParameterSymbol) return false;
|
|
123120
|
+
const subtreeHasShaderProgramWrite = (subtree) => {
|
|
123121
|
+
let hasWrite = false;
|
|
123122
|
+
walkAst(subtree, (candidate) => {
|
|
123123
|
+
if (hasWrite || candidate !== subtree && isFunctionLike$1(candidate)) return false;
|
|
123124
|
+
if (isNodeOfType(candidate, "AssignmentExpression") && isNodeOfType(candidate.left, "MemberExpression") && getShaderProgramPropertyName(candidate.left, shaderParameterSymbol, context)) {
|
|
123125
|
+
hasWrite = true;
|
|
123126
|
+
return false;
|
|
123127
|
+
}
|
|
123128
|
+
});
|
|
123129
|
+
return hasWrite;
|
|
123130
|
+
};
|
|
123131
|
+
let hasVariantDependentPatch = false;
|
|
123132
|
+
walkAst(callback.body, (candidate) => {
|
|
123133
|
+
if (hasVariantDependentPatch || candidate !== callback.body && isFunctionLike$1(candidate)) return false;
|
|
123134
|
+
if (isNodeOfType(candidate, "AssignmentExpression") && isNodeOfType(candidate.left, "MemberExpression") && getShaderProgramPropertyName(candidate.left, shaderParameterSymbol, context) && expressionDependsOnMutableCapture(candidate.right, callbackScope, context)) {
|
|
123135
|
+
hasVariantDependentPatch = true;
|
|
123136
|
+
return false;
|
|
123137
|
+
}
|
|
123138
|
+
if (isNodeOfType(candidate, "IfStatement") && expressionDependsOnMutableCapture(candidate.test, callbackScope, context) && (subtreeHasShaderProgramWrite(candidate.consequent) || Boolean(candidate.alternate && subtreeHasShaderProgramWrite(candidate.alternate)))) {
|
|
123139
|
+
hasVariantDependentPatch = true;
|
|
123140
|
+
return false;
|
|
123141
|
+
}
|
|
123142
|
+
if (isNodeOfType(candidate, "ConditionalExpression") && expressionDependsOnMutableCapture(candidate.test, callbackScope, context) && (subtreeHasShaderProgramWrite(candidate.consequent) || subtreeHasShaderProgramWrite(candidate.alternate))) {
|
|
123143
|
+
hasVariantDependentPatch = true;
|
|
123144
|
+
return false;
|
|
123145
|
+
}
|
|
123146
|
+
if (isNodeOfType(candidate, "LogicalExpression") && expressionDependsOnMutableCapture(candidate.left, callbackScope, context) && subtreeHasShaderProgramWrite(candidate.right)) {
|
|
123147
|
+
hasVariantDependentPatch = true;
|
|
123148
|
+
return false;
|
|
123149
|
+
}
|
|
123150
|
+
if (isNodeOfType(candidate, "SwitchStatement") && (expressionDependsOnMutableCapture(candidate.discriminant, callbackScope, context) || candidate.cases.some((switchCase) => switchCase.test && expressionDependsOnMutableCapture(switchCase.test, callbackScope, context))) && candidate.cases.some((switchCase) => switchCase.consequent.some(subtreeHasShaderProgramWrite))) {
|
|
123151
|
+
hasVariantDependentPatch = true;
|
|
123152
|
+
return false;
|
|
123153
|
+
}
|
|
123154
|
+
if ((isNodeOfType(candidate, "WhileStatement") || isNodeOfType(candidate, "DoWhileStatement")) && expressionDependsOnMutableCapture(candidate.test, callbackScope, context) && subtreeHasShaderProgramWrite(candidate.body)) {
|
|
123155
|
+
hasVariantDependentPatch = true;
|
|
123156
|
+
return false;
|
|
123157
|
+
}
|
|
123158
|
+
if (isNodeOfType(candidate, "ForStatement") && candidate.test && expressionDependsOnMutableCapture(candidate.test, callbackScope, context) && subtreeHasShaderProgramWrite(candidate.body)) {
|
|
123159
|
+
hasVariantDependentPatch = true;
|
|
123160
|
+
return false;
|
|
123161
|
+
}
|
|
123162
|
+
if ((isNodeOfType(candidate, "ForInStatement") || isNodeOfType(candidate, "ForOfStatement")) && expressionDependsOnMutableCapture(candidate.right, callbackScope, context) && subtreeHasShaderProgramWrite(candidate.body)) {
|
|
123163
|
+
hasVariantDependentPatch = true;
|
|
123164
|
+
return false;
|
|
123165
|
+
}
|
|
123166
|
+
});
|
|
123167
|
+
return hasVariantDependentPatch;
|
|
123168
|
+
};
|
|
123169
|
+
const getStableMaterialSymbol = (expression, context) => {
|
|
123170
|
+
if (!isNodeOfType(expression, "Identifier")) return null;
|
|
123171
|
+
const symbol = context.scopes.symbolFor(expression);
|
|
123172
|
+
const initializer = symbol ? getDirectUnreassignedInitializer(symbol) : null;
|
|
123173
|
+
if (!symbol || !initializer || !getThreeConstructorName(initializer, context.scopes)?.endsWith("Material")) return null;
|
|
123174
|
+
return symbol;
|
|
123175
|
+
};
|
|
123176
|
+
const getConstructorMaterialSymbol = (node, context) => {
|
|
123177
|
+
const parent = node.parent;
|
|
123178
|
+
if (!isNodeOfType(parent, "VariableDeclarator") || parent.init !== node || !isNodeOfType(parent.id, "Identifier")) return null;
|
|
123179
|
+
const symbol = context.scopes.symbolFor(parent.id);
|
|
123180
|
+
return symbol && getDirectUnreassignedInitializer(symbol) === node ? symbol : null;
|
|
123181
|
+
};
|
|
123182
|
+
const isUsableProgramCacheKey = (expression, context) => {
|
|
123183
|
+
if (resolveExactLocalFunction(expression, context.scopes)) return true;
|
|
123184
|
+
let candidate = stripParenExpression(expression);
|
|
123185
|
+
if (isNodeOfType(candidate, "Identifier")) {
|
|
123186
|
+
if (candidate.name === "undefined" && context.scopes.isGlobalReference(candidate)) return false;
|
|
123187
|
+
const symbol = resolveConstIdentifierAlias(candidate, context.scopes);
|
|
123188
|
+
if (symbol?.kind !== "const" || !symbol.initializer) return true;
|
|
123189
|
+
candidate = stripParenExpression(symbol.initializer);
|
|
123190
|
+
}
|
|
123191
|
+
return !(isNodeOfType(candidate, "ArrayExpression") || isNodeOfType(candidate, "ClassExpression") || isNodeOfType(candidate, "Literal") || isNodeOfType(candidate, "ObjectExpression") || isNodeOfType(candidate, "TemplateLiteral") || isNodeOfType(candidate, "UnaryExpression"));
|
|
123192
|
+
};
|
|
123193
|
+
const threeOnBeforeCompileRequireProgramCacheKey = defineRule({
|
|
123194
|
+
id: "three-on-before-compile-require-program-cache-key",
|
|
123195
|
+
title: "onBeforeCompile variant lacks a program cache key",
|
|
123196
|
+
category: "Correctness",
|
|
123197
|
+
severity: "error",
|
|
123198
|
+
recommendation: "Return every mutable shader-patch variant from customProgramCacheKey on the same material",
|
|
123199
|
+
create: (context) => {
|
|
123200
|
+
const candidates = [];
|
|
123201
|
+
const materialSymbolsWithCacheKeys = /* @__PURE__ */ new Set();
|
|
123202
|
+
return {
|
|
123203
|
+
NewExpression(node) {
|
|
123204
|
+
if (!getThreeConstructorName(node, context.scopes)?.endsWith("Material")) return;
|
|
123205
|
+
const options = node.arguments[0];
|
|
123206
|
+
if (!options || isNodeOfType(options, "SpreadElement")) return;
|
|
123207
|
+
const optionsObject = resolveStableOptionsObject(options, MATERIAL_PROGRAM_OPTION_NAMES, context.scopes, node);
|
|
123208
|
+
if (!optionsObject) return;
|
|
123209
|
+
const properties = getEffectiveObjectPropertiesInInsertionOrder(optionsObject.properties);
|
|
123210
|
+
if (!properties) return;
|
|
123211
|
+
const cacheKeyProperty = properties.find((property) => getStaticPropertyKeyName(property, { allowComputedString: true }) === "customProgramCacheKey");
|
|
123212
|
+
const onBeforeCompileProperty = properties.find((property) => getStaticPropertyKeyName(property, { allowComputedString: true }) === "onBeforeCompile");
|
|
123213
|
+
const materialSymbol = getConstructorMaterialSymbol(node, context);
|
|
123214
|
+
const hasConstructorCacheKey = Boolean(cacheKeyProperty && isUsableProgramCacheKey(cacheKeyProperty.value, context));
|
|
123215
|
+
if (hasConstructorCacheKey && materialSymbol) materialSymbolsWithCacheKeys.add(materialSymbol.id);
|
|
123216
|
+
if (!onBeforeCompileProperty) return;
|
|
123217
|
+
const callback = resolveExactLocalFunction(onBeforeCompileProperty.value, context.scopes);
|
|
123218
|
+
if (!callback || !callbackHasVariantDependentPatch(callback, context)) return;
|
|
123219
|
+
candidates.push({
|
|
123220
|
+
hasConstructorCacheKey,
|
|
123221
|
+
materialSymbolId: materialSymbol?.id ?? null,
|
|
123222
|
+
node: onBeforeCompileProperty
|
|
123223
|
+
});
|
|
123224
|
+
},
|
|
123225
|
+
AssignmentExpression(node) {
|
|
123226
|
+
if (!isNodeOfType(node.left, "MemberExpression")) return;
|
|
123227
|
+
const propertyName = getStaticPropertyName(node.left);
|
|
123228
|
+
if (propertyName !== "onBeforeCompile" && propertyName !== "customProgramCacheKey") return;
|
|
123229
|
+
const materialSymbol = getStableMaterialSymbol(node.left.object, context);
|
|
123230
|
+
if (!materialSymbol) return;
|
|
123231
|
+
if (propertyName === "customProgramCacheKey") {
|
|
123232
|
+
if (isUsableProgramCacheKey(node.right, context)) materialSymbolsWithCacheKeys.add(materialSymbol.id);
|
|
123233
|
+
else materialSymbolsWithCacheKeys.delete(materialSymbol.id);
|
|
123234
|
+
return;
|
|
123235
|
+
}
|
|
123236
|
+
const callback = resolveExactLocalFunction(node.right, context.scopes);
|
|
123237
|
+
if (!callback || !callbackHasVariantDependentPatch(callback, context)) return;
|
|
123238
|
+
candidates.push({
|
|
123239
|
+
hasConstructorCacheKey: false,
|
|
123240
|
+
materialSymbolId: materialSymbol.id,
|
|
123241
|
+
node
|
|
123242
|
+
});
|
|
123243
|
+
},
|
|
123244
|
+
"Program:exit"() {
|
|
123245
|
+
for (const candidate of candidates) {
|
|
123246
|
+
if (candidate.materialSymbolId === null && candidate.hasConstructorCacheKey || candidate.materialSymbolId !== null && materialSymbolsWithCacheKeys.has(candidate.materialSymbolId)) continue;
|
|
123247
|
+
context.report({
|
|
123248
|
+
node: candidate.node,
|
|
123249
|
+
message: "This onBeforeCompile patch changes shader source from mutable captured state, but the material has no customProgramCacheKey for those program variants"
|
|
123250
|
+
});
|
|
123251
|
+
}
|
|
123252
|
+
}
|
|
123253
|
+
};
|
|
123254
|
+
}
|
|
123255
|
+
});
|
|
123256
|
+
//#endregion
|
|
123257
|
+
//#region src/plugin/rules/r3f/utils/get-glsl-type-specifier-name.ts
|
|
123258
|
+
const getGlslTypeSpecifierName = (specifier) => {
|
|
123259
|
+
const typeName = specifier.specifier;
|
|
123260
|
+
if (typeName.type === "keyword") return typeName.token;
|
|
123261
|
+
if (typeName.type === "identifier" || typeName.type === "type_name") return typeName.identifier;
|
|
123262
|
+
return null;
|
|
123263
|
+
};
|
|
123264
|
+
//#endregion
|
|
123265
|
+
//#region src/plugin/rules/r3f/utils/mask-glsl-comments.ts
|
|
123266
|
+
const GLSL_COMMENT_PATTERN = /\/\*[\s\S]*?(?:\*\/|$)|\/\/[^\r\n]*/g;
|
|
123267
|
+
const NON_LINE_BREAK_PATTERN = /[^\r\n]/g;
|
|
123268
|
+
const maskGlslComments = (source) => source.replace(GLSL_COMMENT_PATTERN, (comment) => comment.replace(NON_LINE_BREAK_PATTERN, " "));
|
|
123269
|
+
//#endregion
|
|
123270
|
+
//#region src/plugin/rules/r3f/utils/parse-glsl-shader-source.ts
|
|
123271
|
+
const GLSL_CONDITIONAL_DIRECTIVE_PATTERN = /^[ \t]*#[ \t]*(?:if|ifdef|ifndef|elif|else|endif)\b/m;
|
|
123272
|
+
const THREE_INCLUDE_DIRECTIVE_PATTERN = /^[ \t]*#[ \t]*include[ \t]+<[^>\r\n]+>[^\r\n]*/gm;
|
|
123273
|
+
const maskThreeIncludeDirectives = (source) => source.replace(THREE_INCLUDE_DIRECTIVE_PATTERN, (directive) => directive.replace(/[^\r\n]/g, " "));
|
|
123274
|
+
const parseGlslShaderSource = (source, stage) => {
|
|
123275
|
+
const sourceWithoutComments = maskGlslComments(source);
|
|
123276
|
+
if (GLSL_CONDITIONAL_DIRECTIVE_PATTERN.test(sourceWithoutComments)) return null;
|
|
123277
|
+
try {
|
|
123278
|
+
return parse(maskThreeIncludeDirectives(sourceWithoutComments), {
|
|
123279
|
+
includeLocation: true,
|
|
123280
|
+
quiet: true,
|
|
123281
|
+
stage
|
|
123282
|
+
});
|
|
123283
|
+
} catch {
|
|
123284
|
+
return null;
|
|
123285
|
+
}
|
|
123286
|
+
};
|
|
123287
|
+
//#endregion
|
|
123288
|
+
//#region src/plugin/rules/r3f/utils/resolve-static-shader-source.ts
|
|
123289
|
+
const shiftSegments = (source, offset) => source.segments.map((segment) => ({
|
|
123290
|
+
endOffset: segment.endOffset + offset,
|
|
123291
|
+
node: segment.node,
|
|
123292
|
+
startOffset: segment.startOffset + offset
|
|
123293
|
+
}));
|
|
123294
|
+
const combineStaticShaderSources = (sources, separator, fallbackNode) => {
|
|
123295
|
+
let text = "";
|
|
123296
|
+
const segments = [];
|
|
123297
|
+
sources.forEach((source, sourceIndex) => {
|
|
123298
|
+
if (sourceIndex > 0 && separator) {
|
|
123299
|
+
segments.push(...shiftSegments(separator, text.length));
|
|
123300
|
+
text += separator.text;
|
|
123301
|
+
}
|
|
123302
|
+
segments.push(...shiftSegments(source, text.length));
|
|
123303
|
+
text += source.text;
|
|
123304
|
+
});
|
|
123305
|
+
return {
|
|
123306
|
+
fallbackNode,
|
|
123307
|
+
segments,
|
|
123308
|
+
text
|
|
123309
|
+
};
|
|
123310
|
+
};
|
|
123311
|
+
const resolveStaticShaderSourceExpression = (expression, scopes, visitedSymbolIds) => {
|
|
123312
|
+
const candidate = stripParenExpression(expression);
|
|
123313
|
+
const directStaticString = getStaticStringExpression$1(candidate);
|
|
123314
|
+
if (directStaticString !== null) return {
|
|
123315
|
+
fallbackNode: candidate,
|
|
123316
|
+
segments: directStaticString.length === 0 ? [] : [{
|
|
123317
|
+
endOffset: directStaticString.length,
|
|
123318
|
+
node: candidate,
|
|
123319
|
+
startOffset: 0
|
|
123320
|
+
}],
|
|
123321
|
+
text: directStaticString
|
|
123322
|
+
};
|
|
123323
|
+
if (isNodeOfType(candidate, "Identifier")) {
|
|
123324
|
+
const symbol = scopes.symbolFor(candidate);
|
|
123325
|
+
if (symbol?.kind !== "const" || !symbol.initializer || visitedSymbolIds.has(symbol.id) || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return null;
|
|
123326
|
+
const nextVisitedSymbolIds = new Set(visitedSymbolIds);
|
|
123327
|
+
nextVisitedSymbolIds.add(symbol.id);
|
|
123328
|
+
return resolveStaticShaderSourceExpression(symbol.initializer, scopes, nextVisitedSymbolIds);
|
|
123329
|
+
}
|
|
123330
|
+
if (isNodeOfType(candidate, "BinaryExpression") && candidate.operator === "+") {
|
|
123331
|
+
const leftSource = resolveStaticShaderSourceExpression(candidate.left, scopes, new Set(visitedSymbolIds));
|
|
123332
|
+
const rightSource = resolveStaticShaderSourceExpression(candidate.right, scopes, new Set(visitedSymbolIds));
|
|
123333
|
+
return leftSource && rightSource ? combineStaticShaderSources([leftSource, rightSource], null, candidate) : null;
|
|
123334
|
+
}
|
|
123335
|
+
if (!isNodeOfType(candidate, "CallExpression") || !isNodeOfType(candidate.callee, "MemberExpression") || getStaticPropertyName(candidate.callee) !== "join") return null;
|
|
123336
|
+
const arrayExpression = stripParenExpression(candidate.callee.object);
|
|
123337
|
+
if (!isNodeOfType(arrayExpression, "ArrayExpression") || candidate.arguments.length > 1) return null;
|
|
123338
|
+
const separatorArgument = candidate.arguments[0];
|
|
123339
|
+
let separator;
|
|
123340
|
+
if (!separatorArgument) separator = {
|
|
123341
|
+
fallbackNode: candidate,
|
|
123342
|
+
segments: [],
|
|
123343
|
+
text: ","
|
|
123344
|
+
};
|
|
123345
|
+
else if (isNodeOfType(separatorArgument, "SpreadElement")) separator = null;
|
|
123346
|
+
else separator = resolveStaticShaderSourceExpression(separatorArgument, scopes, new Set(visitedSymbolIds));
|
|
123347
|
+
if (!separator) return null;
|
|
123348
|
+
const elementSources = [];
|
|
123349
|
+
for (const element of arrayExpression.elements) {
|
|
123350
|
+
if (!element || isNodeOfType(element, "SpreadElement")) return null;
|
|
123351
|
+
const elementSource = resolveStaticShaderSourceExpression(element, scopes, new Set(visitedSymbolIds));
|
|
123352
|
+
if (!elementSource) return null;
|
|
123353
|
+
elementSources.push(elementSource);
|
|
123354
|
+
}
|
|
123355
|
+
return combineStaticShaderSources(elementSources, separator, candidate);
|
|
123356
|
+
};
|
|
123357
|
+
const resolveStaticShaderSource = (expression, scopes) => {
|
|
123358
|
+
const source = resolveStaticShaderSourceExpression(expression, scopes, /* @__PURE__ */ new Set());
|
|
123359
|
+
if (!source) return null;
|
|
123360
|
+
return {
|
|
123361
|
+
getOriginNodeAtOffset: (offset) => source.segments.find((segment) => offset >= segment.startOffset && offset < segment.endOffset)?.node ?? source.fallbackNode,
|
|
123362
|
+
text: source.text
|
|
123363
|
+
};
|
|
123364
|
+
};
|
|
123365
|
+
//#endregion
|
|
123366
|
+
//#region src/plugin/rules/r3f/utils/resolve-static-three-shader-material.ts
|
|
123367
|
+
const SHADER_MATERIAL_CONSTRUCTORS = new Set(["RawShaderMaterial", "ShaderMaterial"]);
|
|
123368
|
+
const STATIC_SHADER_MATERIAL_PROPERTY_NAMES = [
|
|
123369
|
+
"clipping",
|
|
123370
|
+
"fog",
|
|
123371
|
+
"fragmentShader",
|
|
123372
|
+
"glslVersion",
|
|
123373
|
+
"lights",
|
|
123374
|
+
"morphNormals",
|
|
123375
|
+
"morphTargets",
|
|
123376
|
+
"skinning",
|
|
123377
|
+
"transmission",
|
|
123378
|
+
"uniforms",
|
|
123379
|
+
"vertexShader"
|
|
123380
|
+
];
|
|
123381
|
+
const resolveShaderStage = (expression, stage, context) => {
|
|
123382
|
+
if (!expression) return null;
|
|
123383
|
+
const source = resolveStaticShaderSource(expression, context.scopes);
|
|
123384
|
+
if (!source) return null;
|
|
123385
|
+
const program = parseGlslShaderSource(source.text, stage);
|
|
123386
|
+
return program ? {
|
|
123387
|
+
expression,
|
|
123388
|
+
program,
|
|
123389
|
+
source,
|
|
123390
|
+
stage
|
|
123391
|
+
} : null;
|
|
123392
|
+
};
|
|
123393
|
+
const resolveStaticThreeShaderMaterial = (node, context) => {
|
|
123394
|
+
const provenance = getApiReferenceProvenance(node.callee, context.scopes);
|
|
123395
|
+
if (!provenance || !isThreeModuleSource$1(provenance.moduleSource) || !SHADER_MATERIAL_CONSTRUCTORS.has(provenance.apiName)) return null;
|
|
123396
|
+
const options = node.arguments[0];
|
|
123397
|
+
if (!options || isNodeOfType(options, "SpreadElement")) return null;
|
|
123398
|
+
const optionsObject = resolveStableOptionsObject(options, STATIC_SHADER_MATERIAL_PROPERTY_NAMES, context.scopes, node);
|
|
123399
|
+
if (!optionsObject) return null;
|
|
123400
|
+
const unresolvedPropertyNames = new Set(STATIC_SHADER_MATERIAL_PROPERTY_NAMES);
|
|
123401
|
+
const properties = /* @__PURE__ */ new Map();
|
|
123402
|
+
for (let propertyIndex = optionsObject.properties.length - 1; propertyIndex >= 0; propertyIndex -= 1) {
|
|
123403
|
+
const property = optionsObject.properties[propertyIndex];
|
|
123404
|
+
if (!property || isNodeOfType(property, "SpreadElement")) return null;
|
|
123405
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
123406
|
+
const propertyName = getStaticPropertyKeyName(property, { allowComputedString: true });
|
|
123407
|
+
if (!propertyName) return null;
|
|
123408
|
+
if (!unresolvedPropertyNames.has(propertyName)) continue;
|
|
123409
|
+
unresolvedPropertyNames.delete(propertyName);
|
|
123410
|
+
if (property.kind === "init" && !property.method) properties.set(propertyName, property.value);
|
|
123411
|
+
}
|
|
123412
|
+
const fragmentShaderExpression = properties.get("fragmentShader");
|
|
123413
|
+
const vertexShaderExpression = properties.get("vertexShader");
|
|
123414
|
+
return {
|
|
123415
|
+
constructorName: provenance.apiName,
|
|
123416
|
+
constructorNode: node,
|
|
123417
|
+
fragmentShader: resolveShaderStage(fragmentShaderExpression, "fragment", context),
|
|
123418
|
+
optionsObject,
|
|
123419
|
+
properties,
|
|
123420
|
+
vertexShader: resolveShaderStage(vertexShaderExpression, "vertex", context)
|
|
123421
|
+
};
|
|
123422
|
+
};
|
|
123423
|
+
//#endregion
|
|
123424
|
+
//#region src/plugin/rules/r3f/three-raw-shader-require-fragment-float-precision.ts
|
|
123425
|
+
const FLOAT_TYPE_NAME_PATTERN = /^(?:float|vec[234]|mat[234](?:x[234])?)$/;
|
|
123426
|
+
const PRECISION_QUALIFIER_NAMES$1 = new Set([
|
|
123427
|
+
"highp",
|
|
123428
|
+
"lowp",
|
|
123429
|
+
"mediump"
|
|
123430
|
+
]);
|
|
123431
|
+
const hasPrecisionQualifier = (qualifiers) => Boolean(qualifiers?.some((qualifier) => qualifier.type === "keyword" && PRECISION_QUALIFIER_NAMES$1.has(qualifier.token)));
|
|
123432
|
+
const threeRawShaderRequireFragmentFloatPrecision = defineRule({
|
|
123433
|
+
id: "three-raw-shader-require-fragment-float-precision",
|
|
123434
|
+
title: "Raw fragment shader lacks float precision",
|
|
123435
|
+
category: "Correctness",
|
|
123436
|
+
severity: "error",
|
|
123437
|
+
recommendation: "Declare a default fragment float precision or qualify every floating-point declaration",
|
|
123438
|
+
create: (context) => ({ NewExpression(node) {
|
|
123439
|
+
const material = resolveStaticThreeShaderMaterial(node, context);
|
|
123440
|
+
if (material?.constructorName !== "RawShaderMaterial" || !material.fragmentShader) return;
|
|
123441
|
+
let hasDefaultFloatPrecision = false;
|
|
123442
|
+
const unqualifiedFloatNodes = [];
|
|
123443
|
+
visit(material.fragmentShader.program, {
|
|
123444
|
+
precision: { enter: ({ node: precision }) => {
|
|
123445
|
+
if (getGlslTypeSpecifierName(precision.specifier) === "float") hasDefaultFloatPrecision = true;
|
|
123446
|
+
} },
|
|
123447
|
+
fully_specified_type: { enter: ({ node: specifiedType }) => {
|
|
123448
|
+
const typeName = getGlslTypeSpecifierName(specifiedType.specifier);
|
|
123449
|
+
if (unqualifiedFloatNodes.length === 0 && !hasDefaultFloatPrecision && typeName && FLOAT_TYPE_NAME_PATTERN.test(typeName) && !hasPrecisionQualifier(specifiedType.qualifiers)) unqualifiedFloatNodes.push(specifiedType);
|
|
123450
|
+
} },
|
|
123451
|
+
parameter_declaration: { enter: ({ node: parameter }) => {
|
|
123452
|
+
const typeName = getGlslTypeSpecifierName(parameter.specifier);
|
|
123453
|
+
if (unqualifiedFloatNodes.length === 0 && !hasDefaultFloatPrecision && typeName && FLOAT_TYPE_NAME_PATTERN.test(typeName) && !hasPrecisionQualifier(parameter.qualifier)) unqualifiedFloatNodes.push(parameter);
|
|
123454
|
+
} }
|
|
123455
|
+
});
|
|
123456
|
+
const firstUnqualifiedFloatNode = unqualifiedFloatNodes[0];
|
|
123457
|
+
if (!firstUnqualifiedFloatNode) return;
|
|
123458
|
+
context.report({
|
|
123459
|
+
node: material.fragmentShader.source.getOriginNodeAtOffset(firstUnqualifiedFloatNode.location?.start.offset ?? 0),
|
|
123460
|
+
message: "RawShaderMaterial does not receive Three.js precision declarations, and fragment GLSL has no default float precision for this unqualified declaration"
|
|
123461
|
+
});
|
|
123462
|
+
} })
|
|
123463
|
+
});
|
|
123464
|
+
//#endregion
|
|
123465
|
+
//#region src/plugin/rules/r3f/utils/get-glsl-numeric-constant.ts
|
|
123466
|
+
const getGlslNumericConstant = (node) => {
|
|
123467
|
+
let value = null;
|
|
123468
|
+
if (node.type === "float_constant") value = Number(node.token.replace(/(?:lf|LF|f|F)$/, ""));
|
|
123469
|
+
else if (node.type === "double_constant") value = Number(node.token.replace(/(?:lf|LF)$/, ""));
|
|
123470
|
+
else if (node.type === "uint_constant") value = Number(node.token.replace(/[uU]$/, ""));
|
|
123471
|
+
else if (node.type === "int_constant") value = Number(node.token);
|
|
123472
|
+
else if (node.type === "group") value = getGlslNumericConstant(node.expression);
|
|
123473
|
+
else if (node.type === "unary" && (node.operator.literal === "+" || node.operator.literal === "-")) {
|
|
123474
|
+
const operand = getGlslNumericConstant(node.expression);
|
|
123475
|
+
if (operand !== null) value = node.operator.literal === "-" ? -operand : operand;
|
|
123476
|
+
}
|
|
123477
|
+
return value !== null && Number.isFinite(value) ? value : null;
|
|
123478
|
+
};
|
|
123479
|
+
//#endregion
|
|
123480
|
+
//#region src/plugin/rules/r3f/utils/collect-glsl-global-declarations.ts
|
|
123481
|
+
const GLSL_INTERPOLATION_QUALIFIER_NAMES = new Set([
|
|
123482
|
+
"centroid",
|
|
123483
|
+
"flat",
|
|
123484
|
+
"noperspective",
|
|
123485
|
+
"sample",
|
|
123486
|
+
"smooth"
|
|
123487
|
+
]);
|
|
123488
|
+
const GLSL_PRIMARY_INTERPOLATION_QUALIFIER_NAMES = new Set([
|
|
123489
|
+
"flat",
|
|
123490
|
+
"noperspective",
|
|
123491
|
+
"smooth"
|
|
123492
|
+
]);
|
|
123493
|
+
const getArraySize = (declaration, typeQuantifiers) => {
|
|
123494
|
+
const quantifiers = [...Array.isArray(typeQuantifiers) ? typeQuantifiers : [], ...Array.isArray(declaration.quantifier) ? declaration.quantifier : []];
|
|
123495
|
+
if (quantifiers.length === 0) return null;
|
|
123496
|
+
if (quantifiers.length !== 1) return void 0;
|
|
123497
|
+
return getGlslNumericConstant(quantifiers[0].expression) ?? void 0;
|
|
123498
|
+
};
|
|
123499
|
+
const getQualifierName = (qualifier) => qualifier.type === "keyword" ? qualifier.token : null;
|
|
123500
|
+
const getInterpolation = (qualifiers) => {
|
|
123501
|
+
const interpolationQualifiers = [...qualifiers].filter((qualifier) => GLSL_INTERPOLATION_QUALIFIER_NAMES.has(qualifier));
|
|
123502
|
+
if (!interpolationQualifiers.some((qualifier) => GLSL_PRIMARY_INTERPOLATION_QUALIFIER_NAMES.has(qualifier))) interpolationQualifiers.push("smooth");
|
|
123503
|
+
return interpolationQualifiers.sort().join(" ");
|
|
123504
|
+
};
|
|
123505
|
+
const collectGlslGlobalDeclarations = (program) => {
|
|
123506
|
+
const declarations = [];
|
|
123507
|
+
const globalBindings = program.scopes[0]?.bindings;
|
|
123508
|
+
for (const statement of program.program) {
|
|
123509
|
+
if (statement.type !== "declaration_statement" || statement.declaration.type !== "declarator_list") continue;
|
|
123510
|
+
const declaratorList = statement.declaration;
|
|
123511
|
+
const typeName = getGlslTypeSpecifierName(declaratorList.specified_type.specifier);
|
|
123512
|
+
if (!typeName) continue;
|
|
123513
|
+
const qualifierNodes = declaratorList.specified_type.qualifiers ?? [];
|
|
123514
|
+
const qualifiers = /* @__PURE__ */ new Set();
|
|
123515
|
+
let hasLayoutQualifier = false;
|
|
123516
|
+
for (const qualifier of qualifierNodes) {
|
|
123517
|
+
if (qualifier.type === "layout_qualifier") {
|
|
123518
|
+
hasLayoutQualifier = true;
|
|
123519
|
+
continue;
|
|
123520
|
+
}
|
|
123521
|
+
const qualifierName = getQualifierName(qualifier);
|
|
123522
|
+
if (qualifierName) qualifiers.add(qualifierName);
|
|
123523
|
+
}
|
|
123524
|
+
for (const declaration of declaratorList.declarations) {
|
|
123525
|
+
const name = declaration.identifier.identifier;
|
|
123526
|
+
const binding = globalBindings?.[name];
|
|
123527
|
+
declarations.push({
|
|
123528
|
+
arraySize: getArraySize(declaration, declaratorList.specified_type.specifier.quantifier),
|
|
123529
|
+
hasLayoutQualifier,
|
|
123530
|
+
interpolation: getInterpolation(qualifiers),
|
|
123531
|
+
isStaticallyUsed: Boolean(binding?.references.some((reference) => reference !== binding.declaration)),
|
|
123532
|
+
name,
|
|
123533
|
+
node: declaration,
|
|
123534
|
+
qualifiers,
|
|
123535
|
+
typeName
|
|
123536
|
+
});
|
|
123537
|
+
}
|
|
123538
|
+
}
|
|
123539
|
+
return declarations;
|
|
123540
|
+
};
|
|
123541
|
+
//#endregion
|
|
123542
|
+
//#region src/plugin/rules/r3f/utils/get-glsl-function-call-name.ts
|
|
123543
|
+
const getGlslFunctionCallName = (node) => {
|
|
123544
|
+
if (node.identifier.type === "identifier") return node.identifier.identifier;
|
|
123545
|
+
if (node.identifier.type !== "type_specifier") return null;
|
|
123546
|
+
const specifier = node.identifier.specifier;
|
|
123547
|
+
if (specifier.type === "keyword") return specifier.token;
|
|
123548
|
+
return specifier.type === "identifier" || specifier.type === "type_name" ? specifier.identifier : null;
|
|
123549
|
+
};
|
|
123550
|
+
//#endregion
|
|
123551
|
+
//#region src/plugin/rules/r3f/utils/has-glsl-function-declaration.ts
|
|
123552
|
+
const hasGlslFunctionDeclaration = (program, functionName) => {
|
|
123553
|
+
const overloads = program.scopes[0]?.functions[functionName];
|
|
123554
|
+
return Boolean(overloads && Object.values(overloads).some((overload) => Boolean(overload.declaration)));
|
|
123555
|
+
};
|
|
123556
|
+
//#endregion
|
|
123557
|
+
//#region src/plugin/rules/r3f/utils/has-glsl-function-like-macro.ts
|
|
123558
|
+
const hasGlslFunctionLikeMacro = (source, functionName) => new RegExp(`^[ \\t]*#[ \\t]*define[ \\t]+${functionName}(?:[ \\t]|\\()`, "m").test(maskGlslComments(source));
|
|
123559
|
+
//#endregion
|
|
123560
|
+
//#region src/plugin/rules/r3f/three-raw-shader-require-glsl3-version.ts
|
|
123561
|
+
const GLSL3_ONLY_FUNCTION_NAMES = new Set([
|
|
123562
|
+
"acosh",
|
|
123563
|
+
"asinh",
|
|
123564
|
+
"atanh",
|
|
123565
|
+
"bitCount",
|
|
123566
|
+
"bitfieldExtract",
|
|
123567
|
+
"bitfieldInsert",
|
|
123568
|
+
"bitfieldReverse",
|
|
123569
|
+
"cosh",
|
|
123570
|
+
"determinant",
|
|
123571
|
+
"findLSB",
|
|
123572
|
+
"findMSB",
|
|
123573
|
+
"floatBitsToInt",
|
|
123574
|
+
"floatBitsToUint",
|
|
123575
|
+
"frexp",
|
|
123576
|
+
"imulExtended",
|
|
123577
|
+
"intBitsToFloat",
|
|
123578
|
+
"interpolateAtCentroid",
|
|
123579
|
+
"interpolateAtOffset",
|
|
123580
|
+
"interpolateAtSample",
|
|
123581
|
+
"inverse",
|
|
123582
|
+
"isinf",
|
|
123583
|
+
"isnan",
|
|
123584
|
+
"ldexp",
|
|
123585
|
+
"modf",
|
|
123586
|
+
"outerProduct",
|
|
123587
|
+
"packHalf2x16",
|
|
123588
|
+
"packSnorm2x16",
|
|
123589
|
+
"packUnorm2x16",
|
|
123590
|
+
"round",
|
|
123591
|
+
"roundEven",
|
|
123592
|
+
"sinh",
|
|
123593
|
+
"tanh",
|
|
123594
|
+
"texelFetch",
|
|
123595
|
+
"texelFetchOffset",
|
|
123596
|
+
"texture",
|
|
123597
|
+
"textureGrad",
|
|
123598
|
+
"textureGradOffset",
|
|
123599
|
+
"textureLod",
|
|
123600
|
+
"textureLodOffset",
|
|
123601
|
+
"textureOffset",
|
|
123602
|
+
"textureProj",
|
|
123603
|
+
"textureProjGrad",
|
|
123604
|
+
"textureProjGradOffset",
|
|
123605
|
+
"textureProjLod",
|
|
123606
|
+
"textureProjLodOffset",
|
|
123607
|
+
"textureProjOffset",
|
|
123608
|
+
"textureSize",
|
|
123609
|
+
"transpose",
|
|
123610
|
+
"trunc",
|
|
123611
|
+
"uaddCarry",
|
|
123612
|
+
"uintBitsToFloat",
|
|
123613
|
+
"umulExtended",
|
|
123614
|
+
"unpackHalf2x16",
|
|
123615
|
+
"unpackSnorm2x16",
|
|
123616
|
+
"unpackUnorm2x16",
|
|
123617
|
+
"usubBorrow"
|
|
123618
|
+
]);
|
|
123619
|
+
const GLSL3_ONLY_TYPE_NAME_PATTERN = /^(?:uint|uvec[234]|mat[234]x[234]|sampler3D|sampler2DArray|[iu]sampler(?:2D|3D|Cube|2DArray))$/;
|
|
123620
|
+
const GLSL3_ONLY_CONSTRUCTOR_NAME_PATTERN = /^(?:uint|uvec[234]|mat[234]x[234])$/;
|
|
123621
|
+
const shaderUsesGlsl3Syntax = (shader) => {
|
|
123622
|
+
if (collectGlslGlobalDeclarations(shader.program).some((declaration) => declaration.hasLayoutQualifier || declaration.qualifiers.has("in") || declaration.qualifiers.has("out") || GLSL3_ONLY_TYPE_NAME_PATTERN.test(declaration.typeName))) return true;
|
|
123623
|
+
let usesGlsl3Syntax = false;
|
|
123624
|
+
visit(shader.program, {
|
|
123625
|
+
binary: { enter: ({ node }) => {
|
|
123626
|
+
if (node.operator.literal === "<<" || node.operator.literal === ">>" || node.operator.literal === "&" || node.operator.literal === "|" || node.operator.literal === "^") usesGlsl3Syntax = true;
|
|
123627
|
+
} },
|
|
123628
|
+
assignment: { enter: ({ node }) => {
|
|
123629
|
+
const operatorLiteral = String(node.operator.literal);
|
|
123630
|
+
if (operatorLiteral === "<<=" || operatorLiteral === ">>=" || operatorLiteral === "&=" || operatorLiteral === "|=" || operatorLiteral === "^=") usesGlsl3Syntax = true;
|
|
123631
|
+
} },
|
|
123632
|
+
function_call: { enter: ({ node }) => {
|
|
123633
|
+
const functionName = getGlslFunctionCallName(node);
|
|
123634
|
+
if (functionName && (GLSL3_ONLY_FUNCTION_NAMES.has(functionName) || GLSL3_ONLY_CONSTRUCTOR_NAME_PATTERN.test(functionName)) && !hasGlslFunctionLikeMacro(shader.source.text, functionName) && !hasGlslFunctionDeclaration(shader.program, functionName)) usesGlsl3Syntax = true;
|
|
123635
|
+
} },
|
|
123636
|
+
switch_statement: { enter: () => {
|
|
123637
|
+
usesGlsl3Syntax = true;
|
|
123638
|
+
} }
|
|
123639
|
+
});
|
|
123640
|
+
return usesGlsl3Syntax;
|
|
123641
|
+
};
|
|
123642
|
+
const getGlsl3Configuration = (expression, context) => {
|
|
123643
|
+
if (!expression) return false;
|
|
123644
|
+
if (isNodeOfType(expression, "Literal")) return expression.value === "300 es";
|
|
123645
|
+
const provenance = getApiReferenceProvenance(expression, context.scopes);
|
|
123646
|
+
if (!provenance || !isThreeModuleSource$1(provenance.moduleSource)) return null;
|
|
123647
|
+
return provenance.apiName === "GLSL3";
|
|
123648
|
+
};
|
|
123649
|
+
const threeRawShaderRequireGlsl3Version = defineRule({
|
|
123650
|
+
id: "three-raw-shader-require-glsl3-version",
|
|
123651
|
+
title: "Raw shader uses GLSL 3 syntax without GLSL3",
|
|
123652
|
+
category: "Correctness",
|
|
123653
|
+
severity: "error",
|
|
123654
|
+
recommendation: "Set glslVersion to THREE.GLSL3 when RawShaderMaterial uses GLSL 3 syntax",
|
|
123655
|
+
create: (context) => ({ NewExpression(node) {
|
|
123656
|
+
const material = resolveStaticThreeShaderMaterial(node, context);
|
|
123657
|
+
if (material?.constructorName !== "RawShaderMaterial") return;
|
|
123658
|
+
if (getGlsl3Configuration(material.properties.get("glslVersion"), context) !== false) return;
|
|
123659
|
+
const incompatibleShader = [material.vertexShader, material.fragmentShader].find((shader) => Boolean(shader && shaderUsesGlsl3Syntax(shader)));
|
|
123660
|
+
if (!incompatibleShader) return;
|
|
123661
|
+
context.report({
|
|
123662
|
+
node: incompatibleShader.expression,
|
|
123663
|
+
message: "This RawShaderMaterial uses GLSL 3-only syntax but does not set glslVersion to THREE.GLSL3, so Three.js compiles it as GLSL 1"
|
|
123664
|
+
});
|
|
123665
|
+
} })
|
|
123666
|
+
});
|
|
123667
|
+
//#endregion
|
|
122897
123668
|
//#region src/plugin/rules/r3f/three-require-animation-mixer-cleanup.ts
|
|
122898
123669
|
const MIXER_BORROWING_METHOD_NAMES = /* @__PURE__ */ new Set();
|
|
122899
123670
|
const FINE_GRAINED_UNCACHE_METHOD_NAMES = ["uncacheAction", "uncacheClip"];
|
|
@@ -123651,6 +124422,917 @@ const threeRequireRendererCleanup = defineRule({
|
|
|
123651
124422
|
} })
|
|
123652
124423
|
});
|
|
123653
124424
|
//#endregion
|
|
124425
|
+
//#region src/plugin/rules/r3f/three-shader-no-constant-out-of-bounds-index.ts
|
|
124426
|
+
const getTypeIndexElementCounts = (typeName) => {
|
|
124427
|
+
const vectorMatch = /^[biud]?vec([234])$/.exec(typeName);
|
|
124428
|
+
if (vectorMatch) return [Number(vectorMatch[1])];
|
|
124429
|
+
const floatingPointMatrixMatch = /^mat([234])(?:x([234]))?$/.exec(typeName);
|
|
124430
|
+
return floatingPointMatrixMatch ? [Number(floatingPointMatrixMatch[1]), Number(floatingPointMatrixMatch[2] ?? floatingPointMatrixMatch[1])] : [];
|
|
124431
|
+
};
|
|
124432
|
+
const getDeclarationIndexElementCounts = (declaration) => {
|
|
124433
|
+
const typeElementCounts = getTypeIndexElementCounts(declaration.typeName);
|
|
124434
|
+
if (declaration.arraySize === null) return typeElementCounts;
|
|
124435
|
+
return [typeof declaration.arraySize === "number" ? declaration.arraySize : null, ...typeElementCounts];
|
|
124436
|
+
};
|
|
124437
|
+
const getIndexQuantifiers = (postfix) => {
|
|
124438
|
+
const quantifiers = [];
|
|
124439
|
+
let currentPostfix = postfix;
|
|
124440
|
+
while (currentPostfix.type === "postfix") {
|
|
124441
|
+
if (currentPostfix.expression.type !== "quantifier") return quantifiers.length > 0 ? quantifiers : null;
|
|
124442
|
+
quantifiers.push(currentPostfix.expression);
|
|
124443
|
+
currentPostfix = currentPostfix.postfix;
|
|
124444
|
+
}
|
|
124445
|
+
if (currentPostfix.type === "quantifier") quantifiers.push(currentPostfix);
|
|
124446
|
+
return quantifiers.length > 0 ? quantifiers : null;
|
|
124447
|
+
};
|
|
124448
|
+
const checkShader$10 = (shader, context) => {
|
|
124449
|
+
const declarationsByName = new Map(collectGlslGlobalDeclarations(shader.program).map((declaration) => [declaration.name, declaration]));
|
|
124450
|
+
const globalBindings = shader.program.scopes[0]?.bindings;
|
|
124451
|
+
visit(shader.program, { postfix: { enter: ({ node }) => {
|
|
124452
|
+
if (node.expression.type !== "identifier") return;
|
|
124453
|
+
const quantifiers = getIndexQuantifiers(node.postfix);
|
|
124454
|
+
if (!quantifiers) return;
|
|
124455
|
+
const declaration = declarationsByName.get(node.expression.identifier);
|
|
124456
|
+
const binding = globalBindings?.[node.expression.identifier];
|
|
124457
|
+
if (!declaration || !binding?.references.includes(node.expression)) return;
|
|
124458
|
+
const elementCounts = getDeclarationIndexElementCounts(declaration);
|
|
124459
|
+
for (const [quantifierIndex, quantifier] of quantifiers.entries()) {
|
|
124460
|
+
const elementCount = elementCounts[quantifierIndex];
|
|
124461
|
+
const index = getGlslNumericConstant(quantifier.expression);
|
|
124462
|
+
if (elementCount === null || elementCount === void 0 || index === null || !Number.isInteger(index) || index >= 0 && index < elementCount) continue;
|
|
124463
|
+
context.report({
|
|
124464
|
+
node: shader.source.getOriginNodeAtOffset(quantifier.location?.start.offset ?? 0),
|
|
124465
|
+
message: `The constant index ${index} is outside ${declaration.name}'s valid range 0–${elementCount - 1}, which is a GLSL compile error or undefined access`
|
|
124466
|
+
});
|
|
124467
|
+
}
|
|
124468
|
+
} } });
|
|
124469
|
+
};
|
|
124470
|
+
const threeShaderNoConstantOutOfBoundsIndex = defineRule({
|
|
124471
|
+
id: "three-shader-no-constant-out-of-bounds-index",
|
|
124472
|
+
title: "Shader uses a constant out-of-bounds index",
|
|
124473
|
+
category: "Correctness",
|
|
124474
|
+
severity: "error",
|
|
124475
|
+
recommendation: "Keep constant array, vector, and matrix indices within their declared bounds",
|
|
124476
|
+
create: (context) => ({ NewExpression(node) {
|
|
124477
|
+
const material = resolveStaticThreeShaderMaterial(node, context);
|
|
124478
|
+
if (!material) return;
|
|
124479
|
+
if (material.fragmentShader) checkShader$10(material.fragmentShader, context);
|
|
124480
|
+
if (material.vertexShader) checkShader$10(material.vertexShader, context);
|
|
124481
|
+
} })
|
|
124482
|
+
});
|
|
124483
|
+
//#endregion
|
|
124484
|
+
//#region src/plugin/rules/r3f/utils/does-glsl-expression-depend-on-fragment-input.ts
|
|
124485
|
+
const FRAGMENT_INPUT_BUILTIN_NAMES = new Set([
|
|
124486
|
+
"gl_FragCoord",
|
|
124487
|
+
"gl_FrontFacing",
|
|
124488
|
+
"gl_HelperInvocation",
|
|
124489
|
+
"gl_Layer",
|
|
124490
|
+
"gl_PointCoord",
|
|
124491
|
+
"gl_PrimitiveID",
|
|
124492
|
+
"gl_SampleID",
|
|
124493
|
+
"gl_SampleMaskIn",
|
|
124494
|
+
"gl_SamplePosition",
|
|
124495
|
+
"gl_ViewportIndex"
|
|
124496
|
+
]);
|
|
124497
|
+
const doesGlslExpressionDependOnFragmentInput = (expression, fragmentInputReferences) => {
|
|
124498
|
+
let dependsOnFragmentInput = false;
|
|
124499
|
+
visit(expression, { identifier: { enter: (path) => {
|
|
124500
|
+
if (dependsOnFragmentInput || path.parent?.type === "field_selection" || path.parent?.type === "function_call" && path.key === "identifier" || path.parent?.type === "type_specifier") return;
|
|
124501
|
+
dependsOnFragmentInput = fragmentInputReferences.has(path.node) || FRAGMENT_INPUT_BUILTIN_NAMES.has(path.node.identifier);
|
|
124502
|
+
} } });
|
|
124503
|
+
return dependsOnFragmentInput;
|
|
124504
|
+
};
|
|
124505
|
+
//#endregion
|
|
124506
|
+
//#region src/plugin/rules/r3f/three-shader-no-derivatives-in-nonuniform-flow.ts
|
|
124507
|
+
const DERIVATIVE_FUNCTION_NAMES = new Set([
|
|
124508
|
+
"dFdx",
|
|
124509
|
+
"dFdy",
|
|
124510
|
+
"fwidth"
|
|
124511
|
+
]);
|
|
124512
|
+
const IMPLICIT_DERIVATIVE_TEXTURE_FUNCTION_NAMES = new Set([
|
|
124513
|
+
"texture",
|
|
124514
|
+
"texture2D",
|
|
124515
|
+
"texture2DProj",
|
|
124516
|
+
"texture3D",
|
|
124517
|
+
"texture3DProj",
|
|
124518
|
+
"textureCube",
|
|
124519
|
+
"textureOffset",
|
|
124520
|
+
"textureProj",
|
|
124521
|
+
"textureProjOffset"
|
|
124522
|
+
]);
|
|
124523
|
+
const getControllingExpressions = (path) => {
|
|
124524
|
+
const controllingExpressions = [];
|
|
124525
|
+
let currentPath = path;
|
|
124526
|
+
while (currentPath?.parentPath) {
|
|
124527
|
+
const parentPath = currentPath.parentPath;
|
|
124528
|
+
const parent = parentPath.node;
|
|
124529
|
+
if (parent.type === "function") break;
|
|
124530
|
+
const alternateNodes = parent.type === "if_statement" ? Reflect.get(parent, "else") : null;
|
|
124531
|
+
const isInsideAlternate = currentPath.key === "else" || Array.isArray(alternateNodes) && alternateNodes.includes(currentPath.node);
|
|
124532
|
+
if (parent.type === "if_statement" && (currentPath.key === "body" || isInsideAlternate)) controllingExpressions.push(parent.condition);
|
|
124533
|
+
if (parent.type === "while_statement" && currentPath.key === "body") controllingExpressions.push(parent.condition);
|
|
124534
|
+
if (parent.type === "do_statement" && currentPath.key === "body") controllingExpressions.push(parent.expression);
|
|
124535
|
+
if (parent.type === "for_statement" && currentPath.key === "body" && parent.condition) controllingExpressions.push(...[
|
|
124536
|
+
parent.init,
|
|
124537
|
+
parent.condition,
|
|
124538
|
+
parent.operation
|
|
124539
|
+
].filter(Boolean));
|
|
124540
|
+
if (parent.type === "ternary" && (currentPath.key === "left" || currentPath.key === "right")) controllingExpressions.push(parent.expression);
|
|
124541
|
+
if (parent.type === "binary" && currentPath.key === "right" && (parent.operator.literal === "&&" || parent.operator.literal === "||")) controllingExpressions.push(parent.left);
|
|
124542
|
+
if (parent.type === "switch_statement" && currentPath.key === "cases") controllingExpressions.push(parent.expression);
|
|
124543
|
+
currentPath = parentPath;
|
|
124544
|
+
}
|
|
124545
|
+
return controllingExpressions;
|
|
124546
|
+
};
|
|
124547
|
+
const checkShader$9 = (shader, context) => {
|
|
124548
|
+
const globalBindings = shader.program.scopes[0]?.bindings;
|
|
124549
|
+
const fragmentInputReferences = /* @__PURE__ */ new Set();
|
|
124550
|
+
for (const declaration of collectGlslGlobalDeclarations(shader.program)) {
|
|
124551
|
+
if (!declaration.qualifiers.has("in") && !declaration.qualifiers.has("varying")) continue;
|
|
124552
|
+
const binding = globalBindings?.[declaration.name];
|
|
124553
|
+
if (!binding) continue;
|
|
124554
|
+
for (const reference of binding.references) if (reference !== binding.declaration) fragmentInputReferences.add(reference);
|
|
124555
|
+
}
|
|
124556
|
+
visit(shader.program, { function_call: { enter: (path) => {
|
|
124557
|
+
const functionName = getGlslFunctionCallName(path.node);
|
|
124558
|
+
if (!functionName || !DERIVATIVE_FUNCTION_NAMES.has(functionName) && !IMPLICIT_DERIVATIVE_TEXTURE_FUNCTION_NAMES.has(functionName) || hasGlslFunctionLikeMacro(shader.source.text, functionName) || hasGlslFunctionDeclaration(shader.program, functionName)) return;
|
|
124559
|
+
if (!getControllingExpressions(path).some((controllingExpression) => doesGlslExpressionDependOnFragmentInput(controllingExpression, fragmentInputReferences))) return;
|
|
124560
|
+
context.report({
|
|
124561
|
+
node: shader.source.getOriginNodeAtOffset(path.node.location?.start.offset ?? 0),
|
|
124562
|
+
message: `${functionName} executes only on fragment-input-dependent lanes, so implicit derivatives are undefined. Move it before the divergent branch or use explicit gradients`
|
|
124563
|
+
});
|
|
124564
|
+
} } });
|
|
124565
|
+
};
|
|
124566
|
+
const threeShaderNoDerivativesInNonuniformFlow = defineRule({
|
|
124567
|
+
id: "three-shader-no-derivatives-in-nonuniform-flow",
|
|
124568
|
+
title: "Shader derivative runs in non-uniform control flow",
|
|
124569
|
+
category: "Correctness",
|
|
124570
|
+
severity: "error",
|
|
124571
|
+
recommendation: "Evaluate derivatives and implicit-LOD texture samples before fragment-input-dependent branches",
|
|
124572
|
+
create: (context) => ({ NewExpression(node) {
|
|
124573
|
+
const material = resolveStaticThreeShaderMaterial(node, context);
|
|
124574
|
+
if (material?.fragmentShader) checkShader$9(material.fragmentShader, context);
|
|
124575
|
+
} })
|
|
124576
|
+
});
|
|
124577
|
+
//#endregion
|
|
124578
|
+
//#region src/plugin/rules/r3f/utils/get-glsl-function-call-arguments.ts
|
|
124579
|
+
const getGlslFunctionCallArguments = (node) => node.args.filter((argument) => argument.type !== "literal" || argument.literal !== ",");
|
|
124580
|
+
//#endregion
|
|
124581
|
+
//#region src/plugin/rules/r3f/three-shader-no-invalid-clamp-bounds.ts
|
|
124582
|
+
const checkShader$8 = (shader, context) => {
|
|
124583
|
+
if (hasGlslFunctionLikeMacro(shader.source.text, "clamp") || hasGlslFunctionDeclaration(shader.program, "clamp")) return;
|
|
124584
|
+
visit(shader.program, { function_call: { enter: ({ node }) => {
|
|
124585
|
+
if (getGlslFunctionCallName(node) !== "clamp") return;
|
|
124586
|
+
const argumentsWithoutSeparators = getGlslFunctionCallArguments(node);
|
|
124587
|
+
if (argumentsWithoutSeparators.length !== 3) return;
|
|
124588
|
+
const minimum = getGlslNumericConstant(argumentsWithoutSeparators[1]);
|
|
124589
|
+
const maximum = getGlslNumericConstant(argumentsWithoutSeparators[2]);
|
|
124590
|
+
if (minimum === null || maximum === null || minimum <= maximum) return;
|
|
124591
|
+
context.report({
|
|
124592
|
+
node: shader.source.getOriginNodeAtOffset(node.location?.start.offset ?? 0),
|
|
124593
|
+
message: `GLSL clamp requires minVal not to exceed maxVal, but this call uses ${minimum} and ${maximum}, so its result is undefined`
|
|
124594
|
+
});
|
|
124595
|
+
} } });
|
|
124596
|
+
};
|
|
124597
|
+
const threeShaderNoInvalidClampBounds = defineRule({
|
|
124598
|
+
id: "three-shader-no-invalid-clamp-bounds",
|
|
124599
|
+
title: "Shader uses invalid clamp bounds",
|
|
124600
|
+
category: "Correctness",
|
|
124601
|
+
severity: "error",
|
|
124602
|
+
recommendation: "Keep clamp minVal less than or equal to maxVal",
|
|
124603
|
+
create: (context) => ({ NewExpression(node) {
|
|
124604
|
+
const material = resolveStaticThreeShaderMaterial(node, context);
|
|
124605
|
+
if (!material) return;
|
|
124606
|
+
if (material.fragmentShader) checkShader$8(material.fragmentShader, context);
|
|
124607
|
+
if (material.vertexShader) checkShader$8(material.vertexShader, context);
|
|
124608
|
+
} })
|
|
124609
|
+
});
|
|
124610
|
+
//#endregion
|
|
124611
|
+
//#region src/plugin/rules/r3f/three-shader-no-invalid-constant-bit-operations.ts
|
|
124612
|
+
const getInvalidBitfieldMessage = (node, shader) => {
|
|
124613
|
+
const functionName = getGlslFunctionCallName(node);
|
|
124614
|
+
if (functionName !== "bitfieldExtract" && functionName !== "bitfieldInsert" || hasGlslFunctionLikeMacro(shader.source.text, functionName) || hasGlslFunctionDeclaration(shader.program, functionName)) return null;
|
|
124615
|
+
const callArguments = getGlslFunctionCallArguments(node);
|
|
124616
|
+
const offsetArgumentIndex = functionName === "bitfieldExtract" ? 1 : 2;
|
|
124617
|
+
const bitsArgumentIndex = offsetArgumentIndex + 1;
|
|
124618
|
+
const offset = callArguments[offsetArgumentIndex] ? getGlslNumericConstant(callArguments[offsetArgumentIndex]) : null;
|
|
124619
|
+
const bits = callArguments[bitsArgumentIndex] ? getGlslNumericConstant(callArguments[bitsArgumentIndex]) : null;
|
|
124620
|
+
if (offset === null || bits === null || offset >= 0 && bits >= 0 && offset + bits <= 32) return null;
|
|
124621
|
+
return `GLSL ${functionName} uses offset ${offset} and width ${bits}, outside a 32-bit integer`;
|
|
124622
|
+
};
|
|
124623
|
+
const reportInvalidShift = (right, locationOffset, shader, context) => {
|
|
124624
|
+
const shiftCount = getGlslNumericConstant(right);
|
|
124625
|
+
if (shiftCount === null || shiftCount >= 0 && shiftCount < 32) return;
|
|
124626
|
+
context.report({
|
|
124627
|
+
node: shader.source.getOriginNodeAtOffset(locationOffset ?? 0),
|
|
124628
|
+
message: `GLSL shift count ${shiftCount} is outside the valid range 0–31`
|
|
124629
|
+
});
|
|
124630
|
+
};
|
|
124631
|
+
const checkShader$7 = (shader, context) => {
|
|
124632
|
+
visit(shader.program, {
|
|
124633
|
+
binary: { enter: ({ node }) => {
|
|
124634
|
+
if (node.operator.literal !== "<<" && node.operator.literal !== ">>") return;
|
|
124635
|
+
reportInvalidShift(node.right, node.location?.start.offset, shader, context);
|
|
124636
|
+
} },
|
|
124637
|
+
assignment: { enter: ({ node }) => {
|
|
124638
|
+
const operatorLiteral = String(node.operator.literal);
|
|
124639
|
+
if (operatorLiteral !== "<<=" && operatorLiteral !== ">>=") return;
|
|
124640
|
+
reportInvalidShift(node.right, node.location?.start.offset, shader, context);
|
|
124641
|
+
} },
|
|
124642
|
+
function_call: { enter: ({ node }) => {
|
|
124643
|
+
const message = getInvalidBitfieldMessage(node, shader);
|
|
124644
|
+
if (!message) return;
|
|
124645
|
+
context.report({
|
|
124646
|
+
node: shader.source.getOriginNodeAtOffset(node.location?.start.offset ?? 0),
|
|
124647
|
+
message
|
|
124648
|
+
});
|
|
124649
|
+
} }
|
|
124650
|
+
});
|
|
124651
|
+
};
|
|
124652
|
+
const threeShaderNoInvalidConstantBitOperations = defineRule({
|
|
124653
|
+
id: "three-shader-no-invalid-constant-bit-operations",
|
|
124654
|
+
title: "Shader uses invalid constant bit operations",
|
|
124655
|
+
category: "Correctness",
|
|
124656
|
+
severity: "error",
|
|
124657
|
+
recommendation: "Keep shift counts and bitfield ranges within the integer bit width",
|
|
124658
|
+
create: (context) => ({ NewExpression(node) {
|
|
124659
|
+
const material = resolveStaticThreeShaderMaterial(node, context);
|
|
124660
|
+
if (!material) return;
|
|
124661
|
+
if (material.fragmentShader) checkShader$7(material.fragmentShader, context);
|
|
124662
|
+
if (material.vertexShader) checkShader$7(material.vertexShader, context);
|
|
124663
|
+
} })
|
|
124664
|
+
});
|
|
124665
|
+
//#endregion
|
|
124666
|
+
//#region src/plugin/rules/r3f/three-shader-no-invalid-constant-math.ts
|
|
124667
|
+
const INVALID_NONPOSITIVE_FUNCTIONS = new Set([
|
|
124668
|
+
"inversesqrt",
|
|
124669
|
+
"log",
|
|
124670
|
+
"log2"
|
|
124671
|
+
]);
|
|
124672
|
+
const getInvalidFunctionCallMessage = (node, shader) => {
|
|
124673
|
+
const functionName = getGlslFunctionCallName(node);
|
|
124674
|
+
if (!functionName || hasGlslFunctionLikeMacro(shader.source.text, functionName) || hasGlslFunctionDeclaration(shader.program, functionName)) return null;
|
|
124675
|
+
const callArguments = getGlslFunctionCallArguments(node);
|
|
124676
|
+
const firstArgument = callArguments[0];
|
|
124677
|
+
if (!firstArgument) return null;
|
|
124678
|
+
const firstValue = getGlslNumericConstant(firstArgument);
|
|
124679
|
+
if (functionName === "asin" || functionName === "acos") return firstValue !== null && Math.abs(firstValue) > 1 ? `GLSL ${functionName} is undefined outside the range -1 to 1, but received ${firstValue}` : null;
|
|
124680
|
+
if (functionName === "atan" && callArguments.length === 2) {
|
|
124681
|
+
const secondValue = getGlslNumericConstant(callArguments[1]);
|
|
124682
|
+
return firstValue === 0 && secondValue === 0 ? "GLSL atan is undefined when both arguments are zero" : null;
|
|
124683
|
+
}
|
|
124684
|
+
if (functionName === "acosh") return firstValue !== null && firstValue < 1 ? `GLSL acosh is undefined below 1, but received ${firstValue}` : null;
|
|
124685
|
+
if (functionName === "atanh") return firstValue !== null && Math.abs(firstValue) >= 1 ? `GLSL atanh is undefined outside the range -1 to 1, but received ${firstValue}` : null;
|
|
124686
|
+
if (functionName === "ldexp" && callArguments.length === 2) {
|
|
124687
|
+
const exponent = getGlslNumericConstant(callArguments[1]);
|
|
124688
|
+
return exponent !== null && exponent > 128 ? `GLSL ldexp is undefined above exponent 128, but received ${exponent}` : null;
|
|
124689
|
+
}
|
|
124690
|
+
if (functionName === "pow") {
|
|
124691
|
+
if (firstValue !== null && firstValue < 0) return `GLSL pow is undefined for the negative base ${firstValue}`;
|
|
124692
|
+
const exponent = callArguments[1] ? getGlslNumericConstant(callArguments[1]) : null;
|
|
124693
|
+
return firstValue === 0 && exponent !== null && exponent <= 0 ? `GLSL pow is undefined for a zero base and the nonpositive exponent ${exponent}` : null;
|
|
124694
|
+
}
|
|
124695
|
+
if (functionName === "sqrt") return firstValue !== null && firstValue < 0 ? `GLSL sqrt is undefined for the negative argument ${firstValue}` : null;
|
|
124696
|
+
if (INVALID_NONPOSITIVE_FUNCTIONS.has(functionName)) return firstValue !== null && firstValue <= 0 ? `GLSL ${functionName} is undefined for the nonpositive argument ${firstValue}` : null;
|
|
124697
|
+
if (functionName !== "mod" || callArguments.length !== 2) return null;
|
|
124698
|
+
return getGlslNumericConstant(callArguments[1]) === 0 ? "GLSL mod is undefined with a zero divisor" : null;
|
|
124699
|
+
};
|
|
124700
|
+
const reportConstantZeroDivisor = (operator, right, locationOffset, shader, context) => {
|
|
124701
|
+
if (getGlslNumericConstant(right) !== 0) return;
|
|
124702
|
+
context.report({
|
|
124703
|
+
node: shader.source.getOriginNodeAtOffset(locationOffset ?? 0),
|
|
124704
|
+
message: `GLSL ${operator.startsWith("/") ? "division" : "remainder"} by a constant zero has undefined results`
|
|
124705
|
+
});
|
|
124706
|
+
};
|
|
124707
|
+
const checkShader$6 = (shader, context) => {
|
|
124708
|
+
visit(shader.program, {
|
|
124709
|
+
binary: { enter: ({ node }) => {
|
|
124710
|
+
if (node.operator.literal !== "/" && node.operator.literal !== "%") return;
|
|
124711
|
+
reportConstantZeroDivisor(node.operator.literal, node.right, node.location?.start.offset, shader, context);
|
|
124712
|
+
} },
|
|
124713
|
+
assignment: { enter: ({ node }) => {
|
|
124714
|
+
if (node.operator.literal !== "/=" && node.operator.literal !== "%=") return;
|
|
124715
|
+
reportConstantZeroDivisor(node.operator.literal, node.right, node.location?.start.offset, shader, context);
|
|
124716
|
+
} },
|
|
124717
|
+
function_call: { enter: ({ node }) => {
|
|
124718
|
+
const message = getInvalidFunctionCallMessage(node, shader);
|
|
124719
|
+
if (!message) return;
|
|
124720
|
+
context.report({
|
|
124721
|
+
node: shader.source.getOriginNodeAtOffset(node.location?.start.offset ?? 0),
|
|
124722
|
+
message
|
|
124723
|
+
});
|
|
124724
|
+
} }
|
|
124725
|
+
});
|
|
124726
|
+
};
|
|
124727
|
+
const threeShaderNoInvalidConstantMath = defineRule({
|
|
124728
|
+
id: "three-shader-no-invalid-constant-math",
|
|
124729
|
+
title: "Shader uses invalid constant math",
|
|
124730
|
+
category: "Correctness",
|
|
124731
|
+
severity: "error",
|
|
124732
|
+
recommendation: "Keep constant arguments within the defined GLSL builtin and operator domains",
|
|
124733
|
+
create: (context) => ({ NewExpression(node) {
|
|
124734
|
+
const material = resolveStaticThreeShaderMaterial(node, context);
|
|
124735
|
+
if (!material) return;
|
|
124736
|
+
if (material.fragmentShader) checkShader$6(material.fragmentShader, context);
|
|
124737
|
+
if (material.vertexShader) checkShader$6(material.vertexShader, context);
|
|
124738
|
+
} })
|
|
124739
|
+
});
|
|
124740
|
+
//#endregion
|
|
124741
|
+
//#region src/plugin/rules/r3f/three-shader-no-invalid-smoothstep-edges.ts
|
|
124742
|
+
const checkShaderSource = (shader, context) => {
|
|
124743
|
+
if (hasGlslFunctionLikeMacro(shader.source.text, "smoothstep") || hasGlslFunctionDeclaration(shader.program, "smoothstep")) return;
|
|
124744
|
+
visit(shader.program, { function_call: { enter: ({ node }) => {
|
|
124745
|
+
if (getGlslFunctionCallName(node) !== "smoothstep") return;
|
|
124746
|
+
const argumentsWithoutSeparators = getGlslFunctionCallArguments(node);
|
|
124747
|
+
if (argumentsWithoutSeparators.length !== 3) return;
|
|
124748
|
+
const firstEdge = getGlslNumericConstant(argumentsWithoutSeparators[0]);
|
|
124749
|
+
const secondEdge = getGlslNumericConstant(argumentsWithoutSeparators[1]);
|
|
124750
|
+
if (firstEdge === null || secondEdge === null || firstEdge < secondEdge) return;
|
|
124751
|
+
const shaderOffset = node.location?.start.offset ?? 0;
|
|
124752
|
+
context.report({
|
|
124753
|
+
node: shader.source.getOriginNodeAtOffset(shaderOffset),
|
|
124754
|
+
message: `GLSL smoothstep requires edge0 to be less than edge1, but this call uses ${firstEdge} and ${secondEdge}, so its result is undefined`
|
|
124755
|
+
});
|
|
124756
|
+
} } });
|
|
124757
|
+
};
|
|
124758
|
+
const threeShaderNoInvalidSmoothstepEdges = defineRule({
|
|
124759
|
+
id: "three-shader-no-invalid-smoothstep-edges",
|
|
124760
|
+
title: "Shader uses invalid smoothstep edges",
|
|
124761
|
+
category: "Correctness",
|
|
124762
|
+
severity: "error",
|
|
124763
|
+
recommendation: "Keep smoothstep edge0 below edge1 and invert the result when a descending transition is needed",
|
|
124764
|
+
create: (context) => ({ NewExpression(node) {
|
|
124765
|
+
const material = resolveStaticThreeShaderMaterial(node, context);
|
|
124766
|
+
if (!material) return;
|
|
124767
|
+
if (material.fragmentShader) checkShaderSource(material.fragmentShader, context);
|
|
124768
|
+
if (material.vertexShader) checkShaderSource(material.vertexShader, context);
|
|
124769
|
+
} })
|
|
124770
|
+
});
|
|
124771
|
+
//#endregion
|
|
124772
|
+
//#region src/plugin/rules/r3f/three-shader-no-inverse-of-uniform.ts
|
|
124773
|
+
const checkShader$5 = (shader, context) => {
|
|
124774
|
+
if (hasGlslFunctionLikeMacro(shader.source.text, "inverse") || hasGlslFunctionDeclaration(shader.program, "inverse")) return;
|
|
124775
|
+
const globalBindings = shader.program.scopes[0]?.bindings;
|
|
124776
|
+
const uniformReferences = new Set(collectGlslGlobalDeclarations(shader.program).filter((declaration) => declaration.qualifiers.has("uniform")).flatMap((declaration) => globalBindings?.[declaration.name]?.references ?? []));
|
|
124777
|
+
visit(shader.program, { function_call: { enter: ({ node }) => {
|
|
124778
|
+
if (getGlslFunctionCallName(node) !== "inverse") return;
|
|
124779
|
+
const callArguments = getGlslFunctionCallArguments(node);
|
|
124780
|
+
const matrix = callArguments[0];
|
|
124781
|
+
if (callArguments.length !== 1 || matrix?.type !== "identifier" || !uniformReferences.has(matrix)) return;
|
|
124782
|
+
context.report({
|
|
124783
|
+
node: shader.source.getOriginNodeAtOffset(node.location?.start.offset ?? 0),
|
|
124784
|
+
message: `The matrix ${matrix.identifier} is uniform across this draw, but inverse recomputes it for every shader invocation. Compute and bind the inverse matrix on the CPU`
|
|
124785
|
+
});
|
|
124786
|
+
} } });
|
|
124787
|
+
};
|
|
124788
|
+
const threeShaderNoInverseOfUniform = defineRule({
|
|
124789
|
+
id: "three-shader-no-inverse-of-uniform",
|
|
124790
|
+
title: "Shader inverts a uniform matrix per invocation",
|
|
124791
|
+
category: "Performance",
|
|
124792
|
+
severity: "warn",
|
|
124793
|
+
recommendation: "Compute uniform matrix inverses once on the CPU and upload the result",
|
|
124794
|
+
create: (context) => ({ NewExpression(node) {
|
|
124795
|
+
const material = resolveStaticThreeShaderMaterial(node, context);
|
|
124796
|
+
if (!material) return;
|
|
124797
|
+
if (material.fragmentShader) checkShader$5(material.fragmentShader, context);
|
|
124798
|
+
if (material.vertexShader) checkShader$5(material.vertexShader, context);
|
|
124799
|
+
} })
|
|
124800
|
+
});
|
|
124801
|
+
//#endregion
|
|
124802
|
+
//#region src/plugin/rules/r3f/three-shader-no-redeclared-builtins.ts
|
|
124803
|
+
const checkShader$4 = (shader, injectedNames, context) => {
|
|
124804
|
+
for (const declaration of collectGlslGlobalDeclarations(shader.program)) {
|
|
124805
|
+
if (!injectedNames.has(declaration.name)) continue;
|
|
124806
|
+
context.report({
|
|
124807
|
+
node: shader.source.getOriginNodeAtOffset(declaration.node.location?.start.offset ?? 0),
|
|
124808
|
+
message: `ShaderMaterial already injects ${declaration.name} into the ${shader.stage} shader, so this declaration collides with Three.js's generated prefix`
|
|
124809
|
+
});
|
|
124810
|
+
}
|
|
124811
|
+
};
|
|
124812
|
+
const threeShaderNoRedeclaredBuiltins = defineRule({
|
|
124813
|
+
id: "three-shader-no-redeclared-builtins",
|
|
124814
|
+
title: "ShaderMaterial redeclares a Three.js builtin",
|
|
124815
|
+
category: "Correctness",
|
|
124816
|
+
severity: "error",
|
|
124817
|
+
recommendation: "Use ShaderMaterial built-in attributes and uniforms without redeclaring them",
|
|
124818
|
+
create: (context) => ({ NewExpression(node) {
|
|
124819
|
+
const material = resolveStaticThreeShaderMaterial(node, context);
|
|
124820
|
+
if (material?.constructorName !== "ShaderMaterial") return;
|
|
124821
|
+
if (material.vertexShader) checkShader$4(material.vertexShader, THREE_SHADER_MATERIAL_INJECTED_VERTEX_NAMES, context);
|
|
124822
|
+
if (material.fragmentShader) checkShader$4(material.fragmentShader, THREE_SHADER_MATERIAL_INJECTED_FRAGMENT_NAMES, context);
|
|
124823
|
+
} })
|
|
124824
|
+
});
|
|
124825
|
+
//#endregion
|
|
124826
|
+
//#region src/plugin/rules/r3f/three-shader-no-redundant-frag-depth.ts
|
|
124827
|
+
const FRAGMENT_DEPTH_OUTPUT_NAMES = new Set(["gl_FragDepth", "gl_FragDepthEXT"]);
|
|
124828
|
+
const CONDITIONAL_NODE_TYPES = new Set([
|
|
124829
|
+
"do_statement",
|
|
124830
|
+
"for_statement",
|
|
124831
|
+
"if_statement",
|
|
124832
|
+
"switch_statement",
|
|
124833
|
+
"ternary",
|
|
124834
|
+
"while_statement"
|
|
124835
|
+
]);
|
|
124836
|
+
const isDefaultFragmentDepthAssignment = (node) => {
|
|
124837
|
+
if (node.operator.literal !== "=" || node.left.type !== "identifier" || !FRAGMENT_DEPTH_OUTPUT_NAMES.has(node.left.identifier) || node.right.type !== "postfix" || node.right.expression.type !== "identifier" || node.right.expression.identifier !== "gl_FragCoord" || node.right.postfix.type !== "field_selection") return false;
|
|
124838
|
+
return Reflect.get(node.right.postfix.selection, "identifier") === "z";
|
|
124839
|
+
};
|
|
124840
|
+
const isUnconditionalMainAssignment = (path) => {
|
|
124841
|
+
let currentPath = path.parentPath;
|
|
124842
|
+
while (currentPath) {
|
|
124843
|
+
if (CONDITIONAL_NODE_TYPES.has(currentPath.node.type)) return false;
|
|
124844
|
+
if (currentPath.node.type === "binary") {
|
|
124845
|
+
const operator = currentPath.node.operator.literal;
|
|
124846
|
+
if (operator === "&&" || operator === "||") return false;
|
|
124847
|
+
}
|
|
124848
|
+
if (currentPath.node.type === "function") return currentPath.node.prototype.header.name.identifier === "main";
|
|
124849
|
+
currentPath = currentPath.parentPath;
|
|
124850
|
+
}
|
|
124851
|
+
return false;
|
|
124852
|
+
};
|
|
124853
|
+
const checkShader$3 = (shader, context) => {
|
|
124854
|
+
const depthAssignments = [];
|
|
124855
|
+
visit(shader.program, { assignment: { enter: (path) => {
|
|
124856
|
+
if (path.node.left.type === "identifier" && FRAGMENT_DEPTH_OUTPUT_NAMES.has(path.node.left.identifier)) depthAssignments.push(path);
|
|
124857
|
+
} } });
|
|
124858
|
+
if (depthAssignments.length !== 1 || !isDefaultFragmentDepthAssignment(depthAssignments[0].node) || !isUnconditionalMainAssignment(depthAssignments[0])) return;
|
|
124859
|
+
const assignment = depthAssignments[0].node;
|
|
124860
|
+
context.report({
|
|
124861
|
+
node: shader.source.getOriginNodeAtOffset(assignment.location?.start.offset ?? 0),
|
|
124862
|
+
message: "This shader unconditionally writes the fixed-function depth value back to gl_FragDepth. Remove the redundant write so early depth testing remains available"
|
|
124863
|
+
});
|
|
124864
|
+
};
|
|
124865
|
+
const threeShaderNoRedundantFragDepth = defineRule({
|
|
124866
|
+
id: "three-shader-no-redundant-frag-depth",
|
|
124867
|
+
title: "Shader redundantly writes fragment depth",
|
|
124868
|
+
category: "Performance",
|
|
124869
|
+
severity: "warn",
|
|
124870
|
+
recommendation: "Let fixed-function depth supply gl_FragCoord.z when depth is unchanged",
|
|
124871
|
+
create: (context) => ({ NewExpression(node) {
|
|
124872
|
+
const material = resolveStaticThreeShaderMaterial(node, context);
|
|
124873
|
+
if (material?.fragmentShader) checkShader$3(material.fragmentShader, context);
|
|
124874
|
+
} })
|
|
124875
|
+
});
|
|
124876
|
+
//#endregion
|
|
124877
|
+
//#region src/plugin/rules/r3f/three-shader-no-version-directive.ts
|
|
124878
|
+
const VERSION_DIRECTIVE_PATTERN = /^[ \t]*#[ \t]*version\b/m;
|
|
124879
|
+
const checkShader$2 = (shader, context) => {
|
|
124880
|
+
const versionDirectiveOffset = maskGlslComments(shader.source.text).search(VERSION_DIRECTIVE_PATTERN);
|
|
124881
|
+
if (versionDirectiveOffset < 0) return;
|
|
124882
|
+
context.report({
|
|
124883
|
+
node: shader.source.getOriginNodeAtOffset(versionDirectiveOffset),
|
|
124884
|
+
message: "Three.js prepends its own shader prefix before custom source, so an inline #version directive is not first and cannot compile. Set the material glslVersion property instead"
|
|
124885
|
+
});
|
|
124886
|
+
};
|
|
124887
|
+
const threeShaderNoVersionDirective = defineRule({
|
|
124888
|
+
id: "three-shader-no-version-directive",
|
|
124889
|
+
title: "Three shader source contains a version directive",
|
|
124890
|
+
category: "Correctness",
|
|
124891
|
+
severity: "error",
|
|
124892
|
+
recommendation: "Set glslVersion on the material instead of writing #version in shader source",
|
|
124893
|
+
create: (context) => ({ NewExpression(node) {
|
|
124894
|
+
const material = resolveStaticThreeShaderMaterial(node, context);
|
|
124895
|
+
if (!material) return;
|
|
124896
|
+
if (material.fragmentShader) checkShader$2(material.fragmentShader, context);
|
|
124897
|
+
if (material.vertexShader) checkShader$2(material.vertexShader, context);
|
|
124898
|
+
} })
|
|
124899
|
+
});
|
|
124900
|
+
//#endregion
|
|
124901
|
+
//#region src/plugin/rules/r3f/three-shader-prefer-small-integer-pow.ts
|
|
124902
|
+
const SMALL_INTEGER_POW_EXPONENTS = new Set([
|
|
124903
|
+
2,
|
|
124904
|
+
3,
|
|
124905
|
+
4
|
|
124906
|
+
]);
|
|
124907
|
+
const checkShader$1 = (shader, context) => {
|
|
124908
|
+
if (hasGlslFunctionLikeMacro(shader.source.text, "pow") || hasGlslFunctionDeclaration(shader.program, "pow")) return;
|
|
124909
|
+
visit(shader.program, { function_call: { enter: ({ node }) => {
|
|
124910
|
+
if (getGlslFunctionCallName(node) !== "pow") return;
|
|
124911
|
+
const callArguments = getGlslFunctionCallArguments(node);
|
|
124912
|
+
if (callArguments.length !== 2) return;
|
|
124913
|
+
const exponent = getGlslNumericConstant(callArguments[1]);
|
|
124914
|
+
if (exponent === null || !SMALL_INTEGER_POW_EXPONENTS.has(exponent)) return;
|
|
124915
|
+
context.report({
|
|
124916
|
+
node: shader.source.getOriginNodeAtOffset(node.location?.start.offset ?? 0),
|
|
124917
|
+
message: `This pow call has the small integer exponent ${exponent}. Explicit multiplication avoids a general exponentiation operation and makes the intended cost clear`
|
|
124918
|
+
});
|
|
124919
|
+
} } });
|
|
124920
|
+
};
|
|
124921
|
+
const threeShaderPreferSmallIntegerPow = defineRule({
|
|
124922
|
+
id: "three-shader-prefer-small-integer-pow",
|
|
124923
|
+
title: "Shader uses pow for a small integer exponent",
|
|
124924
|
+
category: "Performance",
|
|
124925
|
+
severity: "warn",
|
|
124926
|
+
recommendation: "Use explicit multiplication for powers of two, three, or four",
|
|
124927
|
+
create: (context) => ({ NewExpression(node) {
|
|
124928
|
+
const material = resolveStaticThreeShaderMaterial(node, context);
|
|
124929
|
+
if (!material) return;
|
|
124930
|
+
if (material.fragmentShader) checkShader$1(material.fragmentShader, context);
|
|
124931
|
+
if (material.vertexShader) checkShader$1(material.vertexShader, context);
|
|
124932
|
+
} })
|
|
124933
|
+
});
|
|
124934
|
+
//#endregion
|
|
124935
|
+
//#region src/plugin/rules/r3f/three-shader-prefer-squared-distance-comparison.ts
|
|
124936
|
+
const DISTANCE_FUNCTION_NAMES = new Set(["distance", "length"]);
|
|
124937
|
+
const ORDERING_OPERATORS = new Set([
|
|
124938
|
+
"<",
|
|
124939
|
+
"<=",
|
|
124940
|
+
">",
|
|
124941
|
+
">="
|
|
124942
|
+
]);
|
|
124943
|
+
const getDistanceFunctionCall = (node) => {
|
|
124944
|
+
if (node.type === "group") return getDistanceFunctionCall(node.expression);
|
|
124945
|
+
return node.type === "function_call" && DISTANCE_FUNCTION_NAMES.has(getGlslFunctionCallName(node) ?? "") ? node : null;
|
|
124946
|
+
};
|
|
124947
|
+
const getDistanceComparison = (left, right) => {
|
|
124948
|
+
const leftCall = getDistanceFunctionCall(left);
|
|
124949
|
+
const rightThreshold = getGlslNumericConstant(right);
|
|
124950
|
+
if (leftCall && rightThreshold !== null && rightThreshold >= 0) return {
|
|
124951
|
+
functionCall: leftCall,
|
|
124952
|
+
threshold: rightThreshold
|
|
124953
|
+
};
|
|
124954
|
+
const rightCall = getDistanceFunctionCall(right);
|
|
124955
|
+
const leftThreshold = getGlslNumericConstant(left);
|
|
124956
|
+
return rightCall && leftThreshold !== null && leftThreshold >= 0 ? {
|
|
124957
|
+
functionCall: rightCall,
|
|
124958
|
+
threshold: leftThreshold
|
|
124959
|
+
} : null;
|
|
124960
|
+
};
|
|
124961
|
+
const checkShader = (shader, context) => {
|
|
124962
|
+
visit(shader.program, { binary: { enter: ({ node }) => {
|
|
124963
|
+
if (!ORDERING_OPERATORS.has(node.operator.literal)) return;
|
|
124964
|
+
const comparison = getDistanceComparison(node.left, node.right);
|
|
124965
|
+
if (!comparison) return;
|
|
124966
|
+
const functionName = getGlslFunctionCallName(comparison.functionCall);
|
|
124967
|
+
if (!functionName || hasGlslFunctionLikeMacro(shader.source.text, functionName) || hasGlslFunctionDeclaration(shader.program, functionName)) return;
|
|
124968
|
+
context.report({
|
|
124969
|
+
node: shader.source.getOriginNodeAtOffset(node.location?.start.offset ?? 0),
|
|
124970
|
+
message: `Compare squared ${functionName === "length" ? "length using dot(v, v)" : "distance using dot(a - b, a - b)"} against ${comparison.threshold * comparison.threshold} to avoid computing a square root`
|
|
124971
|
+
});
|
|
124972
|
+
} } });
|
|
124973
|
+
};
|
|
124974
|
+
const threeShaderPreferSquaredDistanceComparison = defineRule({
|
|
124975
|
+
id: "three-shader-prefer-squared-distance-comparison",
|
|
124976
|
+
title: "Shader compares a computed distance",
|
|
124977
|
+
category: "Performance",
|
|
124978
|
+
severity: "warn",
|
|
124979
|
+
recommendation: "Compare squared vector distance against a squared nonnegative threshold",
|
|
124980
|
+
create: (context) => ({ NewExpression(node) {
|
|
124981
|
+
const material = resolveStaticThreeShaderMaterial(node, context);
|
|
124982
|
+
if (!material) return;
|
|
124983
|
+
if (material.fragmentShader) checkShader(material.fragmentShader, context);
|
|
124984
|
+
if (material.vertexShader) checkShader(material.vertexShader, context);
|
|
124985
|
+
} })
|
|
124986
|
+
});
|
|
124987
|
+
//#endregion
|
|
124988
|
+
//#region src/plugin/rules/r3f/utils/has-glsl-array-dimension-mismatch.ts
|
|
124989
|
+
const hasGlslArrayDimensionMismatch = (firstArraySize, secondArraySize) => firstArraySize === null !== (secondArraySize === null) || typeof firstArraySize === "number" && typeof secondArraySize === "number" && firstArraySize !== secondArraySize;
|
|
124990
|
+
//#endregion
|
|
124991
|
+
//#region src/plugin/rules/r3f/three-shader-require-matching-uniforms.ts
|
|
124992
|
+
const PRECISION_QUALIFIER_NAMES = [
|
|
124993
|
+
"highp",
|
|
124994
|
+
"mediump",
|
|
124995
|
+
"lowp"
|
|
124996
|
+
];
|
|
124997
|
+
const getPrecision = (declaration) => PRECISION_QUALIFIER_NAMES.find((precision) => declaration.qualifiers.has(precision)) ?? null;
|
|
124998
|
+
const getMismatch = (vertexUniform, fragmentUniform) => {
|
|
124999
|
+
if (vertexUniform.typeName !== fragmentUniform.typeName) return `type ${vertexUniform.typeName} in the vertex shader and ${fragmentUniform.typeName} in the fragment shader`;
|
|
125000
|
+
if (hasGlslArrayDimensionMismatch(vertexUniform.arraySize, fragmentUniform.arraySize)) return "different array dimensions";
|
|
125001
|
+
const vertexPrecision = getPrecision(vertexUniform);
|
|
125002
|
+
const fragmentPrecision = getPrecision(fragmentUniform);
|
|
125003
|
+
return vertexPrecision === null || fragmentPrecision === null || vertexPrecision === fragmentPrecision ? null : `precision ${vertexPrecision} in the vertex shader and ${fragmentPrecision} in the fragment shader`;
|
|
125004
|
+
};
|
|
125005
|
+
const getUsedUniforms = (declarations) => new Map(declarations.filter((declaration) => declaration.qualifiers.has("uniform") && declaration.isStaticallyUsed).map((declaration) => [declaration.name, declaration]));
|
|
125006
|
+
const threeShaderRequireMatchingUniforms = defineRule({
|
|
125007
|
+
id: "three-shader-require-matching-uniforms",
|
|
125008
|
+
title: "Shader stages declare incompatible uniforms",
|
|
125009
|
+
category: "Correctness",
|
|
125010
|
+
severity: "error",
|
|
125011
|
+
recommendation: "Use the same type, array dimensions, and precision for uniforms shared by both shader stages",
|
|
125012
|
+
create: (context) => ({ NewExpression(node) {
|
|
125013
|
+
const material = resolveStaticThreeShaderMaterial(node, context);
|
|
125014
|
+
if (!material?.vertexShader || !material.fragmentShader) return;
|
|
125015
|
+
const vertexUniforms = getUsedUniforms(collectGlslGlobalDeclarations(material.vertexShader.program));
|
|
125016
|
+
const fragmentUniforms = getUsedUniforms(collectGlslGlobalDeclarations(material.fragmentShader.program));
|
|
125017
|
+
for (const [uniformName, fragmentUniform] of fragmentUniforms) {
|
|
125018
|
+
const vertexUniform = vertexUniforms.get(uniformName);
|
|
125019
|
+
if (!vertexUniform) continue;
|
|
125020
|
+
const mismatch = getMismatch(vertexUniform, fragmentUniform);
|
|
125021
|
+
if (!mismatch) continue;
|
|
125022
|
+
context.report({
|
|
125023
|
+
node: material.fragmentShader.source.getOriginNodeAtOffset(fragmentUniform.node.location?.start.offset ?? 0),
|
|
125024
|
+
message: `Uniform ${uniformName} has ${mismatch}, so the shader stages cannot link consistently`
|
|
125025
|
+
});
|
|
125026
|
+
}
|
|
125027
|
+
} })
|
|
125028
|
+
});
|
|
125029
|
+
//#endregion
|
|
125030
|
+
//#region src/plugin/rules/r3f/three-shader-require-matching-varyings.ts
|
|
125031
|
+
const isVertexOutput = (declaration) => declaration.qualifiers.has("out") || declaration.qualifiers.has("varying");
|
|
125032
|
+
const isFragmentInput = (declaration) => declaration.qualifiers.has("in") || declaration.qualifiers.has("varying");
|
|
125033
|
+
const getInterfaceMismatch = (vertexOutput, fragmentInput) => {
|
|
125034
|
+
if (vertexOutput.typeName !== fragmentInput.typeName) return `type ${vertexOutput.typeName} in the vertex shader but ${fragmentInput.typeName} in the fragment shader`;
|
|
125035
|
+
if (hasGlslArrayDimensionMismatch(vertexOutput.arraySize, fragmentInput.arraySize)) return "different array dimensions";
|
|
125036
|
+
return vertexOutput.interpolation === fragmentInput.interpolation ? null : `interpolation ${vertexOutput.interpolation} in the vertex shader but ${fragmentInput.interpolation} in the fragment shader`;
|
|
125037
|
+
};
|
|
125038
|
+
const threeShaderRequireMatchingVaryings = defineRule({
|
|
125039
|
+
id: "three-shader-require-matching-varyings",
|
|
125040
|
+
title: "Shader stage interface does not match",
|
|
125041
|
+
category: "Correctness",
|
|
125042
|
+
severity: "error",
|
|
125043
|
+
recommendation: "Declare every statically used fragment input as a compatible vertex output with the same name",
|
|
125044
|
+
create: (context) => ({ NewExpression(node) {
|
|
125045
|
+
const material = resolveStaticThreeShaderMaterial(node, context);
|
|
125046
|
+
if (!material?.vertexShader || !material.fragmentShader) return;
|
|
125047
|
+
const vertexOutputs = new Map(collectGlslGlobalDeclarations(material.vertexShader.program).filter(isVertexOutput).map((declaration) => [declaration.name, declaration]));
|
|
125048
|
+
const fragmentInputs = collectGlslGlobalDeclarations(material.fragmentShader.program).filter((declaration) => isFragmentInput(declaration) && declaration.isStaticallyUsed && !declaration.name.startsWith("gl_") && !declaration.hasLayoutQualifier);
|
|
125049
|
+
for (const fragmentInput of fragmentInputs) {
|
|
125050
|
+
const vertexOutput = vertexOutputs.get(fragmentInput.name);
|
|
125051
|
+
const mismatch = vertexOutput ? getInterfaceMismatch(vertexOutput, fragmentInput) : "no matching vertex output";
|
|
125052
|
+
if (!mismatch) continue;
|
|
125053
|
+
context.report({
|
|
125054
|
+
node: material.fragmentShader.source.getOriginNodeAtOffset(fragmentInput.node.location?.start.offset ?? 0),
|
|
125055
|
+
message: `Fragment input ${fragmentInput.name} has ${mismatch}, so the shader program cannot link with a defined value`
|
|
125056
|
+
});
|
|
125057
|
+
}
|
|
125058
|
+
} })
|
|
125059
|
+
});
|
|
125060
|
+
//#endregion
|
|
125061
|
+
//#region src/plugin/rules/r3f/utils/does-glsl-main-write-position-on-all-paths.ts
|
|
125062
|
+
const getPositionSwizzleWriteMask = (selection) => {
|
|
125063
|
+
let writeMask = 0;
|
|
125064
|
+
for (const componentAlias of selection) {
|
|
125065
|
+
const componentBit = GLSL_POSITION_COMPONENT_BIT_BY_ALIAS.get(componentAlias);
|
|
125066
|
+
if (!componentBit) return 0;
|
|
125067
|
+
writeMask |= componentBit;
|
|
125068
|
+
}
|
|
125069
|
+
return writeMask;
|
|
125070
|
+
};
|
|
125071
|
+
const getExpressionPositionWriteMask = (expression) => {
|
|
125072
|
+
if (expression.type === "assignment" && expression.operator.literal === "=") {
|
|
125073
|
+
if (expression.left.type === "identifier" && expression.left.identifier === "gl_Position") return 15;
|
|
125074
|
+
if (expression.left.type === "postfix" && expression.left.expression.type === "identifier" && expression.left.expression.identifier === "gl_Position" && expression.left.postfix.type === "field_selection") {
|
|
125075
|
+
const selection = Reflect.get(expression.left.postfix.selection, "identifier");
|
|
125076
|
+
return typeof selection === "string" ? getPositionSwizzleWriteMask(selection) : 0;
|
|
125077
|
+
}
|
|
125078
|
+
}
|
|
125079
|
+
if (expression.type === "binary" && expression.operator.literal === ",") return getExpressionPositionWriteMask(expression.left) | getExpressionPositionWriteMask(expression.right);
|
|
125080
|
+
if (expression.type === "group") return getExpressionPositionWriteMask(expression.expression);
|
|
125081
|
+
if (expression.type === "ternary") return getExpressionPositionWriteMask(expression.left) & getExpressionPositionWriteMask(expression.right);
|
|
125082
|
+
return 0;
|
|
125083
|
+
};
|
|
125084
|
+
const expressionContainsPositionWrite = (expression) => {
|
|
125085
|
+
let containsPositionWrite = false;
|
|
125086
|
+
visit(expression, { assignment: { enter: ({ node }) => {
|
|
125087
|
+
if (getExpressionPositionWriteMask(node) !== 0) containsPositionWrite = true;
|
|
125088
|
+
} } });
|
|
125089
|
+
return containsPositionWrite;
|
|
125090
|
+
};
|
|
125091
|
+
const getBooleanConstant = (expression) => {
|
|
125092
|
+
if (expression.type !== "bool_constant") return null;
|
|
125093
|
+
return expression.token === "true";
|
|
125094
|
+
};
|
|
125095
|
+
const analyzeCompoundStatement = (statement, inputStates) => {
|
|
125096
|
+
let activeStates = inputStates;
|
|
125097
|
+
let hasUnwrittenReturn = false;
|
|
125098
|
+
for (const child of statement.statements) {
|
|
125099
|
+
if (activeStates.size === 0) break;
|
|
125100
|
+
const childResult = analyzeStatement(child, activeStates);
|
|
125101
|
+
if (!childResult.isSupported) return childResult;
|
|
125102
|
+
activeStates = childResult.activeStates;
|
|
125103
|
+
hasUnwrittenReturn ||= childResult.hasUnwrittenReturn;
|
|
125104
|
+
}
|
|
125105
|
+
return {
|
|
125106
|
+
activeStates,
|
|
125107
|
+
hasUnwrittenReturn,
|
|
125108
|
+
isSupported: true
|
|
125109
|
+
};
|
|
125110
|
+
};
|
|
125111
|
+
const analyzeStatement = (statement, inputStates) => {
|
|
125112
|
+
if (statement.type === "compound_statement") return analyzeCompoundStatement(statement, inputStates);
|
|
125113
|
+
if (statement.type === "expression_statement") {
|
|
125114
|
+
const expressionWriteMask = getExpressionPositionWriteMask(statement.expression);
|
|
125115
|
+
if (expressionWriteMask === 0 && expressionContainsPositionWrite(statement.expression)) return {
|
|
125116
|
+
activeStates: inputStates,
|
|
125117
|
+
hasUnwrittenReturn: false,
|
|
125118
|
+
isSupported: false
|
|
125119
|
+
};
|
|
125120
|
+
return {
|
|
125121
|
+
activeStates: expressionWriteMask === 0 ? inputStates : new Set([...inputStates].map((state) => state | expressionWriteMask)),
|
|
125122
|
+
hasUnwrittenReturn: false,
|
|
125123
|
+
isSupported: true
|
|
125124
|
+
};
|
|
125125
|
+
}
|
|
125126
|
+
if (statement.type === "return_statement") return {
|
|
125127
|
+
activeStates: /* @__PURE__ */ new Set(),
|
|
125128
|
+
hasUnwrittenReturn: [...inputStates].some((state) => state !== 15),
|
|
125129
|
+
isSupported: true
|
|
125130
|
+
};
|
|
125131
|
+
if (statement.type === "if_statement") {
|
|
125132
|
+
if (expressionContainsPositionWrite(statement.condition)) return {
|
|
125133
|
+
activeStates: inputStates,
|
|
125134
|
+
hasUnwrittenReturn: false,
|
|
125135
|
+
isSupported: false
|
|
125136
|
+
};
|
|
125137
|
+
const constantCondition = getBooleanConstant(statement.condition);
|
|
125138
|
+
const consequent = analyzeStatement(statement.body, inputStates);
|
|
125139
|
+
if (!consequent.isSupported) return consequent;
|
|
125140
|
+
const alternateNodes = Reflect.get(statement, "else");
|
|
125141
|
+
const alternateStatement = Array.isArray(alternateNodes) ? alternateNodes[alternateNodes.length - 1] : void 0;
|
|
125142
|
+
const alternate = alternateStatement ? analyzeStatement(alternateStatement, inputStates) : {
|
|
125143
|
+
activeStates: inputStates,
|
|
125144
|
+
hasUnwrittenReturn: false,
|
|
125145
|
+
isSupported: true
|
|
125146
|
+
};
|
|
125147
|
+
if (!alternate.isSupported) return alternate;
|
|
125148
|
+
if (constantCondition !== null) return constantCondition ? consequent : alternate;
|
|
125149
|
+
return {
|
|
125150
|
+
activeStates: new Set([...consequent.activeStates, ...alternate.activeStates]),
|
|
125151
|
+
hasUnwrittenReturn: consequent.hasUnwrittenReturn || alternate.hasUnwrittenReturn,
|
|
125152
|
+
isSupported: true
|
|
125153
|
+
};
|
|
125154
|
+
}
|
|
125155
|
+
if (statement.type === "for_statement" || statement.type === "while_statement") {
|
|
125156
|
+
const controlExpressions = statement.type === "for_statement" ? [
|
|
125157
|
+
Reflect.get(statement, "init"),
|
|
125158
|
+
Reflect.get(statement, "condition"),
|
|
125159
|
+
Reflect.get(statement, "operation")
|
|
125160
|
+
] : [statement.condition];
|
|
125161
|
+
if (controlExpressions.some((expression) => !expression) || controlExpressions.some(expressionContainsPositionWrite)) return {
|
|
125162
|
+
activeStates: inputStates,
|
|
125163
|
+
hasUnwrittenReturn: false,
|
|
125164
|
+
isSupported: false
|
|
125165
|
+
};
|
|
125166
|
+
const bodyResult = analyzeStatement(statement.body, inputStates);
|
|
125167
|
+
return {
|
|
125168
|
+
activeStates: inputStates,
|
|
125169
|
+
hasUnwrittenReturn: bodyResult.hasUnwrittenReturn,
|
|
125170
|
+
isSupported: bodyResult.isSupported
|
|
125171
|
+
};
|
|
125172
|
+
}
|
|
125173
|
+
if (statement.type === "declaration_statement" && expressionContainsPositionWrite(statement.declaration)) return {
|
|
125174
|
+
activeStates: inputStates,
|
|
125175
|
+
hasUnwrittenReturn: false,
|
|
125176
|
+
isSupported: false
|
|
125177
|
+
};
|
|
125178
|
+
if (statement.type === "do_statement" || statement.type === "switch_statement" || statement.type === "break_statement" || statement.type === "continue_statement") return {
|
|
125179
|
+
activeStates: inputStates,
|
|
125180
|
+
hasUnwrittenReturn: false,
|
|
125181
|
+
isSupported: false
|
|
125182
|
+
};
|
|
125183
|
+
return {
|
|
125184
|
+
activeStates: inputStates,
|
|
125185
|
+
hasUnwrittenReturn: false,
|
|
125186
|
+
isSupported: true
|
|
125187
|
+
};
|
|
125188
|
+
};
|
|
125189
|
+
const callsUserDefinedFunction = (mainFunction, program) => {
|
|
125190
|
+
let callsFunction = false;
|
|
125191
|
+
visit(mainFunction.body, { function_call: { enter: ({ node }) => {
|
|
125192
|
+
const functionName = getGlslFunctionCallName(node);
|
|
125193
|
+
if (functionName && hasGlslFunctionDeclaration(program, functionName)) callsFunction = true;
|
|
125194
|
+
} } });
|
|
125195
|
+
return callsFunction;
|
|
125196
|
+
};
|
|
125197
|
+
const doesGlslMainWritePositionOnAllPaths = (program, source) => {
|
|
125198
|
+
const mainFunction = program.program.find((node) => node.type === "function" && node.prototype.header.name.identifier === "main") ?? null;
|
|
125199
|
+
if (!mainFunction || /^[ \t]*#[ \t]*define\b/m.test(maskGlslComments(source)) || callsUserDefinedFunction(mainFunction, program)) return {
|
|
125200
|
+
mainFunction,
|
|
125201
|
+
writesPositionOnAllPaths: null
|
|
125202
|
+
};
|
|
125203
|
+
const result = analyzeCompoundStatement(mainFunction.body, new Set([0]));
|
|
125204
|
+
if (!result.isSupported) return {
|
|
125205
|
+
mainFunction,
|
|
125206
|
+
writesPositionOnAllPaths: null
|
|
125207
|
+
};
|
|
125208
|
+
return {
|
|
125209
|
+
mainFunction,
|
|
125210
|
+
writesPositionOnAllPaths: !result.hasUnwrittenReturn && [...result.activeStates].every((state) => state === 15)
|
|
125211
|
+
};
|
|
125212
|
+
};
|
|
125213
|
+
//#endregion
|
|
125214
|
+
//#region src/plugin/rules/r3f/three-shader-require-position-on-all-paths.ts
|
|
125215
|
+
const threeShaderRequirePositionOnAllPaths = defineRule({
|
|
125216
|
+
id: "three-shader-require-position-on-all-paths",
|
|
125217
|
+
title: "Vertex shader leaves gl_Position undefined",
|
|
125218
|
+
category: "Correctness",
|
|
125219
|
+
severity: "error",
|
|
125220
|
+
recommendation: "Assign gl_Position before every return and on every path through main",
|
|
125221
|
+
create: (context) => ({ NewExpression(node) {
|
|
125222
|
+
const material = resolveStaticThreeShaderMaterial(node, context);
|
|
125223
|
+
if (!material?.vertexShader) return;
|
|
125224
|
+
const analysis = doesGlslMainWritePositionOnAllPaths(material.vertexShader.program, material.vertexShader.source.text);
|
|
125225
|
+
if (!analysis.mainFunction || analysis.writesPositionOnAllPaths !== false) return;
|
|
125226
|
+
context.report({
|
|
125227
|
+
node: material.vertexShader.source.getOriginNodeAtOffset(analysis.mainFunction.location?.start.offset ?? 0),
|
|
125228
|
+
message: "At least one path through vertex main returns or falls through without assigning gl_Position, leaving the clip-space position undefined"
|
|
125229
|
+
});
|
|
125230
|
+
} })
|
|
125231
|
+
});
|
|
125232
|
+
//#endregion
|
|
125233
|
+
//#region src/plugin/rules/r3f/utils/collect-authoritative-static-object-property-names.ts
|
|
125234
|
+
const collectAuthoritativeStaticObjectPropertyNames = (objectExpression) => {
|
|
125235
|
+
const propertyNames = /* @__PURE__ */ new Set();
|
|
125236
|
+
for (let propertyIndex = objectExpression.properties.length - 1; propertyIndex >= 0; propertyIndex -= 1) {
|
|
125237
|
+
const property = objectExpression.properties[propertyIndex];
|
|
125238
|
+
if (!property || isNodeOfType(property, "SpreadElement")) return null;
|
|
125239
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
125240
|
+
const propertyName = getStaticPropertyKeyName(property, { allowComputedString: true });
|
|
125241
|
+
if (!propertyName) return null;
|
|
125242
|
+
propertyNames.add(propertyName);
|
|
125243
|
+
}
|
|
125244
|
+
return propertyNames;
|
|
125245
|
+
};
|
|
125246
|
+
//#endregion
|
|
125247
|
+
//#region src/plugin/rules/r3f/three-shader-require-uniform-bindings.ts
|
|
125248
|
+
const MANAGED_UNIFORM_FEATURE_BY_NAME = new Map([
|
|
125249
|
+
["ambientLightColor", "lights"],
|
|
125250
|
+
["bindMatrix", "skinning"],
|
|
125251
|
+
["bindMatrixInverse", "skinning"],
|
|
125252
|
+
["boneTexture", "skinning"],
|
|
125253
|
+
["boneTextureSize", "skinning"],
|
|
125254
|
+
["clippingPlanes", "clipping"],
|
|
125255
|
+
["directionalLights", "lights"],
|
|
125256
|
+
["directionalLightShadows", "lights"],
|
|
125257
|
+
["directionalShadowMap", "lights"],
|
|
125258
|
+
["directionalShadowMatrix", "lights"],
|
|
125259
|
+
["fogColor", "fog"],
|
|
125260
|
+
["fogDensity", "fog"],
|
|
125261
|
+
["fogFar", "fog"],
|
|
125262
|
+
["fogNear", "fog"],
|
|
125263
|
+
["hemisphereLights", "lights"],
|
|
125264
|
+
["lightProbe", "lights"],
|
|
125265
|
+
["ltc_1", "lights"],
|
|
125266
|
+
["ltc_2", "lights"],
|
|
125267
|
+
["morphTargetBaseInfluence", "morphTargets"],
|
|
125268
|
+
["morphTargetInfluences", "morphTargets"],
|
|
125269
|
+
["morphTargetsTexture", "morphTargets"],
|
|
125270
|
+
["morphTargetsTextureSize", "morphTargets"],
|
|
125271
|
+
["pointLights", "lights"],
|
|
125272
|
+
["pointLightShadows", "lights"],
|
|
125273
|
+
["pointShadowMap", "lights"],
|
|
125274
|
+
["pointShadowMatrix", "lights"],
|
|
125275
|
+
["probesMax", "lights"],
|
|
125276
|
+
["probesMin", "lights"],
|
|
125277
|
+
["probesResolution", "lights"],
|
|
125278
|
+
["probesSH", "lights"],
|
|
125279
|
+
["receiveShadow", "lights"],
|
|
125280
|
+
["rectAreaLights", "lights"],
|
|
125281
|
+
["spotLightMap", "lights"],
|
|
125282
|
+
["spotLightMatrix", "lights"],
|
|
125283
|
+
["spotLights", "lights"],
|
|
125284
|
+
["spotLightShadows", "lights"],
|
|
125285
|
+
["spotShadowMap", "lights"],
|
|
125286
|
+
["transmissionSamplerMap", "transmission"],
|
|
125287
|
+
["transmissionSamplerSize", "transmission"]
|
|
125288
|
+
]);
|
|
125289
|
+
const isRendererManagedUniform = (uniformName, material) => {
|
|
125290
|
+
if (material.constructorName !== "ShaderMaterial" || !THREE_RENDERER_MANAGED_SHADER_UNIFORM_NAMES.has(uniformName)) return false;
|
|
125291
|
+
const requiredFeature = MANAGED_UNIFORM_FEATURE_BY_NAME.get(uniformName);
|
|
125292
|
+
if (!requiredFeature) return true;
|
|
125293
|
+
const featureExpression = material.properties.get(requiredFeature);
|
|
125294
|
+
return featureExpression !== void 0 && readStaticBoolean(featureExpression) !== false;
|
|
125295
|
+
};
|
|
125296
|
+
const isCustomUniform = (declaration, material) => declaration.qualifiers.has("uniform") && declaration.isStaticallyUsed && !declaration.name.startsWith("gl_") && !isRendererManagedUniform(declaration.name, material);
|
|
125297
|
+
const threeShaderRequireUniformBindings = defineRule({
|
|
125298
|
+
id: "three-shader-require-uniform-bindings",
|
|
125299
|
+
title: "Shader uniform has no material binding",
|
|
125300
|
+
category: "Correctness",
|
|
125301
|
+
severity: "error",
|
|
125302
|
+
recommendation: "Add every custom GLSL uniform to the ShaderMaterial uniforms object with a compatible value",
|
|
125303
|
+
create: (context) => ({ NewExpression(node) {
|
|
125304
|
+
const material = resolveStaticThreeShaderMaterial(node, context);
|
|
125305
|
+
if (!material) return;
|
|
125306
|
+
const declarationsByName = /* @__PURE__ */ new Map();
|
|
125307
|
+
for (const shader of [material.vertexShader, material.fragmentShader]) {
|
|
125308
|
+
if (!shader) continue;
|
|
125309
|
+
for (const declaration of collectGlslGlobalDeclarations(shader.program)) if (isCustomUniform(declaration, material) && !declarationsByName.has(declaration.name)) declarationsByName.set(declaration.name, {
|
|
125310
|
+
declaration,
|
|
125311
|
+
shader
|
|
125312
|
+
});
|
|
125313
|
+
}
|
|
125314
|
+
if (declarationsByName.size === 0) return;
|
|
125315
|
+
const uniformNames = [...declarationsByName.keys()];
|
|
125316
|
+
const uniformsExpression = material.properties.get("uniforms");
|
|
125317
|
+
let boundUniformNames;
|
|
125318
|
+
if (!uniformsExpression) boundUniformNames = /* @__PURE__ */ new Set();
|
|
125319
|
+
else {
|
|
125320
|
+
const uniformsObject = resolveStableOptionsObject(uniformsExpression, uniformNames, context.scopes, material.constructorNode);
|
|
125321
|
+
if (!uniformsObject || !isNodeOfType(uniformsObject, "ObjectExpression")) return;
|
|
125322
|
+
const propertyNames = collectAuthoritativeStaticObjectPropertyNames(uniformsObject);
|
|
125323
|
+
if (!propertyNames) return;
|
|
125324
|
+
boundUniformNames = propertyNames;
|
|
125325
|
+
}
|
|
125326
|
+
for (const [uniformName, declarationSource] of declarationsByName) {
|
|
125327
|
+
if (boundUniformNames.has(uniformName)) continue;
|
|
125328
|
+
context.report({
|
|
125329
|
+
node: declarationSource.shader.source.getOriginNodeAtOffset(declarationSource.declaration.node.location?.start.offset ?? 0),
|
|
125330
|
+
message: `GLSL uniform ${uniformName} has no matching entry in this material's uniforms object, so it keeps its default value`
|
|
125331
|
+
});
|
|
125332
|
+
}
|
|
125333
|
+
} })
|
|
125334
|
+
});
|
|
125335
|
+
//#endregion
|
|
123654
125336
|
//#region src/plugin/rules/r3f/three-tsl-no-js-uniform-branch.ts
|
|
123655
125337
|
const TSL_MODULES = new Set(["three/tsl", "three/webgpu"]);
|
|
123656
125338
|
const expressionReferencesTslUniformValue = (expression, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
@@ -136754,6 +138436,23 @@ const reactDoctorRules = [
|
|
|
136754
138436
|
requires: [...new Set(["three", ...threeNoCloneInAnimationLoop.requires ?? []])]
|
|
136755
138437
|
}
|
|
136756
138438
|
},
|
|
138439
|
+
{
|
|
138440
|
+
key: "react-doctor/three-no-material-recompile-in-animation-loop",
|
|
138441
|
+
id: "three-no-material-recompile-in-animation-loop",
|
|
138442
|
+
source: "react-doctor",
|
|
138443
|
+
originallyExternal: false,
|
|
138444
|
+
rule: {
|
|
138445
|
+
...threeNoMaterialRecompileInAnimationLoop,
|
|
138446
|
+
framework: "global",
|
|
138447
|
+
category: "Performance",
|
|
138448
|
+
tags: [...new Set([
|
|
138449
|
+
"three",
|
|
138450
|
+
"webgl",
|
|
138451
|
+
...threeNoMaterialRecompileInAnimationLoop.tags ?? []
|
|
138452
|
+
])],
|
|
138453
|
+
requires: [...new Set(["three", ...threeNoMaterialRecompileInAnimationLoop.requires ?? []])]
|
|
138454
|
+
}
|
|
138455
|
+
},
|
|
136757
138456
|
{
|
|
136758
138457
|
key: "react-doctor/three-no-new-in-animation-loop",
|
|
136759
138458
|
id: "three-no-new-in-animation-loop",
|
|
@@ -136792,6 +138491,23 @@ const reactDoctorRules = [
|
|
|
136792
138491
|
])]
|
|
136793
138492
|
}
|
|
136794
138493
|
},
|
|
138494
|
+
{
|
|
138495
|
+
key: "react-doctor/three-no-redundant-uniforms-need-update",
|
|
138496
|
+
id: "three-no-redundant-uniforms-need-update",
|
|
138497
|
+
source: "react-doctor",
|
|
138498
|
+
originallyExternal: false,
|
|
138499
|
+
rule: {
|
|
138500
|
+
...threeNoRedundantUniformsNeedUpdate,
|
|
138501
|
+
framework: "global",
|
|
138502
|
+
category: "Performance",
|
|
138503
|
+
tags: [...new Set([
|
|
138504
|
+
"three",
|
|
138505
|
+
"webgl",
|
|
138506
|
+
...threeNoRedundantUniformsNeedUpdate.tags ?? []
|
|
138507
|
+
])],
|
|
138508
|
+
requires: [...new Set(["three", ...threeNoRedundantUniformsNeedUpdate.requires ?? []])]
|
|
138509
|
+
}
|
|
138510
|
+
},
|
|
136795
138511
|
{
|
|
136796
138512
|
key: "react-doctor/three-no-state-in-animation-loop",
|
|
136797
138513
|
id: "three-no-state-in-animation-loop",
|
|
@@ -136834,6 +138550,57 @@ const reactDoctorRules = [
|
|
|
136834
138550
|
])]
|
|
136835
138551
|
}
|
|
136836
138552
|
},
|
|
138553
|
+
{
|
|
138554
|
+
key: "react-doctor/three-on-before-compile-require-program-cache-key",
|
|
138555
|
+
id: "three-on-before-compile-require-program-cache-key",
|
|
138556
|
+
source: "react-doctor",
|
|
138557
|
+
originallyExternal: false,
|
|
138558
|
+
rule: {
|
|
138559
|
+
...threeOnBeforeCompileRequireProgramCacheKey,
|
|
138560
|
+
framework: "global",
|
|
138561
|
+
category: "Bugs",
|
|
138562
|
+
tags: [...new Set([
|
|
138563
|
+
"three",
|
|
138564
|
+
"webgl",
|
|
138565
|
+
...threeOnBeforeCompileRequireProgramCacheKey.tags ?? []
|
|
138566
|
+
])],
|
|
138567
|
+
requires: [...new Set(["three", ...threeOnBeforeCompileRequireProgramCacheKey.requires ?? []])]
|
|
138568
|
+
}
|
|
138569
|
+
},
|
|
138570
|
+
{
|
|
138571
|
+
key: "react-doctor/three-raw-shader-require-fragment-float-precision",
|
|
138572
|
+
id: "three-raw-shader-require-fragment-float-precision",
|
|
138573
|
+
source: "react-doctor",
|
|
138574
|
+
originallyExternal: false,
|
|
138575
|
+
rule: {
|
|
138576
|
+
...threeRawShaderRequireFragmentFloatPrecision,
|
|
138577
|
+
framework: "global",
|
|
138578
|
+
category: "Bugs",
|
|
138579
|
+
tags: [...new Set([
|
|
138580
|
+
"three",
|
|
138581
|
+
"webgl",
|
|
138582
|
+
...threeRawShaderRequireFragmentFloatPrecision.tags ?? []
|
|
138583
|
+
])],
|
|
138584
|
+
requires: [...new Set(["three", ...threeRawShaderRequireFragmentFloatPrecision.requires ?? []])]
|
|
138585
|
+
}
|
|
138586
|
+
},
|
|
138587
|
+
{
|
|
138588
|
+
key: "react-doctor/three-raw-shader-require-glsl3-version",
|
|
138589
|
+
id: "three-raw-shader-require-glsl3-version",
|
|
138590
|
+
source: "react-doctor",
|
|
138591
|
+
originallyExternal: false,
|
|
138592
|
+
rule: {
|
|
138593
|
+
...threeRawShaderRequireGlsl3Version,
|
|
138594
|
+
framework: "global",
|
|
138595
|
+
category: "Bugs",
|
|
138596
|
+
tags: [...new Set([
|
|
138597
|
+
"three",
|
|
138598
|
+
"webgl",
|
|
138599
|
+
...threeRawShaderRequireGlsl3Version.tags ?? []
|
|
138600
|
+
])],
|
|
138601
|
+
requires: [...new Set(["three", ...threeRawShaderRequireGlsl3Version.requires ?? []])]
|
|
138602
|
+
}
|
|
138603
|
+
},
|
|
136837
138604
|
{
|
|
136838
138605
|
key: "react-doctor/three-require-animation-mixer-cleanup",
|
|
136839
138606
|
id: "three-require-animation-mixer-cleanup",
|
|
@@ -137053,6 +138820,278 @@ const reactDoctorRules = [
|
|
|
137053
138820
|
])]
|
|
137054
138821
|
}
|
|
137055
138822
|
},
|
|
138823
|
+
{
|
|
138824
|
+
key: "react-doctor/three-shader-no-constant-out-of-bounds-index",
|
|
138825
|
+
id: "three-shader-no-constant-out-of-bounds-index",
|
|
138826
|
+
source: "react-doctor",
|
|
138827
|
+
originallyExternal: false,
|
|
138828
|
+
rule: {
|
|
138829
|
+
...threeShaderNoConstantOutOfBoundsIndex,
|
|
138830
|
+
framework: "global",
|
|
138831
|
+
category: "Bugs",
|
|
138832
|
+
tags: [...new Set([
|
|
138833
|
+
"three",
|
|
138834
|
+
"webgl",
|
|
138835
|
+
...threeShaderNoConstantOutOfBoundsIndex.tags ?? []
|
|
138836
|
+
])],
|
|
138837
|
+
requires: [...new Set(["three", ...threeShaderNoConstantOutOfBoundsIndex.requires ?? []])]
|
|
138838
|
+
}
|
|
138839
|
+
},
|
|
138840
|
+
{
|
|
138841
|
+
key: "react-doctor/three-shader-no-derivatives-in-nonuniform-flow",
|
|
138842
|
+
id: "three-shader-no-derivatives-in-nonuniform-flow",
|
|
138843
|
+
source: "react-doctor",
|
|
138844
|
+
originallyExternal: false,
|
|
138845
|
+
rule: {
|
|
138846
|
+
...threeShaderNoDerivativesInNonuniformFlow,
|
|
138847
|
+
framework: "global",
|
|
138848
|
+
category: "Bugs",
|
|
138849
|
+
tags: [...new Set([
|
|
138850
|
+
"three",
|
|
138851
|
+
"webgl",
|
|
138852
|
+
...threeShaderNoDerivativesInNonuniformFlow.tags ?? []
|
|
138853
|
+
])],
|
|
138854
|
+
requires: [...new Set(["three", ...threeShaderNoDerivativesInNonuniformFlow.requires ?? []])]
|
|
138855
|
+
}
|
|
138856
|
+
},
|
|
138857
|
+
{
|
|
138858
|
+
key: "react-doctor/three-shader-no-invalid-clamp-bounds",
|
|
138859
|
+
id: "three-shader-no-invalid-clamp-bounds",
|
|
138860
|
+
source: "react-doctor",
|
|
138861
|
+
originallyExternal: false,
|
|
138862
|
+
rule: {
|
|
138863
|
+
...threeShaderNoInvalidClampBounds,
|
|
138864
|
+
framework: "global",
|
|
138865
|
+
category: "Bugs",
|
|
138866
|
+
tags: [...new Set([
|
|
138867
|
+
"three",
|
|
138868
|
+
"webgl",
|
|
138869
|
+
...threeShaderNoInvalidClampBounds.tags ?? []
|
|
138870
|
+
])],
|
|
138871
|
+
requires: [...new Set(["three", ...threeShaderNoInvalidClampBounds.requires ?? []])]
|
|
138872
|
+
}
|
|
138873
|
+
},
|
|
138874
|
+
{
|
|
138875
|
+
key: "react-doctor/three-shader-no-invalid-constant-bit-operations",
|
|
138876
|
+
id: "three-shader-no-invalid-constant-bit-operations",
|
|
138877
|
+
source: "react-doctor",
|
|
138878
|
+
originallyExternal: false,
|
|
138879
|
+
rule: {
|
|
138880
|
+
...threeShaderNoInvalidConstantBitOperations,
|
|
138881
|
+
framework: "global",
|
|
138882
|
+
category: "Bugs",
|
|
138883
|
+
tags: [...new Set([
|
|
138884
|
+
"three",
|
|
138885
|
+
"webgl",
|
|
138886
|
+
...threeShaderNoInvalidConstantBitOperations.tags ?? []
|
|
138887
|
+
])],
|
|
138888
|
+
requires: [...new Set(["three", ...threeShaderNoInvalidConstantBitOperations.requires ?? []])]
|
|
138889
|
+
}
|
|
138890
|
+
},
|
|
138891
|
+
{
|
|
138892
|
+
key: "react-doctor/three-shader-no-invalid-constant-math",
|
|
138893
|
+
id: "three-shader-no-invalid-constant-math",
|
|
138894
|
+
source: "react-doctor",
|
|
138895
|
+
originallyExternal: false,
|
|
138896
|
+
rule: {
|
|
138897
|
+
...threeShaderNoInvalidConstantMath,
|
|
138898
|
+
framework: "global",
|
|
138899
|
+
category: "Bugs",
|
|
138900
|
+
tags: [...new Set([
|
|
138901
|
+
"three",
|
|
138902
|
+
"webgl",
|
|
138903
|
+
...threeShaderNoInvalidConstantMath.tags ?? []
|
|
138904
|
+
])],
|
|
138905
|
+
requires: [...new Set(["three", ...threeShaderNoInvalidConstantMath.requires ?? []])]
|
|
138906
|
+
}
|
|
138907
|
+
},
|
|
138908
|
+
{
|
|
138909
|
+
key: "react-doctor/three-shader-no-invalid-smoothstep-edges",
|
|
138910
|
+
id: "three-shader-no-invalid-smoothstep-edges",
|
|
138911
|
+
source: "react-doctor",
|
|
138912
|
+
originallyExternal: false,
|
|
138913
|
+
rule: {
|
|
138914
|
+
...threeShaderNoInvalidSmoothstepEdges,
|
|
138915
|
+
framework: "global",
|
|
138916
|
+
category: "Bugs",
|
|
138917
|
+
tags: [...new Set([
|
|
138918
|
+
"three",
|
|
138919
|
+
"webgl",
|
|
138920
|
+
...threeShaderNoInvalidSmoothstepEdges.tags ?? []
|
|
138921
|
+
])],
|
|
138922
|
+
requires: [...new Set(["three", ...threeShaderNoInvalidSmoothstepEdges.requires ?? []])]
|
|
138923
|
+
}
|
|
138924
|
+
},
|
|
138925
|
+
{
|
|
138926
|
+
key: "react-doctor/three-shader-no-inverse-of-uniform",
|
|
138927
|
+
id: "three-shader-no-inverse-of-uniform",
|
|
138928
|
+
source: "react-doctor",
|
|
138929
|
+
originallyExternal: false,
|
|
138930
|
+
rule: {
|
|
138931
|
+
...threeShaderNoInverseOfUniform,
|
|
138932
|
+
framework: "global",
|
|
138933
|
+
category: "Performance",
|
|
138934
|
+
tags: [...new Set([
|
|
138935
|
+
"three",
|
|
138936
|
+
"webgl",
|
|
138937
|
+
...threeShaderNoInverseOfUniform.tags ?? []
|
|
138938
|
+
])],
|
|
138939
|
+
requires: [...new Set(["three", ...threeShaderNoInverseOfUniform.requires ?? []])]
|
|
138940
|
+
}
|
|
138941
|
+
},
|
|
138942
|
+
{
|
|
138943
|
+
key: "react-doctor/three-shader-no-redeclared-builtins",
|
|
138944
|
+
id: "three-shader-no-redeclared-builtins",
|
|
138945
|
+
source: "react-doctor",
|
|
138946
|
+
originallyExternal: false,
|
|
138947
|
+
rule: {
|
|
138948
|
+
...threeShaderNoRedeclaredBuiltins,
|
|
138949
|
+
framework: "global",
|
|
138950
|
+
category: "Bugs",
|
|
138951
|
+
tags: [...new Set([
|
|
138952
|
+
"three",
|
|
138953
|
+
"webgl",
|
|
138954
|
+
...threeShaderNoRedeclaredBuiltins.tags ?? []
|
|
138955
|
+
])],
|
|
138956
|
+
requires: [...new Set(["three", ...threeShaderNoRedeclaredBuiltins.requires ?? []])]
|
|
138957
|
+
}
|
|
138958
|
+
},
|
|
138959
|
+
{
|
|
138960
|
+
key: "react-doctor/three-shader-no-redundant-frag-depth",
|
|
138961
|
+
id: "three-shader-no-redundant-frag-depth",
|
|
138962
|
+
source: "react-doctor",
|
|
138963
|
+
originallyExternal: false,
|
|
138964
|
+
rule: {
|
|
138965
|
+
...threeShaderNoRedundantFragDepth,
|
|
138966
|
+
framework: "global",
|
|
138967
|
+
category: "Performance",
|
|
138968
|
+
tags: [...new Set([
|
|
138969
|
+
"three",
|
|
138970
|
+
"webgl",
|
|
138971
|
+
...threeShaderNoRedundantFragDepth.tags ?? []
|
|
138972
|
+
])],
|
|
138973
|
+
requires: [...new Set(["three", ...threeShaderNoRedundantFragDepth.requires ?? []])]
|
|
138974
|
+
}
|
|
138975
|
+
},
|
|
138976
|
+
{
|
|
138977
|
+
key: "react-doctor/three-shader-no-version-directive",
|
|
138978
|
+
id: "three-shader-no-version-directive",
|
|
138979
|
+
source: "react-doctor",
|
|
138980
|
+
originallyExternal: false,
|
|
138981
|
+
rule: {
|
|
138982
|
+
...threeShaderNoVersionDirective,
|
|
138983
|
+
framework: "global",
|
|
138984
|
+
category: "Bugs",
|
|
138985
|
+
tags: [...new Set([
|
|
138986
|
+
"three",
|
|
138987
|
+
"webgl",
|
|
138988
|
+
...threeShaderNoVersionDirective.tags ?? []
|
|
138989
|
+
])],
|
|
138990
|
+
requires: [...new Set(["three", ...threeShaderNoVersionDirective.requires ?? []])]
|
|
138991
|
+
}
|
|
138992
|
+
},
|
|
138993
|
+
{
|
|
138994
|
+
key: "react-doctor/three-shader-prefer-small-integer-pow",
|
|
138995
|
+
id: "three-shader-prefer-small-integer-pow",
|
|
138996
|
+
source: "react-doctor",
|
|
138997
|
+
originallyExternal: false,
|
|
138998
|
+
rule: {
|
|
138999
|
+
...threeShaderPreferSmallIntegerPow,
|
|
139000
|
+
framework: "global",
|
|
139001
|
+
category: "Performance",
|
|
139002
|
+
tags: [...new Set([
|
|
139003
|
+
"three",
|
|
139004
|
+
"webgl",
|
|
139005
|
+
...threeShaderPreferSmallIntegerPow.tags ?? []
|
|
139006
|
+
])],
|
|
139007
|
+
requires: [...new Set(["three", ...threeShaderPreferSmallIntegerPow.requires ?? []])]
|
|
139008
|
+
}
|
|
139009
|
+
},
|
|
139010
|
+
{
|
|
139011
|
+
key: "react-doctor/three-shader-prefer-squared-distance-comparison",
|
|
139012
|
+
id: "three-shader-prefer-squared-distance-comparison",
|
|
139013
|
+
source: "react-doctor",
|
|
139014
|
+
originallyExternal: false,
|
|
139015
|
+
rule: {
|
|
139016
|
+
...threeShaderPreferSquaredDistanceComparison,
|
|
139017
|
+
framework: "global",
|
|
139018
|
+
category: "Performance",
|
|
139019
|
+
tags: [...new Set([
|
|
139020
|
+
"three",
|
|
139021
|
+
"webgl",
|
|
139022
|
+
...threeShaderPreferSquaredDistanceComparison.tags ?? []
|
|
139023
|
+
])],
|
|
139024
|
+
requires: [...new Set(["three", ...threeShaderPreferSquaredDistanceComparison.requires ?? []])]
|
|
139025
|
+
}
|
|
139026
|
+
},
|
|
139027
|
+
{
|
|
139028
|
+
key: "react-doctor/three-shader-require-matching-uniforms",
|
|
139029
|
+
id: "three-shader-require-matching-uniforms",
|
|
139030
|
+
source: "react-doctor",
|
|
139031
|
+
originallyExternal: false,
|
|
139032
|
+
rule: {
|
|
139033
|
+
...threeShaderRequireMatchingUniforms,
|
|
139034
|
+
framework: "global",
|
|
139035
|
+
category: "Bugs",
|
|
139036
|
+
tags: [...new Set([
|
|
139037
|
+
"three",
|
|
139038
|
+
"webgl",
|
|
139039
|
+
...threeShaderRequireMatchingUniforms.tags ?? []
|
|
139040
|
+
])],
|
|
139041
|
+
requires: [...new Set(["three", ...threeShaderRequireMatchingUniforms.requires ?? []])]
|
|
139042
|
+
}
|
|
139043
|
+
},
|
|
139044
|
+
{
|
|
139045
|
+
key: "react-doctor/three-shader-require-matching-varyings",
|
|
139046
|
+
id: "three-shader-require-matching-varyings",
|
|
139047
|
+
source: "react-doctor",
|
|
139048
|
+
originallyExternal: false,
|
|
139049
|
+
rule: {
|
|
139050
|
+
...threeShaderRequireMatchingVaryings,
|
|
139051
|
+
framework: "global",
|
|
139052
|
+
category: "Bugs",
|
|
139053
|
+
tags: [...new Set([
|
|
139054
|
+
"three",
|
|
139055
|
+
"webgl",
|
|
139056
|
+
...threeShaderRequireMatchingVaryings.tags ?? []
|
|
139057
|
+
])],
|
|
139058
|
+
requires: [...new Set(["three", ...threeShaderRequireMatchingVaryings.requires ?? []])]
|
|
139059
|
+
}
|
|
139060
|
+
},
|
|
139061
|
+
{
|
|
139062
|
+
key: "react-doctor/three-shader-require-position-on-all-paths",
|
|
139063
|
+
id: "three-shader-require-position-on-all-paths",
|
|
139064
|
+
source: "react-doctor",
|
|
139065
|
+
originallyExternal: false,
|
|
139066
|
+
rule: {
|
|
139067
|
+
...threeShaderRequirePositionOnAllPaths,
|
|
139068
|
+
framework: "global",
|
|
139069
|
+
category: "Bugs",
|
|
139070
|
+
tags: [...new Set([
|
|
139071
|
+
"three",
|
|
139072
|
+
"webgl",
|
|
139073
|
+
...threeShaderRequirePositionOnAllPaths.tags ?? []
|
|
139074
|
+
])],
|
|
139075
|
+
requires: [...new Set(["three", ...threeShaderRequirePositionOnAllPaths.requires ?? []])]
|
|
139076
|
+
}
|
|
139077
|
+
},
|
|
139078
|
+
{
|
|
139079
|
+
key: "react-doctor/three-shader-require-uniform-bindings",
|
|
139080
|
+
id: "three-shader-require-uniform-bindings",
|
|
139081
|
+
source: "react-doctor",
|
|
139082
|
+
originallyExternal: false,
|
|
139083
|
+
rule: {
|
|
139084
|
+
...threeShaderRequireUniformBindings,
|
|
139085
|
+
framework: "global",
|
|
139086
|
+
category: "Bugs",
|
|
139087
|
+
tags: [...new Set([
|
|
139088
|
+
"three",
|
|
139089
|
+
"webgl",
|
|
139090
|
+
...threeShaderRequireUniformBindings.tags ?? []
|
|
139091
|
+
])],
|
|
139092
|
+
requires: [...new Set(["three", ...threeShaderRequireUniformBindings.requires ?? []])]
|
|
139093
|
+
}
|
|
139094
|
+
},
|
|
137056
139095
|
{
|
|
137057
139096
|
key: "react-doctor/three-tsl-no-js-uniform-branch",
|
|
137058
139097
|
id: "three-tsl-no-js-uniform-branch",
|