From 8c3625aeaf1b85da76088c61ed21e650cdba18d8 Mon Sep 17 00:00:00 2001 From: excaliborr Date: Wed, 6 Mar 2024 09:08:00 -0500 Subject: [PATCH] feat: finishing logic for remaining identifiers --- src/constants.ts | 8 + src/main.ts | 6 +- src/types.ts | 22 ++- src/utils.ts | 18 ++- src/validator.ts | 94 +++++++++--- test/utils.test.ts | 84 +++++++++++ test/utils/helpers.ts | 15 ++ test/validator.test.ts | 336 ++++++++++++++++++++++++++++++++++++++++- 8 files changed, 554 insertions(+), 29 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 2068af6..0c481a7 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -6,3 +6,11 @@ export const defaultFunctions: Functions = { public: { tags: { dev: false, notice: true, return: true, param: true } }, private: { tags: { dev: false, notice: true, return: true, param: true } }, } as const; + +export const defaultTags = { + tags: { + dev: false, + notice: true, + param: true, + }, +} as const; diff --git a/src/main.ts b/src/main.ts index 053bfa7..c7995a5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -8,7 +8,7 @@ import { getProjectCompiledSources, processConfig } from './utils'; import { Processor } from './processor'; import { Config } from './types'; import { Validator } from './validator'; -import { defaultFunctions } from './constants'; +import { defaultFunctions, defaultTags } from './constants'; /** * Main function that processes the sources and prints the warnings @@ -85,6 +85,10 @@ async function getConfig(configPath: string): Promise { .parseSync(); config.functions = defaultFunctions; + config.modifiers = defaultTags; + config.errors = defaultTags; + config.events = defaultTags; + config.structs = defaultTags; return config as Config; } diff --git a/src/types.ts b/src/types.ts index 2daf8e1..9556222 100644 --- a/src/types.ts +++ b/src/types.ts @@ -11,12 +11,20 @@ import { Static, Type } from '@sinclair/typebox'; // NOTE: For params like `return` if its set to true we will only force it if the function does return something +export const tagSchema = Type.Object({ + tags: Type.Object({ + dev: Type.Boolean({ default: false }), + notice: Type.Boolean({ default: true }), + param: Type.Boolean({ default: true }), + }), +}); + export const functionSchema = Type.Object({ tags: Type.Object({ dev: Type.Boolean({ default: false }), notice: Type.Boolean({ default: true }), - return: Type.Boolean({ default: true }), param: Type.Boolean({ default: true }), + return: Type.Boolean({ default: true }), }), }); @@ -35,13 +43,19 @@ export const configSchema = Type.Object({ exclude: Type.String({ default: '' }), root: Type.String({ default: './' }), functions: functionConfigSchema, + events: tagSchema, + errors: tagSchema, + modifiers: tagSchema, + structs: tagSchema, inheritdoc: Type.Boolean({ default: true }), constructorNatspec: Type.Boolean({ default: false }), }); +export type KeysForSupportedTags = 'events' | 'errors' | 'modifiers' | 'structs'; export type FunctionConfig = Static; export type Config = Static; export type Functions = Static; +export type Tags = Static; export interface NatspecDefinition { name?: string; @@ -81,3 +95,9 @@ export interface IWarning { location: string; messages: string[]; } + +export type HasVParameters = { + vParameters: { + vParameters: Array<{ name: string }>; + }; +}; diff --git a/src/utils.ts b/src/utils.ts index a83d060..377cd8a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,9 +1,9 @@ import fs from 'fs/promises'; import path from 'path'; import Ajv from 'ajv'; -import { Natspec, NatspecDefinition, NodeToProcess, Config, configSchema, Functions } from './types'; +import { Natspec, NatspecDefinition, NodeToProcess, Config, configSchema, Functions, KeysForSupportedTags } from './types'; import { ASTKind, ASTReader, SourceUnit, compileSol, FunctionDefinition } from 'solc-typed-ast'; -import { defaultFunctions } from './constants'; +import { defaultFunctions, defaultTags } from './constants'; /** * Returns the absolute paths of the Solidity files @@ -236,6 +236,10 @@ export async function processConfig(filePath: string): Promise { exclude: detectedConfig.exclude ?? '', root: detectedConfig.root ?? './', functions: detectedConfig.functions, + events: detectedConfig.events ?? defaultTags, + errors: detectedConfig.errors ?? defaultTags, + modifiers: detectedConfig.modifiers ?? defaultTags, + structs: detectedConfig.structs ?? defaultTags, inheritdoc: detectedConfig.inheritdoc ?? true, constructorNatspec: detectedConfig.constructorNatspec ?? false, }; @@ -251,3 +255,13 @@ export async function processConfig(filePath: string): Promise { return config; } + +/** + * Returns if the key being used is for a supported tag + * @dev A "supported tag" is a generalized term for events, errors, modifiers, and structs as they all share the same tag schema + * @param {string} key The key to check + * @returns {boolean} True if the key is for a supported tag + */ +export function isKeyForSupportedTags(key: string): key is KeysForSupportedTags { + return ['events', 'errors', 'modifiers', 'structs'].includes(key); +} diff --git a/src/validator.ts b/src/validator.ts index 0f95a7c..abe6b46 100644 --- a/src/validator.ts +++ b/src/validator.ts @@ -1,5 +1,15 @@ -import { Config, FunctionConfig, Functions, Natspec, NatspecDefinition, NodeToProcess } from './types'; -import { matchesFunctionKind, getElementFrequency } from './utils'; +import { + Config, + FunctionConfig, + Functions, + HasVParameters, + Natspec, + NatspecDefinition, + NodeToProcess, + KeysForSupportedTags, + Tags, +} from './types'; +import { matchesFunctionKind, getElementFrequency, isKeyForSupportedTags } from './utils'; import { EnumDefinition, ErrorDefinition, @@ -51,10 +61,9 @@ export class Validator { // Inheritdoc is not enforced nor present, and there is no other documentation, returning error if (!natspec.tags.length) { + let needsWarning = false; // If node is a function, check the user defined config if (node instanceof FunctionDefinition) { - let needsWarning = false; - Object.keys(this.config.functions).forEach((key) => { Object.keys(this.config.functions[key as keyof Functions].tags).forEach((tag) => { if (this.config.functions[key as keyof Functions][tag as keyof FunctionConfig]) { @@ -62,35 +71,75 @@ export class Validator { } }); }); - - if (needsWarning) return [`Natspec is missing`]; } else { - // TODO: Change this logic when we have more config options for events, structs, enums etc. - return [`Natspec is missing`]; + // The other config rules use the same datatype so we can check them here + Object.keys(this.config).forEach((key) => { + if (isKeyForSupportedTags(key)) { + const tagsConfig = this.config[key]?.tags; + if (tagsConfig) { + Object.values(tagsConfig).forEach((value) => { + if (value) { + needsWarning = true; + } + }); + } + } + }); } + + if (needsWarning) return [`Natspec is missing`]; } // Validate the completeness of the documentation let alerts: string[] = []; + let isDevTagForced: boolean; + let isNoticeTagForced: boolean; if (node instanceof EnumDefinition) { // TODO: Process enums } else if (node instanceof ErrorDefinition) { - alerts = [...alerts, ...this.validateParameters(node, natspecParams)]; + isDevTagForced = this.config.errors.tags.dev; + isNoticeTagForced = this.config.errors.tags.notice; + + alerts = [ + ...alerts, + ...this.validateParameters(node, natspecParams, 'errors'), + ...this.validateTags(isDevTagForced, isNoticeTagForced, natspec.tags), + ]; } else if (node instanceof EventDefinition) { - alerts = [...alerts, ...this.validateParameters(node, natspecParams)]; + isDevTagForced = this.config.events.tags.dev; + isNoticeTagForced = this.config.events.tags.notice; + + alerts = [ + ...alerts, + ...this.validateParameters(node, natspecParams, 'events'), + ...this.validateTags(isDevTagForced, isNoticeTagForced, natspec.tags), + ]; } else if (node instanceof FunctionDefinition) { const natspecReturns = natspec.returns.map((p) => p.name); + isDevTagForced = this.config.functions[node.visibility as keyof Functions]?.tags.dev; + isNoticeTagForced = this.config.functions[node.visibility as keyof Functions]?.tags.notice; + alerts = [ ...alerts, ...this.validateParameters(node, natspecParams), ...this.validateReturnParameters(node, natspecReturns), - ...this.validateTags(node, natspec.tags), + ...this.validateTags(isDevTagForced, isNoticeTagForced, natspec.tags), ]; } else if (node instanceof ModifierDefinition) { - alerts = [...alerts, ...this.validateParameters(node, natspecParams)]; + isDevTagForced = this.config.modifiers.tags.dev; + isNoticeTagForced = this.config.modifiers.tags.notice; + + alerts = [ + ...alerts, + ...this.validateParameters(node, natspecParams, 'modifiers'), + ...this.validateTags(isDevTagForced, isNoticeTagForced, natspec.tags), + ]; } else if (node instanceof StructDefinition) { - alerts = [...alerts, ...this.validateMembers(node, natspecParams)]; + isDevTagForced = this.config.structs.tags.dev; + isNoticeTagForced = this.config.structs.tags.notice; + + alerts = [...alerts, ...this.validateMembers(node, natspecParams), ...this.validateTags(isDevTagForced, isNoticeTagForced, natspec.tags)]; } else if (node instanceof VariableDeclaration) { // Only the presence of a notice is validated } @@ -105,7 +154,11 @@ export class Validator { * @param {string[]} natspecParams - The list of parameters from the natspec * @returns {string[]} - The list of alerts */ - private validateParameters(node: ErrorDefinition | FunctionDefinition | ModifierDefinition, natspecParams: (string | undefined)[]): string[] { + private validateParameters( + node: T, + natspecParams: (string | undefined)[], + key: KeysForSupportedTags | undefined = undefined + ): string[] { let definedParameters = node.vParameters.vParameters.map((p) => p.name); let alerts: string[] = []; const counter = getElementFrequency(natspecParams); @@ -114,6 +167,10 @@ export class Validator { if (!this.config.functions[node.visibility as keyof Functions]?.tags.param) { return []; } + } else if (key !== undefined) { + if (!this.config[key]?.tags.param) { + return []; + } } for (let paramName of definedParameters) { @@ -134,6 +191,10 @@ export class Validator { * @returns {string[]} - The list of alerts */ private validateMembers(node: StructDefinition, natspecParams: (string | undefined)[]): string[] { + if (!this.config.structs.tags.param) { + return []; + } + let members = node.vMembers.map((p) => p.name); let alerts: string[] = []; const counter = getElementFrequency(natspecParams); @@ -179,10 +240,7 @@ export class Validator { return alerts; } - private validateTags(node: FunctionDefinition, natspecTags: NatspecDefinition[]): string[] { - const isDevTagForced = this.config.functions[node.visibility as keyof Functions]?.tags.dev; - const isNoticeTagForced = this.config.functions[node.visibility as keyof Functions]?.tags.notice; - + private validateTags(isDevTagForced: boolean, isNoticeTagForced: boolean, natspecTags: NatspecDefinition[]): string[] { // If both are disabled no warnings should emit so we dont need to check anything if (!isDevTagForced && !isNoticeTagForced) { return []; diff --git a/test/utils.test.ts b/test/utils.test.ts index fb38e3a..a8d74b4 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -205,6 +205,34 @@ describe('Utils', () => { exclude: '', inheritdoc: true, functions: defaultFunctions, + modifiers: { + tags: { + dev: false, + notice: true, + param: true, + }, + }, + events: { + tags: { + dev: false, + notice: true, + param: true, + }, + }, + errors: { + tags: { + dev: false, + notice: true, + param: true, + }, + }, + structs: { + tags: { + dev: false, + notice: true, + param: true, + }, + }, constructorNatspec: false, }); }); @@ -236,6 +264,34 @@ describe('Utils', () => { exclude: './contracts/ignored.sol', inheritdoc: false, functions: defaultFunctions, + modifiers: { + tags: { + dev: false, + notice: true, + param: true, + }, + }, + events: { + tags: { + dev: false, + notice: true, + param: true, + }, + }, + errors: { + tags: { + dev: false, + notice: true, + param: true, + }, + }, + structs: { + tags: { + dev: false, + notice: true, + param: true, + }, + }, constructorNatspec: false, }); }); @@ -298,6 +354,34 @@ describe('Utils', () => { }, }, }, + modifiers: { + tags: { + dev: false, + notice: true, + param: true, + }, + }, + events: { + tags: { + dev: false, + notice: true, + param: true, + }, + }, + errors: { + tags: { + dev: false, + notice: true, + param: true, + }, + }, + structs: { + tags: { + dev: false, + notice: true, + param: true, + }, + }, constructorNatspec: true, }); }); diff --git a/test/utils/helpers.ts b/test/utils/helpers.ts index d7a627f..fd55cc1 100644 --- a/test/utils/helpers.ts +++ b/test/utils/helpers.ts @@ -21,3 +21,18 @@ export const defaultFunctions: Functions = { public: { tags: { dev: false, notice: true, return: true, param: true } }, private: { tags: { dev: false, notice: true, return: true, param: true } }, }; + +export const defaultTags = { tags: { dev: false, notice: true, param: true } }; + +export const defaultConfig = { + include: '', + exclude: '', + root: './', + functions: defaultFunctions, + events: defaultTags, + errors: defaultTags, + modifiers: defaultTags, + structs: defaultTags, + inheritdoc: false, + constructorNatspec: false, +}; diff --git a/test/validator.test.ts b/test/validator.test.ts index 5fd99b3..ff26703 100644 --- a/test/validator.test.ts +++ b/test/validator.test.ts @@ -1,4 +1,4 @@ -import { defaultFunctions } from './utils/helpers'; +import { defaultConfig, defaultFunctions, defaultTags } from './utils/helpers'; import { Validator } from '../src/validator'; import { getFileCompiledSource, expectWarning, findNode } from './utils/helpers'; import { mockConfig, mockNatspec } from './utils/mocks'; @@ -6,7 +6,7 @@ import { ContractDefinition } from 'solc-typed-ast'; describe('Validator', () => { let contract: ContractDefinition; - let validator: Validator = new Validator(mockConfig({ functions: defaultFunctions })); + let validator: Validator = new Validator(mockConfig(defaultConfig)); beforeAll(async () => { const compileResult = await getFileCompiledSource('test/contracts/BasicSample.sol'); @@ -421,8 +421,8 @@ describe('Validator', () => { }); }); - describe('function rules', () => { - it('should have no warnings if return is disabled', () => { + describe('config rules', () => { + it('should have no warnings if return is disabled for a function', () => { const mockFunctions = defaultFunctions; mockFunctions.external.tags.return = false; const noReturnValidator = new Validator(mockConfig({ functions: mockFunctions })); @@ -492,7 +492,7 @@ describe('Validator', () => { expect(result).toEqual([]); }); - it('should have a warning if notice is forced', () => { + it('should have a warning if notice is forced for a function', () => { const mockFunctions = defaultFunctions; mockFunctions.external.tags.notice = true; const noticeValidator = new Validator(mockConfig({ functions: mockFunctions })); @@ -517,7 +517,83 @@ describe('Validator', () => { expectWarning(result, `@notice is missing`, 1); }); - it('should have a warning if dev is forced', () => { + it('should have a warning if notice is forced for a modifier', () => { + const mockModifier = defaultTags; + mockModifier.tags.notice = true; + const noticeValidator = new Validator(mockConfig({ modifiers: mockModifier })); + const node = findNode(contract.vModifiers, 'transferFee'); + const natspec = mockNatspec({ + tags: [ + { + name: 'dev', + content: 'Modifier notice', + }, + ], + params: [], + }); + + const result = noticeValidator.validate(node, natspec); + expectWarning(result, `@notice is missing`, 1); + }); + + it('should have a warning if notice is forced for a struct', () => { + const mockStruct = defaultTags; + mockStruct.tags.notice = true; + const noticeValidator = new Validator(mockConfig({ structs: mockStruct })); + const node = findNode(contract.vStructs, 'TestStruct'); + const natspec = mockNatspec({ + tags: [ + { + name: 'dev', + content: 'Modifier notice', + }, + ], + params: [], + }); + + const result = noticeValidator.validate(node, natspec); + expectWarning(result, `@notice is missing`, 1); + }); + + it('should have a warning if notice is forced for an error', () => { + const mockError = defaultTags; + mockError.tags.notice = true; + const noticeValidator = new Validator(mockConfig({ errors: mockError })); + const node = findNode(contract.vErrors, 'BasicSample_SomeError'); + const natspec = mockNatspec({ + tags: [ + { + name: 'dev', + content: 'Modifier notice', + }, + ], + params: [], + }); + + const result = noticeValidator.validate(node, natspec); + expectWarning(result, `@notice is missing`, 1); + }); + + it('should have a warning if notice is forced for an event', () => { + const mockEvent = defaultTags; + mockEvent.tags.notice = true; + const noticeValidator = new Validator(mockConfig({ events: mockEvent })); + const node = findNode(contract.vEvents, 'BasicSample_BasicEvent'); + const natspec = mockNatspec({ + tags: [ + { + name: 'dev', + content: 'Modifier notice', + }, + ], + params: [], + }); + + const result = noticeValidator.validate(node, natspec); + expectWarning(result, `@notice is missing`, 1); + }); + + it('should have a warning if dev is forced for a function', () => { const mockFunctions = defaultFunctions; mockFunctions.external.tags.dev = true; const devValidator = new Validator(mockConfig({ functions: mockFunctions })); @@ -542,7 +618,83 @@ describe('Validator', () => { expectWarning(result, `@dev is missing`, 1); }); - it('should have a warning if notice is duplicated', () => { + it('should have a warning if dev is forced for a modifier', () => { + const mockModifier = defaultTags; + mockModifier.tags.dev = true; + const devValidator = new Validator(mockConfig({ modifiers: mockModifier })); + const node = findNode(contract.vModifiers, 'transferFee'); + const natspec = mockNatspec({ + tags: [ + { + name: 'notice', + content: 'Modifier notice', + }, + ], + params: [], + }); + + const result = devValidator.validate(node, natspec); + expectWarning(result, `@dev is missing`, 1); + }); + + it('should have a warning if dev is forced for a struct', () => { + const mockStruct = defaultTags; + mockStruct.tags.dev = true; + const devValidator = new Validator(mockConfig({ structs: mockStruct })); + const node = findNode(contract.vStructs, 'TestStruct'); + const natspec = mockNatspec({ + tags: [ + { + name: 'notice', + content: 'Modifier notice', + }, + ], + params: [], + }); + + const result = devValidator.validate(node, natspec); + expectWarning(result, `@dev is missing`, 1); + }); + + it('should have a warning if dev is forced for an error', () => { + const mockError = defaultTags; + mockError.tags.dev = true; + const devValidator = new Validator(mockConfig({ errors: mockError })); + const node = findNode(contract.vErrors, 'BasicSample_SomeError'); + const natspec = mockNatspec({ + tags: [ + { + name: 'notice', + content: 'Modifier notice', + }, + ], + params: [], + }); + + const result = devValidator.validate(node, natspec); + expectWarning(result, `@dev is missing`, 1); + }); + + it('should have a warning if dev is forced for an event', () => { + const mockEvent = defaultTags; + mockEvent.tags.dev = true; + const devValidator = new Validator(mockConfig({ events: mockEvent })); + const node = findNode(contract.vEvents, 'BasicSample_BasicEvent'); + const natspec = mockNatspec({ + tags: [ + { + name: 'notice', + content: 'Modifier notice', + }, + ], + params: [], + }); + + const result = devValidator.validate(node, natspec); + expectWarning(result, `@dev is missing`, 1); + }); + + it('should have a warning if notice is duplicated for a function', () => { const mockFunctions = defaultFunctions; mockFunctions.external.tags.notice = true; const noticeValidator = new Validator(mockConfig({ functions: mockFunctions })); @@ -571,6 +723,98 @@ describe('Validator', () => { expectWarning(result, `@notice is duplicated`, 1); }); + it('should have a warning if noticve is duplicated for a modifier', () => { + const mockModifier = defaultTags; + mockModifier.tags.notice = true; + const noticeValidator = new Validator(mockConfig({ modifiers: mockModifier })); + const node = findNode(contract.vModifiers, 'transferFee'); + const natspec = mockNatspec({ + tags: [ + { + name: 'notice', + content: 'Modifier notice', + }, + { + name: 'notice', + content: 'Modifier notice', + }, + ], + params: [], + }); + + const result = noticeValidator.validate(node, natspec); + expectWarning(result, `@notice is duplicated`, 1); + }); + + it('should have a warning if notice is duplicated for a struct', () => { + const mockStruct = defaultTags; + mockStruct.tags.notice = true; + const noticeValidator = new Validator(mockConfig({ structs: mockStruct })); + const node = findNode(contract.vStructs, 'TestStruct'); + const natspec = mockNatspec({ + tags: [ + { + name: 'notice', + content: 'Modifier notice', + }, + { + name: 'notice', + content: 'Modifier notice', + }, + ], + params: [], + }); + + const result = noticeValidator.validate(node, natspec); + expectWarning(result, `@notice is duplicated`, 1); + }); + + it('should have a warning if notice is duplicated for an error', () => { + const mockError = defaultTags; + mockError.tags.notice = true; + const noticeValidator = new Validator(mockConfig({ errors: mockError })); + const node = findNode(contract.vErrors, 'BasicSample_SomeError'); + const natspec = mockNatspec({ + tags: [ + { + name: 'notice', + content: 'Modifier notice', + }, + { + name: 'notice', + content: 'Modifier notice', + }, + ], + params: [], + }); + + const result = noticeValidator.validate(node, natspec); + expectWarning(result, `@notice is duplicated`, 1); + }); + + it('should have a warning if notice is duplicated for an event', () => { + const mockEvent = defaultTags; + mockEvent.tags.notice = true; + const noticeValidator = new Validator(mockConfig({ events: mockEvent })); + const node = findNode(contract.vEvents, 'BasicSample_BasicEvent'); + const natspec = mockNatspec({ + tags: [ + { + name: 'notice', + content: 'Modifier notice', + }, + { + name: 'notice', + content: 'Modifier notice', + }, + ], + params: [], + }); + + const result = noticeValidator.validate(node, natspec); + expectWarning(result, `@notice is duplicated`, 1); + }); + it('should have no warnings if everything is disabled for a function', () => { const mockFunctions = defaultFunctions; mockFunctions.external.tags.dev = false; @@ -588,5 +832,83 @@ describe('Validator', () => { const result = noNoticeValidator.validate(node, natspec); expect(result).toEqual([]); }); + + it('should have no warnings if everything is disabled for a modifier', () => { + const mockModifier = defaultTags; + mockModifier.tags.dev = false; + mockModifier.tags.notice = false; + mockModifier.tags.param = false; + const noNoticeValidator = new Validator(mockConfig({ modifiers: mockModifier })); + const node = findNode(contract.vModifiers, 'transferFee'); + const natspec = mockNatspec({ + tags: [], + params: [], + }); + + const result = noNoticeValidator.validate(node, natspec); + expect(result).toEqual([]); + }); + + it('should have no warnings if everything is disabled for a struct', () => { + const mockStruct = defaultTags; + mockStruct.tags.dev = false; + mockStruct.tags.notice = false; + mockStruct.tags.param = false; + const noNoticeValidator = new Validator(mockConfig({ structs: mockStruct })); + const node = findNode(contract.vStructs, 'TestStruct'); + const natspec = mockNatspec({ + tags: [], + params: [], + }); + + const result = noNoticeValidator.validate(node, natspec); + expect(result).toEqual([]); + }); + + it('should have no warnings if everything is disabled for an error', () => { + const mockError = defaultTags; + mockError.tags.dev = false; + mockError.tags.notice = false; + mockError.tags.param = false; + const noNoticeValidator = new Validator(mockConfig({ errors: mockError })); + const node = findNode(contract.vErrors, 'BasicSample_SomeError'); + const natspec = mockNatspec({ + tags: [], + params: [], + }); + + const result = noNoticeValidator.validate(node, natspec); + expect(result).toEqual([]); + }); + + it('should have no warnings if everything is disabled for an event', () => { + const mockEvent = defaultTags; + mockEvent.tags.dev = false; + mockEvent.tags.notice = false; + mockEvent.tags.param = false; + const noNoticeValidator = new Validator(mockConfig({ events: mockEvent })); + const node = findNode(contract.vEvents, 'BasicSample_BasicEvent'); + const natspec = mockNatspec({ + tags: [], + params: [], + }); + + const result = noNoticeValidator.validate(node, natspec); + expect(result).toEqual([]); + }); + + it('should have a warning if tags are empty and notice is forced for a modifier', () => { + const mockModifier = defaultTags; + mockModifier.tags.notice = true; + const noticeValidator = new Validator(mockConfig({ modifiers: mockModifier })); + const node = findNode(contract.vModifiers, 'transferFee'); + const natspec = mockNatspec({ + tags: [], + params: [], + }); + + const result = noticeValidator.validate(node, natspec); + expectWarning(result, `Natspec is missing`, 1); + }); }); });