From 7c9caed7b0642d1f50ae3884c70e387f39728499 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Tue, 21 Apr 2026 11:58:49 +0200 Subject: [PATCH 1/2] docs: Add agent skills --- .agents/skills/create-autofix/SKILL.md | 391 +++++++++++++++++++++++++ .agents/skills/create-rule/SKILL.md | 279 ++++++++++++++++++ .claude/skills | 1 + AGENTS.md | 146 +++++++++ CLAUDE.md | 1 + 5 files changed, 818 insertions(+) create mode 100644 .agents/skills/create-autofix/SKILL.md create mode 100644 .agents/skills/create-rule/SKILL.md create mode 120000 .claude/skills create mode 100644 AGENTS.md create mode 100644 CLAUDE.md diff --git a/.agents/skills/create-autofix/SKILL.md b/.agents/skills/create-autofix/SKILL.md new file mode 100644 index 000000000..606d4be06 --- /dev/null +++ b/.agents/skills/create-autofix/SKILL.md @@ -0,0 +1,391 @@ +--- +name: create-autofix +description: Add a new autofix for a deprecated UI5 API. Autofixes are declarative fix definitions that map deprecated APIs to their replacements, registered in fix collection files under src/linter/ui5Types/fix/collections/. Use when adding migration support for a deprecated method, property, or import to a new or existing fix collection. +argument-hint: "[deprecated-api] [replacement-api]" +--- + +# Create a New UI5 Linter Autofix + +Add an autofix that migrates `$0` to `$1`. + +**Full context**: $ARGUMENTS + +## Architecture overview + +The autofix system has three layers: + +1. **Fix collections** (`src/linter/ui5Types/fix/collections/`) - Declarative trees mapping deprecated APIs to fix callbacks using `Ui5TypeInfoMatcher` +2. **FixFactory** (`src/linter/ui5Types/fix/FixFactory.ts`) - Loads collections, matches incoming deprecation findings to fixes, provides factory functions +3. **Fix classes** (`src/linter/ui5Types/fix/`) - Concrete implementations that generate source code changes + +## Step 1: Choose the right fix collection + +Existing collections in `src/linter/ui5Types/fix/collections/`: + +| File | Library | Used for | +|---|---|---| +| `sapUiCoreFixes.ts` | `sap.ui.core` | Core, Configuration, Theming, Lib, Element, Component, etc. | +| `jqueryFixes.ts` | `jquery` | jQuery.sap.* APIs (note: uses `namespace()` not `method()`) | +| `sapMFixes.ts` | `sap.m` | sap.m controls and utilities | +| `sapUiLayoutFixes.ts` | `sap.ui.layout` | sap.ui.layout controls | +| `sapUiCompFixes.ts` | `sap.ui.comp` | sap.ui.comp controls | +| `globalFixes.ts` | `global` | Global namespace access like `sap.ui.getCore()` | + +If the deprecated API belongs to an existing library, add to that collection. If it belongs to a new library, create a new collection file (see "Creating a new collection" below). + +## Step 2: Choose the right fix type + +There are 7 fix types, each created via a factory function exported from `FixFactory.ts`: + +### `accessExpressionFix` - Replace property/method access + +For simple replacements where `old.method` becomes `newModule.method`. + +```typescript +import {accessExpressionFix} from "../FixFactory.js"; +import {FixScope} from "../BaseFix.js"; + +// Configuration.getTheme() => Theming.getTheme() +t.method("getTheme", accessExpressionFix({ + moduleName: "sap/ui/core/Theming", // Module to import + scope: FixScope.FirstChild, // Replace up to first child in chain +})) + +// Configuration.getWhitelistService() => Security.getAllowlistService() +t.method("getWhitelistService", accessExpressionFix({ + moduleName: "sap/ui/security/Security", + propertyAccess: "getAllowlistService", // Rename the method too +})) +``` + +**Parameters:** +- `moduleName` - Module to import as replacement +- `scope` - How much of the expression chain to replace (see FixScope below) +- `propertyAccess` - New property/method name (if different from original) +- `preferredIdentifier` - Preferred variable name for the import +- `globalName` - Use a global variable instead of importing +- `obsoleteModuleName` - Mark an old module import as removable + +### `callExpressionFix` - Replace function calls + +For replacing method calls where arguments are preserved as-is. + +```typescript +import {callExpressionFix} from "../FixFactory.js"; + +// Configuration.setRTL(val) => Localization.setRTL(val) +t.method("setRTL", callExpressionFix({ + scope: FixScope.SecondChild, + moduleName: "sap/base/i18n/Localization", + mustNotUseReturnValue: true, // Only apply if return value is unused +})) + +// Configuration.setTheme(val) => Theming.setTheme(val) +t.method("setTheme", callExpressionFix({ + moduleName: "sap/ui/core/Theming", + propertyAccess: "setTheme", + mustNotUseReturnValue: true, +})) +``` + +**Parameters:** Same as `accessExpressionFix` plus: +- `mustNotUseReturnValue` - Only fix if the return value is not used (critical when return types differ) +- `newExpression` - Add `new` keyword to the replacement + +### `callExpressionGeneratorFix` - Replace calls with custom code generation + +For complex replacements where arguments need transformation or multiple imports are needed. + +```typescript +import {callExpressionGeneratorFix} from "../FixFactory.js"; + +// Simple: Core.isMobile() => Device.browser.mobile +t.method("isMobile", callExpressionGeneratorFix({ + moduleName: "sap/ui/Device", + generator: (ctx, [moduleIdentifier]) => { + return `${moduleIdentifier}.browser.mobile`; + }, +})) + +// Multiple imports: Core.getLocale() => new Locale(Localization.getLanguageTag()) +t.method("getLocale", callExpressionGeneratorFix({ + moduleImports: [ + {moduleName: "sap/ui/core/Locale"}, + {moduleName: "sap/base/i18n/Localization"}, + ], + generator(ctx, [localeId, localizationId]) { + return `new ${localeId}(${localizationId}.getLanguageTag())`; + }, +})) + +// With validation and shared context: +t.method("loadLibrary", callExpressionGeneratorFix<{json: string}>({ + moduleName: "sap/ui/core/Lib", + validateArguments: (ctx, fixHelper, arg1, arg2) => { + ctx.json = ""; + // Only migrate if async:true is set + if (arg2?.kind === SyntaxKind.ObjectLiteralExpression) { + let asyncOption = false; + ts.forEachChild(arg2, function (node: ts.Node) { + if (ts.isPropertyAssignment(node) && + ts.isIdentifier(node.name) && node.name.text === "async" && + node.initializer.kind === SyntaxKind.TrueKeyword) { + asyncOption = true; + } + }); + if (ts.isStringLiteralLike(arg1)) { + ctx.json = `{name: ${arg1.getFullText().trim()}}`; + } + return asyncOption; + } + return false; // Don't apply fix + }, + generator: (ctx, [moduleIdentifier]) => { + return `${moduleIdentifier}.load(${ctx.json})`; + }, +})) +``` + +**Parameters:** +- `moduleName` or `moduleImports` - Single or multiple modules to import +- `globalNames` - Global variables to use instead of imports +- `mustNotUseReturnValue` - Only fix if return value unused +- `validateArguments(ctx, fixHelpers, ...args)` - Return `false` to skip the fix. `ctx` is a custom object shared with `generator`. `fixHelpers` provides `checker` (TypeChecker), `manifestContent`, and `libraryDependencies`. `args` are the original call expression arguments as AST nodes. +- `generator(ctx, identifierNames, ...args)` - Return the replacement source code string. `identifierNames` are the resolved import identifiers. `args` are the original arguments as source code strings. Return `undefined` to skip. + +### `accessExpressionGeneratorFix` - Replace property access with custom code + +Like `callExpressionGeneratorFix` but for property access expressions (not calls). + +```typescript +import {accessExpressionGeneratorFix} from "../FixFactory.js"; + +t.namespace("os", accessExpressionGeneratorFix({ + moduleName: "sap/ui/Device", + generator: ([moduleIdentifier]) => { + return `${moduleIdentifier}.os.android && ${moduleIdentifier}.system.phone`; + }, +})) +``` + +### `propertyAssignmentFix` - Rename or remove object properties + +For fixing constructor parameters or settings objects. + +```typescript +import {propertyAssignmentFix} from "../FixFactory.js"; + +// Remove the "synchronizationMode" property from ODataModel constructor +t.constr([ + t.constuctorParameter("mParameters", [ + t.property("synchronizationMode", propertyAssignmentFix({})), // {} = delete + ]), +]) + +// Rename a property +t.property("oldName", propertyAssignmentFix({property: "newName"})) +``` + +**Parameters:** +- `property` - New property name. If omitted, the property is deleted. + +### `propertyAssignmentGeneratorFix` - Complex property transformations + +```typescript +import {propertyAssignmentGeneratorFix} from "../FixFactory.js"; + +t.property("oldProp", propertyAssignmentGeneratorFix({ + validatePropertyAssignment: (ctx, helpers, propAssignment) => { + // Return false to skip + return ts.isStringLiteralLike(propAssignment.initializer); + }, + generator: (ctx, propertyName, propertyInitializer) => { + return `newProp: transformValue(${propertyInitializer})`; + // Return "" to delete the property + }, +})) +``` + +### `obsoleteImportFix` - Remove unused imports after migration + +Always placed on `t.export()` to mark the module import as removable. + +```typescript +import {obsoleteImportFix} from "../FixFactory.js"; + +t.declareModule("sap/ui/core/Configuration", [ + t.class("Configuration", [ + // ... method fixes that migrate away from Configuration + ]), + t.export(obsoleteImportFix({ + moduleName: "sap/ui/core/Configuration", + })), +]); +``` + +## FixScope reference + +Controls how much of the expression chain gets replaced. + +For the expression `sap.ui.core.Core.getConfiguration().getTheme()`: + +| Scope | Replaces | +|---|---| +| `FixScope.FullExpression` (0) | Entire expression | +| `FixScope.FirstChild` (1) | `sap.ui.core.Core.getConfiguration().getTheme` (expression part of call) | +| `FixScope.SecondChild` (2) | `sap.ui.core.Core.getConfiguration()` | +| `FixScope.ThirdChild` (3) | `sap.ui.core.Core.getConfiguration` | +| `FixScope.FourthChild` (4) | `sap.ui.core.Core` | + +**Common patterns:** +- `FullExpression` - When replacing `sap.ui.getCore()` entirely with `Core` +- `FirstChild` - When replacing `Config.getTheme()` with `Theming.getTheme()` (same args) +- `SecondChild` - When replacing `Config.setRTL(val)` with `Localization.setRTL(val)` and the Config access needs to be replaced but the method name stays + +## Step 3: Add the fix declaration + +### Adding to an existing collection + +Open the appropriate collection file and add your declaration within the correct `declareModule` block (or add a new one): + +```typescript +// In the existing collection file: + +t.declareModule("sap/ui/core/SomeModule", [ + t.class("SomeClass", [ + // Your new fix: + t.method("deprecatedMethod", accessExpressionFix({ + moduleName: "sap/ui/core/NewModule", + scope: FixScope.FirstChild, + })), + ]), +]); +``` + +### Creating a new collection + +1. Create `src/linter/ui5Types/fix/collections/myLibFixes.ts`: + +```typescript +import Ui5TypeInfoMatcher from "../../Ui5TypeInfoMatcher.js"; +import {FixTypeInfoMatcher, accessExpressionFix, callExpressionFix} from "../FixFactory.js"; +import {FixScope} from "../BaseFix.js"; + +const t: FixTypeInfoMatcher = new Ui5TypeInfoMatcher("sap.my.lib"); +export default t; + +t.declareModule("sap/my/lib/SomeModule", [ + t.class("SomeClass", [ + t.method("deprecatedMethod", accessExpressionFix({ + moduleName: "sap/my/lib/NewModule", + scope: FixScope.FirstChild, + })), + ]), +]); +``` + +2. Register it in `src/linter/ui5Types/fix/FixFactory.ts`: + +```typescript +const AUTOFIX_COLLECTIONS = [ + "sapUiCoreFixes", + "jqueryFixes", + "sapMFixes", + "sapUiLayoutFixes", + "sapUiCompFixes", + "globalFixes", + "myLibFixes", // Add here +]; +``` + +The library name passed to `new Ui5TypeInfoMatcher("sap.my.lib")` must match the `library` field in the `Ui5TypeInfo` of the deprecated API for the matching to work. + +## Step 4: Matcher builder API reference + +The `Ui5TypeInfoMatcher` (`t`) provides these builder methods to declare the API tree: + +```typescript +t.declareModule(moduleName, children) // Top-level: declare a module +t.declareModules(moduleNames, children) // Multiple modules with same children +t.declareNamespace(namespace, children) // For global namespace (globalFixes) +t.class(name, children) // Class within a module +t.method(name, fixCallback) // Instance method +t.methods(names, fixCallback) // Multiple methods with same fix (returns array, use spread) +t.staticMethod(name, fixCallback) // Static method +t.staticMethods(names, fixCallback) // Multiple static methods +t.namespace(name, children | fixCallback) // Namespace/property (used heavily in jqueryFixes) +t.constr(children) // Constructor +t.constuctorParameter(name, children) // Constructor parameter (note: typo is intentional) +t.property(name, fixCallback) // Property in settings object +t.properties(names, fixCallback) // Multiple properties +t.export(fixCallback) // Module default export (for obsoleteImportFix) +t.function(name, fixCallback) // Standalone function +t.managedObjectSetting(name, fixCallback) // ManagedObject setting +t.metadataEvent(name, fixCallback) // Metadata event property +t.metadataProperty(name, fixCallback) // Metadata property +t.metadataAggregation(name, fixCallback) // Metadata aggregation +``` + +**jQuery.sap note:** Since jQuery.sap APIs are not fully typed, the jQuery fixes collection uses `t.namespace()` for everything instead of `t.method()`. This is documented in `jqueryFixes.ts`. + +## Step 5: Test the autofix + +Autofix tests use fixture files with snapshot comparison. + +**Add a fixture** at `test/fixtures/autofix/YourFixtureName.js`: + +```javascript +sap.ui.define(["sap/ui/core/SomeModule"], function(SomeModule) { + // Test the deprecated API usage + SomeModule.deprecatedMethod("arg1"); + + // Test edge cases + var result = SomeModule.deprecatedMethod("arg1"); // Return value used + SomeModule.deprecatedMethod(); // No arguments +}); +``` + +**Run the autofix tests:** + +```bash +npm run unit -- test/lib/autofix/autofix.projects.ts +``` + +**Update snapshots:** + +```bash +npm run unit-update-snapshots -- test/lib/autofix/autofix.projects.ts +``` + +**Also run e2e tests** which exercise the full autofix pipeline: + +```bash +npm run e2e +``` + +Snapshots are at: +- `test/lib/autofix/snapshots/autofix.projects.ts.md` (human-readable) +- `test/lib/autofix/snapshots/autofix.projects.ts.snap` (binary) + +## Autofix development checklist (from docs/Development.md) + +### For 1:1 replacements: +- [ ] Arguments have exactly the same type, order, value, and count +- [ ] Return type of the replacement matches exactly the original +- [ ] If return type is complex (enum/object): values/properties/methods match exactly + +### For complex replacements: +- [ ] If return type differs: only migrate when return value is not used (use `mustNotUseReturnValue: true`) +- [ ] Check the legacy API for argument type checks/assertions +- [ ] Statically verify argument types using TypeChecker if the new API is stricter +- [ ] Preserve comments around shuffled/merged arguments +- [ ] Maintain whitespace and line breaks + +### General: +- [ ] Fix declaration added to correct collection file +- [ ] Collection registered in `AUTOFIX_COLLECTIONS` (if new) +- [ ] Test fixtures cover positive cases (fixable) +- [ ] Test fixtures cover edge cases (not fixable / return value used / wrong arg types) +- [ ] Snapshots generated and reviewed +- [ ] `npm run unit` passes +- [ ] `npm run e2e` passes diff --git a/.agents/skills/create-rule/SKILL.md b/.agents/skills/create-rule/SKILL.md new file mode 100644 index 000000000..a6d3c366e --- /dev/null +++ b/.agents/skills/create-rule/SKILL.md @@ -0,0 +1,279 @@ +--- +name: create-rule +description: Create a new UI5 linter rule with custom AST-based detection. Use when adding a new lint rule that requires custom analysis logic beyond what TypeScript deprecation tags provide (e.g., checking specific API call patterns, validating constructor arguments, enforcing async flags, etc.). +argument-hint: "[rule-id] [short description]" +--- + +# Create a New UI5 Linter Rule + +Create a new custom linter rule named `$0` for the UI5 linter project. + +**Description**: $ARGUMENTS + +## Step-by-step process + +### Step 1: Define the message in `src/linter/messages.ts` + +There are three places to update in this file, all tightly coupled: + +**1a. Add the rule ID to the `RULES` object** (alphabetical order): + +```typescript +export const RULES = { + // ...existing rules... + "$0": "$0", + // ...existing rules... +} as const; +``` + +**1b. Add one or more `MESSAGE` enum entries** (alphabetical order): + +Each distinct finding type the rule can report needs its own enum entry. Use SCREAMING_SNAKE_CASE. + +```typescript +export enum MESSAGE { + // ...existing entries... + MY_NEW_MESSAGE, + // ...existing entries... +} +``` + +**1c. Add `MESSAGE_INFO` entries** for each message: + +```typescript +[MESSAGE.MY_NEW_MESSAGE]: { + severity: LintMessageSeverity.Error, // or Warning + ruleId: RULES["$0"], + + // The message function receives typed args and returns a user-facing string + message: ({paramName}: {paramName: string}) => + `Description of the issue involving '${paramName}'`, + + // The details function provides additional context (shown with --details flag) + // Use the same arg type. Can return undefined if no details are needed. + details: ({details}: {details?: string}) => + details ? `Additional context: ${details}` : undefined, +}, +``` + +**Important patterns for MESSAGE_INFO:** +- Both `message` and `details` receive the SAME args object (their types are merged via `MessageArgs`) +- Use `null` as args when calling `addMessage` if no args are needed (and define message/details with no parameters) +- The `details` field is optional in MESSAGE_INFO - omit it if there's nothing extra to show +- Severity: use `Error` for things that will break in UI5 2.x, `Warning` for recommendations + +### Step 2: Implement the detection logic + +For custom AST-based checks, there are two patterns: + +**Pattern A: Inline in SourceFileLinter** (for checks triggered during standard AST traversal) + +Add an analysis method to `src/linter/ui5Types/SourceFileLinter.ts`: + +```typescript +// Add the hook in visitNode() at the appropriate node type check: +visitNode(node: ts.Node) { + // ... existing checks ... + + // Example: Check class declarations that extend a specific UI5 class + if (ts.isClassDeclaration(node) && + this.isUi5ClassDeclaration(node, "sap/ui/some/BaseClass")) { + this.analyzeMyCustomRule(node); + } + + // ... existing traversal ... + ts.forEachChild(node, this.#boundVisitNode); +} + +// Add the analysis method: +analyzeMyCustomRule(node: ts.ClassDeclaration) { + const className = node.name?.text ?? ""; + + // Perform AST analysis... + // Use helpers from utils/utils.ts: + // findClassMember(node, "memberName", [{modifier: ts.SyntaxKind.StaticKeyword}]) + // getPropertyNameText(node.name) + // getPropertyAssignmentInObjectLiteralExpression("key", objectLiteral) + + // Report findings: + this.#reporter.addMessage(MESSAGE.MY_NEW_MESSAGE, { + paramName: className, + }, {node}); +} +``` + +**Pattern B: Separate module** (for complex checks with significant logic) + +Create a new file like `src/linter/ui5Types/myCustomCheck.ts`: + +```typescript +import ts from "typescript"; +import SourceFileReporter from "./SourceFileReporter.js"; +import {MESSAGE} from "../messages.js"; +import type {LinterContext} from "../LinterContext.js"; + +export default function analyzeMyCustomRule({ + node, + reporter, + checker, + context, +}: { + node: ts.ClassDeclaration; + reporter: SourceFileReporter; + checker: ts.TypeChecker; + context: LinterContext; +}) { + // Complex analysis logic here... + + reporter.addMessage(MESSAGE.MY_NEW_MESSAGE, { + paramName: "value", + }, {node}); +} +``` + +Then call it from `SourceFileLinter.visitNode()`: +```typescript +import analyzeMyCustomRule from "./myCustomCheck.js"; +// ... in visitNode(): +analyzeMyCustomRule({node, reporter: this.#reporter, checker: this.checker, context: this.typeLinter.getContext()}); +``` + +**For non-JS/TS file types**, implement the check in the appropriate linter: +- XML: `src/linter/xmlTemplate/linter.ts` +- manifest.json: `src/linter/manifestJson/` +- HTML: `src/linter/html/` +- YAML: `src/linter/yaml/` + +### Step 3: Create tests + +**3a. Create the test entry file** at `test/lib/linter/rules/$0.ts`: + +```typescript +import {runLintRulesTests} from "../_linterHelper.js"; + +runLintRulesTests(import.meta.url); +``` + +**3b. Create fixture directories** under `test/fixtures/linter/rules/$0/`: + +``` +test/fixtures/linter/rules/$0/ + Positive_1/ # Code that SHOULD trigger the rule + webapp/ + Component.js # (or whatever files are relevant) + manifest.json # (include if needed by the check) + Negative_1/ # Code that should NOT trigger the rule + webapp/ + Component.js + manifest.json +``` + +Naming conventions: +- `Positive_*` directories contain code with rule violations +- `Negative_*` directories contain valid code (no violations expected) +- Files starting with `_` are skipped +- Files starting with `only_` run in isolation (for debugging) +- Include a `manifest.json` with `"_version": "1.58.0"` and basic app descriptor if the linter needs project context + +**3c. Run tests to generate snapshots**: + +```bash +npm run unit -- test/lib/linter/rules/$0.ts +``` + +Then update snapshots: + +```bash +npm run unit-update-snapshots -- test/lib/linter/rules/$0.ts +``` + +Snapshots are generated at: +- `test/lib/linter/snapshots/rules/$0.ts.md` (human-readable) +- `test/lib/linter/snapshots/rules/$0.ts.snap` (binary) + +**3d. Verify the full test suite still passes**: + +```bash +npm run unit +``` + +### Step 4: Document the rule in `docs/Rules.md` + +Add a section (in alphabetical order among rules): + +```markdown +## $0 + +Brief description of what the rule checks and why it matters for UI5 2.x compatibility. + +**Details** +- What patterns are flagged +- What the correct alternative is + +**Related information** +- [Link to relevant UI5 documentation](https://ui5.sap.com/#/topic/xxxxx) +``` + +## Key utilities and patterns + +### AST node type guards +```typescript +ts.isClassDeclaration(node) +ts.isCallExpression(node) +ts.isPropertyAccessExpression(node) +ts.isObjectLiteralExpression(node) +ts.isStringLiteralLike(node) +ts.isNumericLiteral(node) +ts.isIdentifier(node) +ts.isArrayLiteralExpression(node) +ts.isPropertyAssignment(node) +``` + +### Common helper functions (from `src/linter/ui5Types/utils/utils.ts`) +```typescript +getPropertyNameText(name) // Safe property name extraction +findClassMember(classNode, name, modifiers) // Find member by name+modifiers +isClassMethod(member, checker) // Check if member is callable +getPropertyAssignmentInObjectLiteralExpression(key, obj) // Find property in object literal +getPropertyAssignmentsInObjectLiteralExpression(keys, obj) // Find multiple properties +getSymbolForPropertyInConstructSignatures(sigs, idx, name) // Find property in constructor args +extractNamespace(node) // Get full dotted namespace from access chain +isSourceFileOfUi5Type(sourceFile) // Check if source is from UI5 types +``` + +### Checking UI5 class hierarchy +```typescript +// In SourceFileLinter, use the built-in helper: +this.isUi5ClassDeclaration(node, "sap/ui/core/Control") +this.isUi5ClassDeclaration(node, ["sap/ui/core/mvc/View", "sap/ui/core/XMLComposite"]) +``` + +### Getting type information +```typescript +const type = this.checker.getTypeAtLocation(node); +const symbol = this.checker.getSymbolAtLocation(node); +const jsdocTags = symbol.getJsDocTags(this.checker); +``` + +### Reporting with node highlighting +Always pass the most specific node to `{node}` so the error highlights the right code: +```typescript +// Highlight the property assignment, not the whole object +this.#reporter.addMessage(MESSAGE.X, {arg: "val"}, {node: propertyAssignment}); + +// Highlight just the name, not the whole expression +this.#reporter.addMessage(MESSAGE.X, {arg: "val"}, {node: exprNode.name}); +``` + +## Checklist before finishing + +- [ ] Rule ID added to `RULES` object (alphabetical) +- [ ] MESSAGE enum entry added (alphabetical) +- [ ] MESSAGE_INFO entry with severity, ruleId, message, and optionally details +- [ ] Detection logic implemented with proper AST analysis +- [ ] Positive test fixtures (code that triggers the rule) +- [ ] Negative test fixtures (code that should NOT trigger the rule) +- [ ] Test file created and snapshots generated +- [ ] Rule documented in `docs/Rules.md` +- [ ] `npm run unit` passes +- [ ] `npm run lint` passes diff --git a/.claude/skills b/.claude/skills new file mode 120000 index 000000000..2b7a412b8 --- /dev/null +++ b/.claude/skills @@ -0,0 +1 @@ +../.agents/skills \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..dec3af675 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,146 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +UI5 Linter (`@ui5/linter`) is a static code analysis tool that detects UI5 2.x compatibility issues. It scans JS, TS, XML, JSON, YAML, and HTML files for deprecated APIs, global variable usage, CSP violations, and deprecated configurations. + +## Common Commands + +```bash +npm run build # Clean and compile TypeScript +npm run build-watch # Watch mode compilation +npm test # Full suite: lint + type-check + coverage + e2e + knip + license-check +npm run unit # Run all unit tests (AVA) +npm run unit -- test/lib/linter/someFile.ts # Run a single test file +npm run unit-watch # Watch mode for unit tests +npm run unit-update-snapshots # Update unit test snapshots +npm run e2e # Run end-to-end tests +npm run e2e:test-update-snapshots # Update e2e snapshots +npm run lint # ESLint check +npm run coverage # Tests with coverage enforcement +``` + +## Architecture + +### Linting Pipeline + +1. **Entry**: CLI (`src/cli.ts`) or Node API (`src/index.ts` exports `ui5lint()` and `UI5LinterEngine`) +2. **Orchestration**: `src/linter/linter.ts` creates a UI5 project graph, virtual filesystem, loads config (`ui5lint.config.js`), and calls `lintWorkspace()` +3. **Workspace**: `src/linter/lintWorkspace.ts` dispatches files to specialized linters in parallel +4. **Results**: Virtual paths mapped back to real file paths; results sorted deterministically + +### AMD to ESM Transpilation + +TypeScript lacks understanding of UI5's proprietary `sap.ui.define`/`sap.ui.require` APIs. The `amdTranspiler/` rewrites these to ECMAScript Module imports before TypeScript analysis. It also rewrites `Class.extend()` calls to ES6 class syntax with `export default`. Source maps are generated during transpilation to map findings back to original positions. + +A virtual filesystem combines: UI5 types from node_modules (`@sapui5/types`), static types from `/resources/types/`, and the project's source files — enabling the TypeScript compiler to resolve all references. + +### Specialized Linters + +Each file type has its own linter under `src/linter/`: + +- **`ui5Types/`** - JS/TS files. Uses the TypeScript compiler API with `@sapui5/types` definitions. `SourceFileLinter.ts` is the main detector (~70KB). AMD modules are transpiled to ESM via `amdTranspiler/` before analysis. +- **`xmlTemplate/`** - XML views/fragments. Transpiles XML to pseudo-JavaScript (controls become constructor calls with properties as arguments), then runs TypeScript analysis. Source maps map findings back to XML positions. Generates TS definitions for controller references. +- **`html/`** - Bootstrap HTML files (index.html). Checks bootstrap configuration parameters. +- **`manifestJson/`** - manifest.json. Schema validation, deprecated libraries, model types. +- **`yaml/`** - ui5.yaml config files. Deprecated framework versions. +- **`dotLibrary/`** - .library files. Framework version compatibility. +- **`fileTypes/`** - Detects deprecated view formats (.view.json, .view.html, etc.) +- **`binding/`** - Data binding expression linting. + +### Detection Rules + +Key rule IDs and what they detect: + +- **`no-deprecated-api`** — Deprecated classes, properties, events, methods, aggregations, associations, module imports, partially deprecated APIs (e.g. specific parameter combinations), deprecated view/fragment file types, manifest.json deprecated settings +- **`no-globals`** — Usage of UI5 modules via global namespace (e.g. `sap.m.Button` instead of importing `sap/m/Button`). Allowed globals: `sap.ui.define`, `sap.ui.require`, `sap.ui.require.toUrl`, `sap.ui.loader.config` +- **`no-pseudo-modules`** — Importing enums as modules instead of importing the library module they are declared in +- **`async-component-flags`** — Missing async flags in manifest.json routing configuration + +### Rules and Messages + +Rules are defined in `src/linter/messages.ts`: +- `RULES` object defines ~24 rule IDs (e.g. `no-deprecated-api`, `no-globals`, `async-component-flags`) +- `MESSAGE` enum defines 98+ message types +- `MESSAGE_INFO` maps each message to its rule ID and severity + +### Autofix System + +`src/autofix/` applies fixes iteratively (up to 11 passes for cascading changes). Fix implementations live in `src/linter/ui5Types/fix/` with a base `Fix` class and specialized subclasses. Source maps are used throughout to map transpiled positions back to original source. + +**Terminology**: Always use "Autofix" (not "Auto-Fix", "AutoFix", "autoFix", or "Auto Fix"). + +**Fix lifecycle** (5 steps per pass): +1. Lint source code and create preliminary fixes (collecting info from transpiled AST and TypeChecker) +2. Collect abstract modifications — ranges of anticipated changes, import requests, and unused dependencies +3. Collect module declarations, process import requests, resolve identifiers (e.g. `Button` or `Button2` if name conflict) +4. Generate concrete changes (`INSERT`, `REPLACE`, `DELETE` operations with absolute character positions) +5. Apply changes to the source string and write back to disk + +**Conflict resolution**: When two fixes touch the same code range, all but one are discarded. Fixes are not guaranteed to be applied in order. Each fix must produce consistent, syntactically correct source code independently. + +**Fix categories**: +- **Fixes** (`--fix`): Guaranteed correct, produce working code, applied automatically +- **Suggested Fixes** (future `--suggest-fixes`): More complex, may require manual review + +### Autofix Restrictions + +General limitations on what cannot be autofixed (see `docs/Scope-of-Autofix.md` for full details): +- **Code outside `sap.ui.define`/`sap.ui.require`** — fixes that require adding module imports cannot be applied +- **Sync to async API changes** — requires restructuring code flow, often affecting multiple files +- **Complex API replacements** — multiple API calls and new local variables needed +- **Context-dependent replacements** — behavior depends on broader usage context +- **Return value usage** — differing return types make automatic replacement impossible when return value is used + +### Directives + +Inline comments disable/enable rules per-line: +- JS/TS: `/* ui5lint-disable rule1, rule2 */` +- XML/HTML: `` +- YAML: `# ui5lint-disable rule1, rule2` + +Variants: `ui5lint-disable`, `ui5lint-enable`, `ui5lint-disable-line`, `ui5lint-disable-next-line`. A description can follow `--` (e.g. `// ui5lint-disable-line no-deprecated-api -- explanation`). + +**Implementation**: Directives are retrieved from the unmodified source file and collected on the LinterContext. After linting (possibly on transpiled code), directives are applied during final report generation to drop disabled findings. + +## Code Style + +- **Indentation**: Tabs +- **Quotes**: Double quotes +- **Semicolons**: Required +- **Max line length**: 120 characters +- **Object spacing**: No spaces in `{}` +- **Line breaks**: LF only + +## Git Conventions + +- **Conventional Commits** required. Valid types: `build`, `ci`, `deps`, `docs`, `feat`, `fix`, `perf`, `refactor`, `release`, `revert`, `style`, `test` +- Format: `type(scope): Sentence case description` +- **Rebase** instead of merge to update branches +- Body max line length: 160 characters + +## Testing + +- **Framework**: AVA with `tsx/esm` loader +- **Unit tests**: `test/lib/**/*.ts` (20s timeout) +- **E2E tests**: `test/e2e/**/*.ts` (60s timeout) +- **Fixtures**: `test/fixtures/` +- **Coverage thresholds**: 89% statements/lines, 82% branches, 95% functions +- Worker threads are disabled (`workerThreads: false`) + +## Key Resources + +- `/resources/api-extract.json` - UI5 API metadata (extracted from SAPUI5 SDK) +- `/resources/types/pseudo-modules/` - Additional module declarations for pseudo module detection +- `@sapui5/types` npm package - TypeScript definitions for all SAPUI5 libraries (public/protected API only; does not include `@ui5-restricted` or `@private` API) +- `docs/Scope-of-Autofix.md` - Lists APIs that cannot be replaced automatically and why +- `docs/Development.md` - Development guidelines and autofix implementation checklist + +## Autofix Development + +When implementing autofix solutions, follow the checklist in `docs/Development.md`: +- **1:1 replacements**: Arguments must have exactly the same type, order, value, and count. Return type must match exactly. +- **Complex replacements**: Only migrate when return value is unused if types differ. Statically verify argument types via TypeScript TypeChecker. Preserve comments, whitespace, and line breaks. +- Use `isExpectedValueExpression()` utility or `mustNotUseReturnValue` flag to guard against return-type mismatches. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..43c994c2d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md From 6ff9a7b36c94986061507441e303155309de21fa Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 29 Apr 2026 17:01:02 +0200 Subject: [PATCH 2/2] docs(SKILL): Fix create-rule test instructions --- .agents/skills/create-rule/SKILL.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.agents/skills/create-rule/SKILL.md b/.agents/skills/create-rule/SKILL.md index a6d3c366e..678fffbb9 100644 --- a/.agents/skills/create-rule/SKILL.md +++ b/.agents/skills/create-rule/SKILL.md @@ -149,9 +149,10 @@ analyzeMyCustomRule({node, reporter: this.#reporter, checker: this.checker, cont **3a. Create the test entry file** at `test/lib/linter/rules/$0.ts`: ```typescript +import {fileURLToPath} from "node:url"; import {runLintRulesTests} from "../_linterHelper.js"; -runLintRulesTests(import.meta.url); +runLintRulesTests(fileURLToPath(import.meta.url)); ``` **3b. Create fixture directories** under `test/fixtures/linter/rules/$0/`: