Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions .eslint/noAmbiguousReturnTypes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* @import {RuleContext, RuleContext, RuleListener, RuleModule, Node} from 'eslint';
*/

/**
* Checks if a comment contains not object return types
*
* @param {string} commentText - The JSDoc comment text
* @returns {boolean} True if the comment has an object return type
*/
const hasObjectReturnType = commentText => {
const regex = /\*\s+@returns?\s+\{(object|Object|any|Record<\w+,\s*(any|object)>)\b/i;
return regex.test(commentText);
};

/**
* Creates an ESLint rule handler to detect and report usages of 'object' as a return type in JSDoc
*
* @param {RuleContext} context - The ESLint rule context object
* @returns {RuleListener} The rule listener with handlers for JSDoc comments
*/
function create(context) {
const sourceCode = context.sourceCode;

/**
* Processes a node to find its JSDoc comment and check for object return types
*
* @param {Node} node - The AST node to check
*/
const checkJSDocComment = node => {
const comments = sourceCode.getAllComments();

// Find the closest comment before the node that looks like JSDoc
const jsDocComments = comments.filter(
comment =>
comment.type === 'Block' &&
comment.value.startsWith('*') &&
comment.loc.end.line + 1 >= node.loc.start.line &&
comment.loc.end.line < node.loc.start.line + 3,
);

for (const comment of jsDocComments) {
if (hasObjectReturnType(comment.value)) {
context.report({
node: comment,
messageId: 'noAmbiguousReturnTypes',
});
}
}
};

return {
FunctionDeclaration: checkJSDocComment,
FunctionExpression: checkJSDocComment,
ArrowFunctionExpression: checkJSDocComment,
MethodDefinition: checkJSDocComment,
};
}

/**
* ESLint rule to enforce specific return types instead of generic 'object' in JSDoc
*
* @type {RuleModule}
*/
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: "Disallow using 'object', 'any', and 'Record<string, any>' type in JSDoc @returns tag",
category: 'Best Practices',
recommended: true,
},
fixable: null,
schema: [],
messages: {
noAmbiguousReturnTypes:
"Don't use 'object', 'any', 'Record<string, any>', 'Record<string, object>' as a return type, use a more specific type instead",
},
},
create,
};
1 change: 1 addition & 0 deletions buildConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const DEFAULT_RELEASE_FOLDER_PATH = path.resolve(__dirname, 'release');
const EXCLUDED_EXTENSIONS = ['.js', '.g4', '.interp', '.tokens'];
const EXCLUDED_FILES = [
'.github',
'.eslint',
'.DS_Store',
'.editorconfig',
'.eslintignore',
Expand Down
9 changes: 9 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ const eslintConfigPrettier = require('eslint-config-prettier');
const importPlugin = require('eslint-plugin-import');
const unusedImportsPlugin = require('eslint-plugin-unused-imports');
const jsdocPlugin = require('eslint-plugin-jsdoc');
const noAmbiguousReturnTypes = require('./.eslint/noAmbiguousReturnTypes');

const customRulesConfig = {
rules: {
'no-ambiguous-return-types': noAmbiguousReturnTypes,
},
};

/**
* @type {import('eslint').Linter.Config[]}
Expand All @@ -16,6 +23,7 @@ module.exports = [
'jsdoc': jsdocPlugin,
'unused-imports': unusedImportsPlugin,
'prettier': prettierPlugin,
'custom': customRulesConfig,
},
},
{
Expand All @@ -34,6 +42,7 @@ module.exports = [
files: ['**/*.{js,cjs,mjs}'],
rules: {
...eslintConfigPrettier.rules,
'custom/no-ambiguous-return-types': 'error',
'jsdoc/require-jsdoc': [
'error',
{
Expand Down
13 changes: 9 additions & 4 deletions forward_engineering/api.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* @import {ContainerLevelScriptFEData, GenerateContainerLevelScriptCallback, ValidateScriptCallback, Logger} from "../shared/types/types"
* @import {ContainerLevelScriptFEData, GenerateContainerLevelScriptCallback, ValidateScriptCallback, Logger, FEDirectiveDefinitionsSchema} from "../shared/types/types"
*/

const validationHelper = require('./helpers/schemaValidationHelper');
Expand Down Expand Up @@ -41,7 +41,12 @@ module.exports = {
});

const directiveStatements = getDirectives({
directives: getModelDefinitionsBySubtype({ modelDefinitions, subtype: 'directive' }),
directives: /** @type {FEDirectiveDefinitionsSchema} */ (
getModelDefinitionsBySubtype({
modelDefinitions,
subtype: 'directive',
})
),
definitionsIdToNameMap,
});

Expand All @@ -55,7 +60,7 @@ module.exports = {
...rootTypeStatements,
...typeDefinitionStatements,
]
.filter(Boolean)
.filter(feStatement => feStatement !== null)
.map(feStatement => formatFEStatement({ feStatement }))
.join('\n\n');

Expand All @@ -82,7 +87,7 @@ module.exports = {
cb(null, validationResults);
} catch (e) {
logger.log('error', { error: e }, 'GraphQL schema validation error');
cb(e.message);
cb(e);
}
},
};
4 changes: 2 additions & 2 deletions forward_engineering/helpers/addRequiredHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
*
* @param {object} param0
* @param {string} param0.type - The type name statement.
* @param {boolean} param0.required - Indicates if the field is required.
* @param {boolean} [param0.required] - Indicates if the field is required. Default is `false`
* @returns {string} - The type name with required indicator.
*/
function addRequired({ type, required }) {
function addRequired({ type, required = false }) {
if (required) {
return `${type}!`;
}
Expand Down
4 changes: 2 additions & 2 deletions forward_engineering/helpers/feStatementFormatHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ function formatDescription(description) {
}

/**
* @param {FEStatement & { isParentActivated: boolean }} params
* @param {Partial<FEStatement> & { isParentActivated: boolean }} params
* @returns {string}
*/
function formatNestedStatements({
Expand All @@ -73,7 +73,7 @@ function formatNestedStatements({
useNestedStatementSigns,
startNestedStatementsSign,
endNestedStatementsSign,
nestedStatementsSeparator,
nestedStatementsSeparator = '',
comment = '',
}) {
if (!nestedStatements?.length) {
Expand Down
7 changes: 6 additions & 1 deletion forward_engineering/helpers/generateIdToNameMap.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
/**
* @import {IdToNameMap} from '../../shared/types/types';
*/

// The system names are the names of the GraphQL system types (configured in the plugin to group different kinds of types).
const SYSTEM_NAMES = ['Scalars', 'Enums', 'Objects', 'Interfaces', 'Input objects', 'Unions', 'Directives'];

/**
* Generate the ID to Name map for the given model definitions schema.
*
* @param {object} modelDefinitionsSchema - The model definitions object properties.
* @returns {object} - The ID to Name map
* @returns {IdToNameMap} - The ID to Name map
*/
const generateIdToNameMap = (modelDefinitionsSchema = {}) => {
/** @type {IdToNameMap} */
let idToNameMap = {};

Object.entries(modelDefinitionsSchema).forEach(([name, schema]) => {
Expand Down
12 changes: 4 additions & 8 deletions forward_engineering/helpers/schemaValidationHelper.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/**
* @import {GraphQLError} from "graphql"
* @import {ValidationResponseItem} from "../../shared/types/types"
*/

Expand All @@ -16,7 +17,7 @@ function validate({ schema }) {
try {
builtSchema = buildSchema(schema);
} catch (error) {
return [mapValidationError(error)];
return [mapValidationError(/** @type {GraphQLError} */ (error))];
}

const errors = validateSchema(builtSchema);
Expand All @@ -30,9 +31,7 @@ function validate({ schema }) {
/**
* Maps a GraphQL validation error to a custom error format.
*
* @param {object} error - The GraphQL validation error.
* @param {string} error.message - The error message.
* @param {object[]} [error.locations] - The locations of the error in the schema.
* @param {GraphQLError} error - The GraphQL validation error.
* @returns {ValidationResponseItem} The mapped error object.
*/
function mapValidationError(error) {
Expand All @@ -46,10 +45,7 @@ function mapValidationError(error) {
/**
* Gets the error position message from a GraphQL validation error.
*
* @param {object} error - The GraphQL validation error.
* @param {object[]} [error.locations] - The locations of the error in the schema.
* @param {number} error.locations[].line - The line number of the error location.
* @param {number} error.locations[].column - The column number of the error location.
* @param {GraphQLError} error - The GraphQL validation error.
* @returns {string} The error position message.
*/
function getErrorPositionMessage(error) {
Expand Down
6 changes: 3 additions & 3 deletions forward_engineering/mappers/argumentDefaultValue.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ const getArgumentDefaultValue = ({ type, defaultValue = '' }) => {
return `"${defaultValue}"`;
}
case 'Int': {
return Number.parseInt(defaultValue);
return Number.parseInt(String(defaultValue));
}
case 'Float': {
return Number.parseFloat(defaultValue);
return Number.parseFloat(String(defaultValue));
}
case 'Boolean': {
return defaultValue.toLowerCase() === 'true';
return String(defaultValue).toLowerCase() === 'true';
}
default: {
return defaultValue;
Expand Down
5 changes: 3 additions & 2 deletions forward_engineering/mappers/arguments.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ const getArgumentType = ({ graphqlArgument, idToNameMap = {} }) => {

if (argumentType === 'List') {
const firstListItem = graphqlArgument.listItems?.[0] || {};
const listItemType = idToNameMap[firstListItem.type] || getCheckedType({ type: firstListItem.type }) || '';
const listTypeKey = firstListItem.type || '';
const listItemType = idToNameMap[listTypeKey] || getCheckedType({ type: listTypeKey }) || '';

if (!listItemType) {
argumentType = EMPTY_LIST;
Expand Down Expand Up @@ -71,7 +72,7 @@ const mapArgument = ({ graphqlArgument, idToNameMap = {} }) => {
* Maps an array of arguments to a formatted string with all configured properties.
*
* @param {object} args - Arguments object.
* @param {Argument[]} args.graphqlArguments - The arguments to map.
* @param {Argument[]} [args.graphqlArguments] - The arguments to map.
* @param {IdToNameMap} [args.idToNameMap] - The ID to name map of all available types in model.
* @returns {ArgumentsResultStatement} Returns an object containing the arguments as a formatted string and a warning
* comment if any.
Expand Down
17 changes: 3 additions & 14 deletions forward_engineering/mappers/customScalars.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,16 @@
/**
* @import {FEStatement, DirectivePropertyData, IdToNameMap} from "../../shared/types/types"
* @import {FEStatement, IdToNameMap, FECustomScalarDefinition, FECustomScalarDefinitionsSchema} from "../../shared/types/types"
*/

const { joinInlineStatements } = require('../helpers/feStatementJoinHelper');
const { getDirectivesUsageStatement } = require('./directiveUsageStatements');

/**
* @typedef {object} CustomScalar
* @property {string} description - The description of the custom scalar.
* @property {boolean} isActivated - Indicates if the custom scalar is activated.
* @property {DirectivePropertyData[]} typeDirectives - The directives of the custom scalar.
*/

/**
* @typedef {Record<string, CustomScalar>} CustomScalars
*/

/**
* Maps a custom scalar to an FEStatement.
*
* @param {object} param0
* @param {string} param0.name - The name of the custom scalar.
* @param {CustomScalar} param0.customScalar - The custom scalar object.
* @param {FECustomScalarDefinition} param0.customScalar - The custom scalar object.
* @param {IdToNameMap} param0.definitionsIdToNameMap - The definitions id to name map.
* @returns {FEStatement}
*/
Expand All @@ -43,7 +32,7 @@ function mapCustomScalar({ name, customScalar, definitionsIdToNameMap }) {
* Gets the custom scalars as an array of FEStatements.
*
* @param {object} param0
* @param {CustomScalars} param0.customScalars - The custom scalars object.
* @param {FECustomScalarDefinitionsSchema} param0.customScalars - The custom scalars object.
* @param {IdToNameMap} param0.definitionsIdToNameMap - The definitions id to name map.
* @returns {FEStatement[]}
*/
Expand Down
10 changes: 5 additions & 5 deletions forward_engineering/mappers/directiveUsageStatements.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const { isUUID } = require('../helpers/isUUID');
* Gets the directives usage statement by mapping directives to strings and joining them.
*
* @param {object} params
* @param {DirectivePropertyData[]} params.directives - Array of directive definitions
* @param {DirectivePropertyData[]} [params.directives] - Array of directive definitions
* @param {IdToNameMap} [params.definitionsIdToNameMap] - The definitions id to name map.
* @returns {string} - The joined directive statements
*/
Expand All @@ -29,11 +29,11 @@ function getDirectivesUsageStatement({ directives = [], definitionsIdToNameMap =
* @returns {string} - The directive statement or empty string if invalid
*/
function mapDirective({ directive, definitionsIdToNameMap }) {
if (directive.directiveFormat === 'Raw') {
if ('rawDirective' in directive && directive.directiveFormat === 'Raw') {
return formatRawDirective({ rawDirective: directive.rawDirective });
}

if (directive.directiveFormat === 'Structured') {
if ('directiveName' in directive && directive.directiveFormat === 'Structured') {
const directiveName = getDirectiveName({ directiveName: directive.directiveName, definitionsIdToNameMap });
if (!directiveName) {
return '';
Expand All @@ -53,7 +53,7 @@ function mapDirective({ directive, definitionsIdToNameMap }) {
* @returns {string} - The formatted arguments string or empty string if no valid arguments
*/
function mapDirectiveRawArguments({ directive }) {
if (directive.argumentValueFormat === 'Raw') {
if ('argumentValueFormat' in directive && directive.argumentValueFormat === 'Raw') {
const argumentsValue = directive.rawArgumentValues?.replace(/\n/g, ' ').trim() || '';
if (!argumentsValue) {
return '';
Expand All @@ -75,7 +75,7 @@ function mapDirectiveRawArguments({ directive }) {
* @returns {string} - The formatted directive name or empty string if invalid
*/
function getDirectiveName({ directiveName, definitionsIdToNameMap }) {
const resolvedDirectiveName = (definitionsIdToNameMap[directiveName] || directiveName || '').trim();
const resolvedDirectiveName = (definitionsIdToNameMap?.[directiveName] || directiveName || '').trim();
if (typeof resolvedDirectiveName !== 'string' || resolvedDirectiveName === '' || isUUID(resolvedDirectiveName)) {
return '';
}
Expand Down
8 changes: 4 additions & 4 deletions forward_engineering/mappers/directives.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* @import {DirectiveDefinitions, Directive, FEStatement, DirectiveLocations, IdToNameMap} from "../../shared/types/types"
* @import {FEDirectiveDefinitionsSchema, FEDirectiveDefinition, FEStatement, DirectiveLocations, IdToNameMap} from "../../shared/types/types"
*/

const { DIRECTIVE_LOCATIONS } = require('../constants/feScriptConstants');
Expand Down Expand Up @@ -40,7 +40,7 @@ const getDirectiveName = name => (name.startsWith('@') ? name : `@${name}`);
*
* @param {object} args - The arguments object
* @param {string} args.name - The name of directive
* @param {Directive} args.directive - The directive object
* @param {FEDirectiveDefinition} args.directive - The directive object
* @param {IdToNameMap} args.definitionsIdToNameMap - The ID to name map of all available types in model - needs for
* arguments
* @returns {FEStatement}
Expand All @@ -65,8 +65,8 @@ function mapDirective({ name, directive, definitionsIdToNameMap }) {
* Maps directives to an FEStatement objects.
*
* @param {object} args - The arguments object
* @param {DirectiveDefinitions} args.definitionsIdToNameMap - The directives schema object
* @param {object} args.directives
* @param {IdToNameMap} args.definitionsIdToNameMap - The directives schema object
* @param {FEDirectiveDefinitionsSchema} args.directives
* @returns {FEStatement[]}
*/
function getDirectives({ definitionsIdToNameMap, directives = {} }) {
Expand Down
Loading