Skip to content

Commit e3c4018

Browse files
authored
feat:eslint plugin consistency rules (#3963)
* refactor: simplify json rules * fix: page name for fe v2 * feat: add page names to diagnostics * chore: remove commented code * chore: fix lint errors * chore: remove redundant code * test: add tests * chore: fix lint errors
1 parent 436894d commit e3c4018

20 files changed

+1323
-655
lines changed

packages/eslint-plugin-fiori-tools/src/language/diagnostics.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const CREATION_MODE_FOR_TABLE = 'sap-creation-mode-for-table';
88
export interface WidthIncludingColumnHeaderDiagnostic {
99
type: typeof WIDTH_INCLUDING_COLUMN_HEADER_RULE_TYPE;
1010
manifest: ManifestPropertyDiagnosticData;
11+
pageName: string;
1112
annotation: {
1213
file: string;
1314
annotationPath: string;
@@ -18,8 +19,7 @@ export interface WidthIncludingColumnHeaderDiagnostic {
1819
export interface ManifestPropertyDiagnosticData {
1920
uri: string;
2021
object: Manifest;
21-
requiredPropertyPath: string[];
22-
optionalPropertyPath: string[];
22+
propertyPath: string[];
2323
}
2424

2525
export interface FlexEnabled {
@@ -36,13 +36,15 @@ export type CreateModeMessageId =
3636
| 'suggestAppLevelV4';
3737
export interface CreationModeForTable {
3838
type: typeof CREATION_MODE_FOR_TABLE;
39+
pageName: string;
3940
manifest: ManifestPropertyDiagnosticData;
4041
messageId: CreateModeMessageId;
4142
tableType: string;
4243
}
4344

4445
export interface DisableCopyToClipboard {
4546
type: typeof DISABLE_COPY_TO_CLIPBOARD;
47+
pageName: string;
4648
manifest: ManifestPropertyDiagnosticData;
4749
}
4850

packages/eslint-plugin-fiori-tools/src/language/json/source-code.ts

Lines changed: 10 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -39,41 +39,25 @@ export class FioriJSONSourceCode extends JSONSourceCode {
3939
/**
4040
* Create a member string matcher from object path.
4141
*
42-
* This method generates an ESLint selector pattern to match JSON properties based on a path.
43-
* It supports both descendant and direct child matching strategies.
42+
* This method generates an ESQuery selector pattern to match JSON properties based on a path.
43+
* Enforces strict hierarchy validation with direct child selectors (>),
44+
* ensuring each property is directly nested within an Object.
4445
*
4546
* @param path - Array of property names representing the path to the target node.
4647
* Each element represents a nested level in the JSON structure.
47-
* @param options - Optional configuration object.
48-
* @param options.strict - When true, enforces strict hierarchy validation with direct child
49-
* selectors (>), ensuring each property is directly nested within an
50-
* Object. When false (default), uses descendant selectors ( ) that
51-
* allow matches at any nesting level.
5248
* @returns A selector string that matches the specified path through nested objects.
53-
*
5449
* @example
5550
* ```typescript
56-
* // Descendant matching (default):
57-
* createMatcherString(['sap.ui.generic.app', 'settings', 'createMode'])
58-
* // Returns: 'Member[name.value="sap.ui.generic.app"] Member[name.value="settings"] Member[name.value="createMode"]'
59-
*
60-
* // Strict matching:
6151
* createMatcherString(['sap.ui.generic.app', 'settings', 'createMode'], { strict: true })
6252
* // Returns: 'Member[name.value="sap.ui.generic.app"] > Object > Member[name.value="settings"] > Object > Member[name.value="createMode"]'
6353
* ```
6454
*/
65-
createMatcherString(path: string[], options?: { strict?: boolean }): string {
66-
const strict = options?.strict ?? false;
67-
68-
if (strict) {
69-
return path
70-
.map((segment, index) => {
71-
const isLast = index === path.length - 1;
72-
return isLast ? `Member[name.value="${segment}"]` : `Member[name.value="${segment}"] > Object`;
73-
})
74-
.join(' > ');
75-
}
76-
77-
return path.map((segment) => `Member[name.value="${segment}"]`).join(' ');
55+
createMatcherString(path: string[]): string {
56+
return path
57+
.map((segment, index) => {
58+
const isLast = index === path.length - 1;
59+
return isLast ? `Member[name.value="${segment}"]` : `Member[name.value="${segment}"] > Object`;
60+
})
61+
.join(' > ');
7862
}
7963
}

packages/eslint-plugin-fiori-tools/src/language/rule-factory.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@ import type { RuleContext, RulesMeta, RuleVisitor } from '@eslint/core';
22
import type { FioriLanguageOptions, FioriSourceCode, Node } from './fiori-language';
33
import { JSONSourceCode } from '@eslint/json';
44
import type { FioriJSONSourceCode } from './json/source-code';
5-
import type { AnyNode } from '@humanwhocodes/momoa';
5+
import type { AnyNode, MemberNode } from '@humanwhocodes/momoa';
66
import { FioriXMLSourceCode } from './xml/source-code';
77
import type { XMLAstNode, XMLToken } from '@xml-tools/ast';
88
import type { FioriRuleDefinition } from '../types';
99
import { FioriAnnotationSourceCode } from './annotations/source-code';
1010
import type { AnyNode as AnyAnnotationNode } from '@sap-ux/odata-annotation-core';
1111
import { DiagnosticCache } from './diagnostic-cache';
1212
import type { Diagnostic } from './diagnostics';
13+
import type { DeepestExistingPathResult } from '../utils/helpers';
14+
import { findDeepestExistingPath } from '../utils/helpers';
1315

1416
export type JSONRuleContext<MessageIds extends string, RuleOptions extends unknown[]> = RuleContext<{
1517
LangOptions: FioriLanguageOptions;
@@ -40,6 +42,7 @@ export type AnnotationRuleContext<MessageIds extends string, RuleOptions extends
4042
* @param param0 - Rule definition.
4143
* @param param0.ruleId
4244
* @param param0.meta
45+
* @param param0.createJsonVisitorHandler
4346
* @param param0.createJson
4447
* @param param0.createXml
4548
* @param param0.createAnnotations
@@ -55,6 +58,7 @@ export function createFioriRule<
5558
ruleId,
5659
meta,
5760
check,
61+
createJsonVisitorHandler,
5862
createJson,
5963
createXml,
6064
createAnnotations
@@ -65,6 +69,11 @@ export function createFioriRule<
6569
context: JSONRuleContext<MessageIds, RuleOptions>,
6670
validationResult: Extract<Diagnostic, { type: T }>[]
6771
) => RuleVisitor;
72+
createJsonVisitorHandler?: (
73+
context: JSONRuleContext<MessageIds, RuleOptions>,
74+
diagnostic: Extract<Diagnostic, { type: T }>,
75+
deepestPathResult: DeepestExistingPathResult
76+
) => (node: MemberNode) => void;
6877
createXml?: (
6978
context: XMLRuleContext<MessageIds, RuleOptions>,
7079
validationResult: Extract<Diagnostic, { type: T }>[]
@@ -99,6 +108,28 @@ export function createFioriRule<
99108
cachedDiagnostics = check(context);
100109
DiagnosticCache.addMessages(ruleId, cachedDiagnostics);
101110
}
111+
const sourceCode = context.sourceCode;
112+
if (sourceCode instanceof JSONSourceCode && createJsonVisitorHandler) {
113+
const applicableDiagnostics = cachedDiagnostics.filter(
114+
(diagnostic) => diagnostic.manifest.uri === sourceCode.uri
115+
);
116+
if (applicableDiagnostics.length === 0) {
117+
return {};
118+
}
119+
const matchers: RuleVisitor = {};
120+
for (const diagnostic of applicableDiagnostics) {
121+
const paths = findDeepestExistingPath(diagnostic.manifest.object, diagnostic.manifest.propertyPath);
122+
if (paths?.validatedPath && paths.validatedPath.length > 0) {
123+
matchers[sourceCode.createMatcherString(paths.validatedPath)] = createJsonVisitorHandler(
124+
// typescript can't infer context based on source code instance
125+
context as JSONRuleContext<MessageIds, RuleOptions>,
126+
diagnostic,
127+
paths
128+
);
129+
}
130+
}
131+
return matchers;
132+
}
102133
if (context.sourceCode instanceof JSONSourceCode && createJson) {
103134
return createJson(context as JSONRuleContext<MessageIds, RuleOptions>, cachedDiagnostics);
104135
}

packages/eslint-plugin-fiori-tools/src/project-context/linker/fe-v2.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ function linkPage(
190190
lookup: {}
191191
};
192192
page.configuration.createMode.valueInFile = createMode;
193-
linkListReportTable(page, path, table, target);
193+
linkListReportTable(page, [...path, name], table, target);
194194
linkedApp.pages.push(page);
195195
} else if (componentName === 'sap.suite.ui.generic.template.ObjectPage') {
196196
const entitySetName = target.entitySet;
@@ -228,12 +228,12 @@ function linkPage(
228228
};
229229
page.configuration.createMode.valueInFile = createMode;
230230

231-
linkObjectPageSections(page, path, entity, mainService, sections, target);
231+
linkObjectPageSections(page, [...path, name], entity, mainService, sections, target);
232232
linkedApp.pages.push(page);
233233
}
234234
const pages = target.pages ?? {};
235235
for (const [key, child] of Object.entries(pages)) {
236-
linkPage(context, service, linkedApp, [...path, 'pages'], key, child);
236+
linkPage(context, service, linkedApp, [...path, name, 'pages'], key, child);
237237
}
238238
}
239239

@@ -368,14 +368,14 @@ function linkObjectPageSections(
368368
controls[`${section.type}|${configurationKey}`] = linkedSection;
369369
let createMode: string | undefined;
370370
let tableType: string | undefined;
371-
let sectionEntityKey = '';
372-
for (const [key, value] of Object.entries(configuration.component?.settings?.sections ?? {})) {
371+
// let sectionEntityKey = '';
372+
for (const [_key, value] of Object.entries(configuration.component?.settings?.sections ?? {})) {
373373
if (value.createMode !== undefined) {
374-
sectionEntityKey = key;
374+
// sectionEntityKey = key;
375375
createMode = value.createMode;
376376
}
377377
if (value.tableSettings?.type !== undefined) {
378-
sectionEntityKey = key;
378+
// sectionEntityKey = key;
379379
tableType = value.tableSettings.type;
380380
}
381381
}
@@ -385,11 +385,26 @@ function linkObjectPageSections(
385385
configuration: {
386386
createMode: {
387387
values: createModeValues,
388-
configurationPath: [...pathToPage, 'component', 'settings', 'sections', 'createMode']
388+
configurationPath: [
389+
...pathToPage,
390+
'component',
391+
'settings',
392+
'sections',
393+
configurationKey,
394+
'createMode'
395+
]
389396
},
390397
tableType: {
391398
values: tableTypeValues,
392-
configurationPath: [...pathToPage, 'component', 'settings', 'sections', 'type']
399+
configurationPath: [
400+
...pathToPage,
401+
'component',
402+
'settings',
403+
'sections',
404+
configurationKey,
405+
'tableSettings',
406+
'type'
407+
]
393408
}
394409
},
395410
children: []

packages/eslint-plugin-fiori-tools/src/project-context/linker/fe-v4.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,16 @@ const tableTypeValues = ['ResponsiveTable', 'GridTable', 'AnalyticalTable', 'Tre
8787
/**
8888
*
8989
* @param configurationKey
90+
* @param pathToPage
9091
* @param table
9192
* @returns
9293
*/
93-
function createTable(configurationKey: string, table?: TableNode): Table | OrphanTable {
94+
function createTable(configurationKey: string, pathToPage: string[], table?: TableNode): Table | OrphanTable {
9495
const base: Omit<Table, 'type' | 'children'> = {
9596
configuration: {
9697
tableType: {
9798
configurationPath: [
99+
...pathToPage,
98100
'options',
99101
'settings',
100102
'controlConfiguration',
@@ -106,6 +108,7 @@ function createTable(configurationKey: string, table?: TableNode): Table | Orpha
106108
},
107109
widthIncludingColumnHeader: {
108110
configurationPath: [
111+
...pathToPage,
109112
'options',
110113
'settings',
111114
'controlConfiguration',
@@ -117,6 +120,7 @@ function createTable(configurationKey: string, table?: TableNode): Table | Orpha
117120
},
118121
disableCopyToClipboard: {
119122
configurationPath: [
123+
...pathToPage,
120124
'options',
121125
'settings',
122126
'controlConfiguration',
@@ -128,6 +132,7 @@ function createTable(configurationKey: string, table?: TableNode): Table | Orpha
128132
},
129133
creationMode: {
130134
configurationPath: [
135+
...pathToPage,
131136
'options',
132137
'settings',
133138
'controlConfiguration',
@@ -216,7 +221,7 @@ export function runFeV4Linker(context: LinkerContext): LinkedFeV4App {
216221
sections: [],
217222
lookup: {}
218223
};
219-
linkObjectPageSections(page, path, entity, mainService, sections, target);
224+
linkObjectPageSections(page, path, name, entity, mainService, sections, target);
220225
linkedApp.pages.push(page);
221226
}
222227
}
@@ -289,7 +294,7 @@ function linkListReport(
289294
tables: [],
290295
lookup: {}
291296
};
292-
linkListReportTable(page, path, tables, target);
297+
linkListReportTable(page, [...path, name], tables, target);
293298
linkedApp.pages.push(page);
294299
}
295300

@@ -310,7 +315,7 @@ function linkListReportTable(
310315

311316
for (const table of tables) {
312317
const configurationKey = table.annotationPath;
313-
const linkedTable = createTable(configurationKey, table);
318+
const linkedTable = createTable(configurationKey, pathToPage, table);
314319
controls[`${linkedTable.type}|${configurationKey}`] = linkedTable;
315320
}
316321

@@ -331,7 +336,7 @@ function linkListReportTable(
331336
}
332337
} else {
333338
// no annotation definition found for this table, but configuration exists
334-
const orphanedSection = createTable(controlKey);
339+
const orphanedSection = createTable(controlKey, pathToPage);
335340
controls[`${orphanedSection.type}|${controlKey}`] = orphanedSection;
336341
}
337342
}
@@ -345,6 +350,7 @@ function linkListReportTable(
345350
*
346351
* @param page
347352
* @param pathToPage
353+
* @param pageName
348354
* @param entity
349355
* @param service
350356
* @param sections
@@ -353,6 +359,7 @@ function linkListReportTable(
353359
function linkObjectPageSections(
354360
page: FeV4ObjectPage,
355361
pathToPage: string[],
362+
pageName: string,
356363
entity: MetadataElement,
357364
service: ParsedService,
358365
sections: SectionNode[],
@@ -374,7 +381,7 @@ function linkObjectPageSections(
374381
children: []
375382
};
376383
controls[`${section.type}|${configurationKey}`] = linkedSection;
377-
const linkedTable = createTable(configurationKey, table);
384+
const linkedTable = createTable(configurationKey, [...pathToPage, pageName], table);
378385
if (linkedTable.type === 'table') {
379386
linkedSection.children.push(linkedTable);
380387
controls[`${linkedTable.type}|${configurationKey}`] = linkedTable;

0 commit comments

Comments
 (0)