Skip to content

Commit 52f6549

Browse files
AlinaGovoruhinagithub-actions[bot]marufrasully
authored
Feat(eslint-fiori-plugin): cds annotations support (#4480)
* fix: get sapux root * fix: uncomment annotation code * test: mock cwd in unti tests * fix: lint * Linting auto fix commit * fix: default odata version * chore: remove performance log * feat: collect cds table sections * refactor: remove commented code * refactor: move get properties function * test: cds annotation provider unit * refactor: reduce complexity * refactor: remove unused function * test: cds cache unit test * test: add cap test project * test: cds parse app tests * test: add cap test data * refactor: unit test * refactor: revert test timeout * test: annotation provider with cds * test: add capp app latest cds * test: use latest cds app * test: increase timeout * test: update snapshots * fix: fiori language types * fix: ann file traversal logic * chore: add changeset * fix: added object fixer * test: cap helper * test: initial setup project * test: add cds ann unit tests * refactor: get property attribute * test: cds cache cases * test: get op sections properties * refactor: reuse duplicate function * refactor: add comment * test: check cds before install * test: fail test condition * test: check cds file * chore: changeset patch * test: check cds module path --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Maruf Rasully <[email protected]>
1 parent cebac6d commit 52f6549

41 files changed

Lines changed: 4374 additions & 459 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/funny-badgers-grab.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@sap-ux/eslint-plugin-fiori-tools': minor
3+
'@sap-ux/fiori-annotation-api': patch
4+
---
5+
6+
Add .cds annotations support to enable linting of CAP apps with the eslint-plugin-fiori-tools.

packages/eslint-plugin-fiori-tools/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { rules } from './rules';
88
import { FioriLanguage } from './language/fiori-language';
99
import { createSyncFn } from 'synckit';
1010
import type { getPathMappings } from '@sap-ux/project-access';
11-
import { uniformUrl } from './project-context/utils';
11+
import { uniformUrl } from '@sap-ux/fiori-annotation-api';
1212
export { DiagnosticCache } from './language/diagnostic-cache';
1313

1414
// Use CommonJS require for modules with resolution issues

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export class FioriAnnotationSourceCode extends TextSourceCodeBase {
7777
const child = node[key];
7878

7979
if (!child) {
80-
return;
80+
continue;
8181
}
8282
if (Array.isArray(child)) {
8383
for (const grandchild of child) {

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,9 @@ function handleInsert(
230230
if (isEmpty) {
231231
// Empty object - insert without trailing comma
232232
textToInsert = `\n${newContent}`;
233+
if (newContent.endsWith('}')) {
234+
textToInsert += '\n'.padEnd(node.loc.start.column);
235+
}
233236
} else {
234237
// Has existing properties - insert with trailing comma
235238
textToInsert = `\n${newContent},`;

packages/eslint-plugin-fiori-tools/src/project-context/artifacts.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type { WorkerResult } from './types';
1010
*/
1111
export async function getProjectArtifacts(filePath: string): Promise<WorkerResult> {
1212
try {
13-
const projectRoot = await findProjectRoot(filePath, false);
13+
const projectRoot = await findProjectRoot(filePath, true); // sapuxRequired for CAP apps to locate the project root
1414
const projectType = await getProjectType(projectRoot);
1515
const artifacts = await findFioriArtifacts({
1616
wsFolders: [projectRoot],

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

Lines changed: 55 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { AliasInformation, Element, MetadataElement } from '@sap-ux/odata-annotation-core';
1+
import type { AliasInformation, Element, ElementChild, MetadataElement } from '@sap-ux/odata-annotation-core';
22
import {
33
Edm,
44
elementsWithName,
@@ -161,15 +161,13 @@ function processReferenceFacetRecord(
161161
return undefined;
162162
}
163163

164-
const properties = getRecordPropertyValue(record);
165-
const id = properties['ID']?.value;
166-
const target = properties['Target'];
164+
const id = getId(record);
165+
const annotationPath = getTargetAnnotationPath(record);
167166

168-
if (!id || target?.kind !== Edm.AnnotationPath) {
167+
if (!id || !annotationPath) {
169168
return undefined;
170169
}
171170

172-
const annotationPath = target.value;
173171
if (annotationPath.startsWith('/')) {
174172
// absolute path is not supported
175173
return undefined;
@@ -350,45 +348,66 @@ export function getRecordType(aliasInfo: AliasInformation, element: Element): st
350348
}
351349
}
352350

353-
interface RecordProperty {
354-
name: string;
355-
value: string;
356-
kind: Edm.String | Edm.AnnotationPath;
357-
}
351+
const findContentByName = (content: ElementChild[], name: string): ElementChild | undefined =>
352+
content.find((c) => (c as Element).name === name);
353+
354+
const getElementText = (element: ElementChild): string | undefined =>
355+
(element as Element).content.find((c) => c.type === 'text')?.text;
358356

359357
/**
360-
* Extracts property values from a record element.
358+
* Returns AnnotationPath property value.
361359
*
362-
* @param record - The record element to extract properties from
360+
* @param record -The record element
361+
* @returns - Annotation path string
363362
*/
364-
function getRecordPropertyValue(record: Element): Record<string, RecordProperty> {
365-
const properties: Record<string, RecordProperty> = {};
366-
for (const child of record.content) {
367-
if (child.type !== ELEMENT_TYPE) {
368-
continue;
369-
}
370-
if (child.name === Edm.PropertyValue) {
363+
function getTargetAnnotationPath(record: Element): string | undefined {
364+
const target = record.content.find((child) => {
365+
if (child.type === ELEMENT_TYPE && child.name === Edm.PropertyValue) {
371366
const name = getElementAttributeValue(child, Edm.Property);
372-
const annotationPathAttribute = getElementAttribute(child, Edm.AnnotationPath);
373-
if (annotationPathAttribute) {
374-
properties[name] = {
375-
name,
376-
value: annotationPathAttribute.value,
377-
kind: Edm.AnnotationPath
378-
};
379-
continue;
367+
return name === 'Target';
368+
}
369+
return false;
370+
});
371+
if (target?.type === ELEMENT_TYPE) {
372+
const stringAttribute = getElementAttribute(target, Edm.AnnotationPath);
373+
if (stringAttribute) {
374+
return stringAttribute.value;
375+
} else {
376+
const annotationPathContent = findContentByName(target.content, Edm.AnnotationPath);
377+
if (annotationPathContent) {
378+
return getElementText(annotationPathContent);
380379
}
381-
const stringAttribute = getElementAttribute(child, Edm.String);
382-
if (stringAttribute) {
383-
properties[name] = {
384-
name,
385-
value: stringAttribute.value,
386-
kind: Edm.String
387-
};
380+
}
381+
}
382+
return undefined;
383+
}
384+
385+
/**
386+
* Returns ID property value.
387+
*
388+
* @param record - The record element
389+
* @returns - String ID value
390+
*/
391+
function getId(record: Element): string | undefined {
392+
const id = record.content.find((child) => {
393+
if (child.type === ELEMENT_TYPE && child.name === Edm.PropertyValue) {
394+
const name = getElementAttributeValue(child, Edm.Property);
395+
return name === 'ID';
396+
}
397+
return false;
398+
});
399+
if (id?.type === ELEMENT_TYPE) {
400+
const stringAttribute = getElementAttribute(id, Edm.String);
401+
if (stringAttribute) {
402+
return stringAttribute.value;
403+
} else {
404+
const idContent = findContentByName(id.content, Edm.String);
405+
if (idContent) {
406+
return getElementText(idContent);
388407
}
389408
}
390409
}
391-
return properties;
410+
return undefined;
392411
}
393412

394413
/**

packages/eslint-plugin-fiori-tools/src/project-context/parser/parser.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import { getMainService } from '@sap-ux/project-access';
77
import { CdsAnnotationProvider, getXmlServiceArtifacts, type ServiceArtifacts } from '@sap-ux/fiori-annotation-api';
88

99
import type { LocalFile, RemoteFileWithLocalServiceCache } from '../types';
10-
import { uniformUrl } from '../utils';
1110
import type { Diagnostic } from '../../language/diagnostics';
1211
import { buildServiceIndex } from './service';
1312
import type { ParsedProject, ParsedApp, ParsedManifest, FoundODataService, CustomViews, MinUI5Version } from './types';
13+
import { uniformUrl } from '@sap-ux/fiori-annotation-api';
1414

1515
export interface ParseResult {
1616
index: ParsedProject;
@@ -257,7 +257,7 @@ export class ApplicationParser {
257257
type: 'cap',
258258
name: dataSourceName,
259259
path: uniformUrl(dataSource.uri),
260-
version: dataSource.settings?.odataVersion ?? '2.0'
260+
version: dataSource.settings?.odataVersion ?? '4.0'
261261
});
262262
continue;
263263
}

packages/eslint-plugin-fiori-tools/src/project-context/utils.ts

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,5 @@
11
import type { ParsedApp, ParsedService } from './parser';
22

3-
/**
4-
* Normalizes a URL by replacing backslashes with forward slashes and removing leading slashes.
5-
*
6-
* @param url - The URL to normalize.
7-
* @returns The normalized URL.
8-
*/
9-
export function uniformUrl(url: string): string {
10-
return url
11-
.replaceAll('\\', '/')
12-
.replaceAll('//', '/')
13-
.replaceAll(/(?:^\/)/g, '');
14-
}
15-
163
/**
174
* Get parsed service by name from parsed application.
185
*

packages/eslint-plugin-fiori-tools/src/types.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import type { CustomRuleDefinitionType, CustomRuleTypeDefinitions, RuleVisitor } from '@eslint/core';
22
import type { AnyNode } from '@humanwhocodes/momoa';
33
import type { JSONLanguageOptions, JSONSourceCode } from '@eslint/json';
4-
import type { FioriJSONSourceCode } from './language/json/source-code';
5-
import type { FioriXMLSourceCode } from './language/xml/source-code';
64
import type { XMLToken, XMLAstNode } from '@xml-tools/ast';
75
import type { AnyNode as AnyAnnotationNode } from '@sap-ux/odata-annotation-core';
6+
import type { FioriLanguageOptions, FioriSourceCode } from './language/fiori-language';
87

98
/**
109
* Type definition for manifest.json specific ESLint rules.
@@ -32,8 +31,8 @@ export type ManifestRuleDefinition<Options extends Partial<CustomRuleTypeDefinit
3231
*/
3332
export type FioriRuleDefinition<Options extends Partial<CustomRuleTypeDefinitions> = object> = CustomRuleDefinitionType<
3433
{
35-
LangOptions: JSONLanguageOptions;
36-
Code: FioriJSONSourceCode | FioriXMLSourceCode;
34+
LangOptions: FioriLanguageOptions;
35+
Code: FioriSourceCode;
3736
Visitor: RuleVisitor;
3837
Node: AnyNode | XMLAstNode | XMLToken | AnyAnnotationNode;
3938
},
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

0 commit comments

Comments
 (0)