Skip to content

Commit

Permalink
Merge pull request #10 from productboard/pd-151-do-not-report-unused-…
Browse files Browse the repository at this point in the history
…node

PD-151: do not report unused node
  • Loading branch information
comatory authored Sep 19, 2023
2 parents 3b880f4 + 02eabd8 commit b573618
Show file tree
Hide file tree
Showing 4 changed files with 346 additions and 6 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## v1.9.6

- rule `unused-fields` has additional functionality which allows you to pass option `edgesAndNodesWhiteListFunctionName`, this is a name of a function and if that function receives an object with `edges` as argument, it will ignore `edges` and `node` warning for unused fields because it considers them to be used inside this function

## v1.9.3

- allow to pass custom error message to `no-future-added-value` rule
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@productboard/eslint-plugin-relay",
"private": true,
"version": "1.9.5",
"version": "1.9.6",
"description": "ESLint plugin for Relay.",
"main": "eslint-plugin-relay",
"repository": "https://github.com/productboard/eslint-plugin-relay",
Expand Down
107 changes: 103 additions & 4 deletions src/rule-unused-fields.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const ESLINT_DISABLE_COMMENT =

function getGraphQLFieldNames(graphQLAst) {
const fieldNames = {};
const edgesParents = [];

function walkAST(node, ignoreLevel) {
if (node.kind === 'Field' && !ignoreLevel) {
Expand All @@ -26,6 +27,10 @@ function getGraphQLFieldNames(graphQLAst) {
}
const nameNode = node.alias || node.name;
fieldNames[nameNode.value] = nameNode;

if (node.kind === 'Field' && node.selectionSet && containsEdges(node)) {
edgesParents.push(node.name.value);
}
}
if (node.kind === 'OperationDefinition') {
if (
Expand All @@ -42,6 +47,7 @@ function getGraphQLFieldNames(graphQLAst) {
});
return;
}

for (const prop in node) {
const value = node[prop];
if (prop === 'loc') {
Expand All @@ -58,7 +64,8 @@ function getGraphQLFieldNames(graphQLAst) {
}

walkAST(graphQLAst);
return fieldNames;

return {fieldNames, edgesParents};
}

function isGraphQLTemplate(node) {
Expand Down Expand Up @@ -93,10 +100,21 @@ function isPageInfoField(field) {
}
}

function containsEdges(node) {
return node.selectionSet.selections.some(
selection =>
selection.name && selection.name.value && selection.name.value === 'edges'
);
}

function rule(context) {
const edgesAndNodesWhiteListFunctionName = context.options[0]
? context.options[0].edgesAndNodesWhiteListFunctionName
: null;
let currentMethod = [];
let foundMemberAccesses = {};
let templateLiterals = [];
const edgesAndNodesWhiteListFunctionCalls = [];

function visitGetByPathCall(node) {
// The `getByPath` utility accesses nested fields in the form
Expand Down Expand Up @@ -129,28 +147,88 @@ function rule(context) {
}
}

function getEdgesAndNodesWhiteListFunctionCallArguments(calls) {
return calls.flatMap(call =>
call.arguments.map(arg => {
if (arg.type === 'Identifier') {
return arg.name;
} else if ('expression' in arg) {
return arg.expression.property.name;
} else if ('property' in arg) {
return arg.property.name;
}
return null;
})
);
}

// Naively checks whether the function call for
// `edgesAndNodesWhiteListFunctionName` contains arguments
// that are property accesses on a field that contains
// `edges`
function wasWhiteListFunctionCalledWithEdgesAndNodesArgument(
edgesParents,
callArguments
) {
const callArgumentsSet = new Set([...callArguments]);
const edgesParentsSet = new Set([...edgesParents]);
const intersect = new Set(
[...callArgumentsSet].filter(callArgument =>
edgesParentsSet.has(callArgument)
)
);

return intersect.size > 0;
}

function shouldIgnoreWhiteListedCollectConnectionFields(
field,
whiteListFunctionCalledWithEdgesAndNodes
) {
return (
(field === 'edges' || field === 'node') &&
whiteListFunctionCalledWithEdgesAndNodes
);
}

return {
Program(_node) {
currentMethod = [];
foundMemberAccesses = {};
templateLiterals = [];
},
'Program:exit'(_node) {
const edgesAndNodesWhiteListFunctionCallArguments =
getEdgesAndNodesWhiteListFunctionCallArguments(
edgesAndNodesWhiteListFunctionCalls
);
templateLiterals.forEach(templateLiteral => {
const graphQLAst = getGraphQLAST(templateLiteral);
if (!graphQLAst) {
// ignore nodes with syntax errors, they're handled by rule-graphql-syntax
return;
}

const queriedFields = getGraphQLFieldNames(graphQLAst);
const {fieldNames: queriedFields, edgesParents} =
getGraphQLFieldNames(graphQLAst);

const whiteListFunctionCalledWithEdgesAndNodes =
wasWhiteListFunctionCalledWithEdgesAndNodesArgument(
edgesParents,
edgesAndNodesWhiteListFunctionCallArguments
);

for (const field in queriedFields) {
if (
!foundMemberAccesses[field] &&
!isPageInfoField(field) &&
// Do not warn for unused __typename which can be a workaround
// when only interested in existence of an object.
field !== '__typename'
field !== '__typename' &&
!shouldIgnoreWhiteListedCollectConnectionFields(
field,
whiteListFunctionCalledWithEdgesAndNodes
)
) {
context.report({
node: templateLiteral,
Expand All @@ -172,6 +250,9 @@ function rule(context) {
return;
}
switch (node.callee.name) {
case edgesAndNodesWhiteListFunctionName:
edgesAndNodesWhiteListFunctionCalls.push(node);
break;
case 'getByPath':
visitGetByPathCall(node);
break;
Expand Down Expand Up @@ -206,4 +287,22 @@ function rule(context) {
};
}

module.exports = rule;
module.exports = {
meta: {
docs: {
description: 'Warns about unused fields in graphql queries'
},
schema: [
{
type: 'object',
properties: {
edgesAndNodesWhiteListFunctionName: {
type: 'string'
}
},
additionalProperties: false
}
]
},
create: rule
};
Loading

0 comments on commit b573618

Please sign in to comment.