diff --git a/README.md b/README.md index 03c42475a..746a58ae8 100644 --- a/README.md +++ b/README.md @@ -54,19 +54,19 @@ Run the `ui5lint [files...]` command in your project root folder UI5 linter report: /application/webapp/controller/App.controller.js - 10:4 error Call to deprecated function 'attachTap' of class 'Button' + 10:4 error Call to deprecated function 'attachTap' of class 'Button' no-deprecated-api /application/webapp/manifest.json - 81:17 error Use of deprecated model type 'sap.ui5/models/odata/type="sap.ui.model.odata.ODataModel"' + 81:17 error Use of deprecated model type 'sap.ui5/models/odata/type="sap.ui.model.odata.ODataModel"' no-deprecated-api /application/webapp/test/unit/unitTests.qunit.js - 6:1 error Call to deprecated function 'attachInit' of class 'Core' - 6:1 error Call to deprecated function 'getCore' (sap.ui.getCore) - 6:1 error Access of global variable 'sap' (sap.ui.getCore) + 6:1 error Call to deprecated function 'attachInit' of class 'Core' no-deprecated-api + 6:1 error Call to deprecated function 'getCore' (sap.ui.getCore) no-deprecated-api + 6:1 error Access of global variable 'sap' (sap.ui.getCore) no-globals /application/webapp/view/Main.view.xml - 16:39 error Import of deprecated module 'sap/m/MessagePage' - 22:5 error Use of deprecated property 'blocked' of class 'Button' + 16:39 error Import of deprecated module 'sap/m/MessagePage' no-deprecated-api + 22:5 error Use of deprecated property 'blocked' of class 'Button' no-deprecated-api 7 problems (7 errors, 0 warnings) @@ -85,16 +85,14 @@ You can provide multiple glob patterns as arguments after the `ui5lint` command UI5 linter report: /application/webapp/view/Main.view.xml - 16:39 error Import of deprecated module 'sap/m/MessagePage' - 22:5 error Use of deprecated property 'blocked' of class 'Button' + 16:39 error Import of deprecated module 'sap/m/MessagePage' no-deprecated-api + 22:5 error Use of deprecated property 'blocked' of class 'Button' no-deprecated-api 2 problems (2 errors, 0 warnings) Note: Use "ui5lint --details" to show more information about the findings ``` - - ### Options #### `--details` @@ -208,6 +206,31 @@ module.exports = { ]; ``` +## Directives + +UI5 linter supports directives similar to ESLint's configuration comments, allowing you to control linting rules in specific sections of your code. + +* **ui5lint-disable**: Disables all linting rules from the position of the comment +* **ui5lint-enable**: Re-enables linting rules that were disabled by ui5lint-disable +* **ui5lint-disable-line**: Disables all linting rules for the current line +* **ui5lint-disable-next-line**: Disables all linting rules for the next line + +### Specifying Rules + +You can disable specific rules by listing them after the directive. Rules must be separated by commas if several are given: + +* `/* ui5lint-disable no-deprecated-api */` +* `/* ui5lint-disable no-deprecated-api, no-deprecated-library */` +* `// ui5lint-disable-line no-deprecated-api` + +An explanation why a rule is disabled can be added after the rule name; it must be separated from the preceding text by two dashes: + +* `// ui5lint-disable-next-line no-deprecated-api -- explanation` + +### Scope + +Directives are currently supported in JavaScript and TypeScript files only; they are **not** supported in XML, YAML, HTML, or any other type of file. + ## Internals UI5 linter makes use of the [TypeScript compiler](https://github.com/microsoft/TypeScript/) to parse and analyze the source code (both JavaScript and TypesScript) of a UI5 project. This allows for a decent level of accuracy and performance. diff --git a/src/cli/base.ts b/src/cli/base.ts index d8aed5006..0d61069b2 100644 --- a/src/cli/base.ts +++ b/src/cli/base.ts @@ -1,5 +1,4 @@ import {Argv, ArgumentsCamelCase, CommandModule, MiddlewareFunction} from "yargs"; -import path from "node:path"; import {lintProject} from "../linter/linter.js"; import {Text} from "../formatter/text.js"; import {Json} from "../formatter/json.js"; @@ -148,10 +147,12 @@ async function handleLint(argv: ArgumentsCamelCase) { await profile.start(); } + const rootDir = process.cwd(); + const reportCoverage = !!(process.env.UI5LINT_COVERAGE_REPORT ?? coverage); const res = await lintProject({ - rootDir: path.join(process.cwd()), + rootDir, ignorePatterns, filePatterns, coverage: reportCoverage, @@ -174,7 +175,7 @@ async function handleLint(argv: ArgumentsCamelCase) { process.stdout.write(markdownFormatter.format(res, details)); process.stdout.write("\n"); } else if (format === "" || format === "stylish") { - const textFormatter = new Text(); + const textFormatter = new Text(rootDir); process.stderr.write(textFormatter.format(res, details)); } // Stop profiling after CLI finished execution diff --git a/src/formatter/text.ts b/src/formatter/text.ts index c760a05ba..6c961e7d7 100644 --- a/src/formatter/text.ts +++ b/src/formatter/text.ts @@ -34,9 +34,16 @@ function formatMessageDetails(msg: LintMessage, showDetails: boolean) { return `. ${detailsHeader} ${chalk.italic(msg.messageDetails.replace(/\s\s+|\n/g, " "))}`; } +function formatRule(ruleId: string) { + return chalk.dim(` ${ruleId}`); +} + export class Text { #buffer = ""; + constructor(private readonly cwd: string) { + } + format(lintResults: LintResult[], showDetails: boolean) { this.#writeln(`UI5 linter report:`); this.#writeln(""); @@ -51,7 +58,7 @@ export class Text { totalWarningCount += warningCount; totalFatalErrorCount += fatalErrorCount; - this.#writeln(chalk.inverse(path.resolve(process.cwd(), filePath))); + this.#writeln(chalk.inverse(path.resolve(this.cwd, filePath))); // Determine maximum line and column for position formatting let maxLine = 0; @@ -80,7 +87,8 @@ export class Text { `${formatSeverity(msg.severity)} ` + `${msg.fatal ? "Fatal error: " : ""}` + `${msg.message}` + - `${formatMessageDetails(msg, showDetails)}`); + `${formatMessageDetails(msg, showDetails)}` + + `${formatRule(msg.ruleId)}`); }); this.#writeln(""); diff --git a/src/linter/LinterContext.ts b/src/linter/LinterContext.ts index 09f053b1e..6baf1df9e 100644 --- a/src/linter/LinterContext.ts +++ b/src/linter/LinterContext.ts @@ -3,6 +3,7 @@ import {createReader} from "@ui5/fs/resourceFactory"; import {resolveLinks} from "../formatter/lib/resolveLinks.js"; import {LintMessageSeverity, MESSAGE, MESSAGE_INFO} from "./messages.js"; import {MessageArgs} from "./MessageArgs.js"; +import {Directive} from "./ui5Types/directives.js"; export type FilePattern = string; // glob patterns export type FilePath = string; // Platform-dependent path @@ -92,10 +93,8 @@ export interface PositionRange { } export interface LintMetadata { - // TODO: Use this to store information shared across linters, - // such as the async flag state in manifest.json which might be relevant - // when parsing the Component.js - _todo: string; + // The metadata holds information to be shared across linters + directives: Set; } export default class LinterContext { @@ -235,13 +234,100 @@ export default class LinterContext { return message; } + #getFilteredMessages(resourcePath: ResourcePath): RawLintMessage[] { + const rawMessages = this.#rawMessages.get(resourcePath); + if (!rawMessages) { + return []; + } + const metadata = this.#metadata.get(resourcePath); + if (!metadata?.directives?.size) { + return rawMessages; + } + + const filteredMessages: RawLintMessage[] = []; + const directives = new Set(metadata.directives); + // Sort messages by position + const sortedMessages = rawMessages.filter((rawMessage) => { + if (!rawMessage.position) { + filteredMessages.push(rawMessage); + return false; + } + return true; + }).sort((a, b) => { + const aPos = a.position!; + const bPos = b.position!; + return aPos.line === bPos.line ? aPos.column - bPos.column : aPos.line - bPos.line; + }); + + // Filter messages based on directives + let directiveStack: Directive[] = []; + for (const rawMessage of sortedMessages) { + const {position} = rawMessage; + const {line, column} = position!; // Undefined positions are already filtered out above + + directiveStack = directiveStack.filter((dir) => { + // Filter out line-based directives that are no longer relevant + if (dir.scope === "line" && dir.line !== line) { + return false; + } + if (dir.scope === "next-line" && dir.line !== line - 1) { + return false; + } + return true; + }); + + for (const dir of directives) { + if (dir.line > line) { + continue; + } + if (dir.scope !== "line" && dir.line === line && dir.column > column) { + continue; + } + directives.delete(dir); + if (dir.scope === "line" && dir.line !== line) { + continue; + } + if (dir.scope === "next-line" && dir.line !== line - 1) { + continue; + } + directiveStack.push(dir); + } + + if (!directiveStack.length) { + filteredMessages.push(rawMessage); + continue; + } + + const messageInfo = MESSAGE_INFO[rawMessage.id]; + if (!messageInfo) { + throw new Error(`Invalid message id '${rawMessage.id}'`); + } + + let disabled = false; + for (const dir of directiveStack) { + if (dir.action === "disable" && + (!dir.ruleNames.length || dir.ruleNames.includes(messageInfo.ruleId))) { + disabled = true; + } else if (dir.action === "enable" && + (!dir.ruleNames.length || dir.ruleNames.includes(messageInfo.ruleId))) { + disabled = false; + } + } + if (!disabled) { + filteredMessages.push(rawMessage); + } + } + return filteredMessages; + } + generateLintResult(resourcePath: ResourcePath): LintResult { - const rawMessages = this.#rawMessages.get(resourcePath) ?? []; const coverageInfo = this.#coverageInfo.get(resourcePath) ?? []; let errorCount = 0; let warningCount = 0; let fatalErrorCount = 0; + const rawMessages = this.#getFilteredMessages(resourcePath); + const messages: LintMessage[] = rawMessages.map((rawMessage) => { const message = this.#getMessageFromRawMessage(rawMessage); if (message.severity === LintMessageSeverity.Error) { diff --git a/src/linter/ui5Types/SourceFileLinter.ts b/src/linter/ui5Types/SourceFileLinter.ts index e17adf067..1367f7de6 100644 --- a/src/linter/ui5Types/SourceFileLinter.ts +++ b/src/linter/ui5Types/SourceFileLinter.ts @@ -11,6 +11,7 @@ import {taskStart} from "../../utils/perf.js"; import {getPositionsForNode} from "../../utils/nodePosition.js"; import {TraceMap} from "@jridgewell/trace-mapping"; import type {ApiExtract} from "../../utils/ApiExtract.js"; +import {findDirectives} from "./directives.js"; const log = getLogger("linter:ui5Types:SourceFileLinter"); @@ -80,6 +81,12 @@ export default class SourceFileLinter { // eslint-disable-next-line @typescript-eslint/require-await async lint() { try { + const metadata = this.context.getMetadata(this.resourcePath); + if (!metadata.directives) { + // Directives might have already been extracted by the amd transpiler + // This is done since the transpile process might loose comments + findDirectives(this.sourceFile, metadata); + } this.visitNode(this.sourceFile); this.#reporter.deduplicateMessages(); } catch (err) { diff --git a/src/linter/ui5Types/TypeLinter.ts b/src/linter/ui5Types/TypeLinter.ts index 902ff2264..bc1405c5a 100644 --- a/src/linter/ui5Types/TypeLinter.ts +++ b/src/linter/ui5Types/TypeLinter.ts @@ -92,7 +92,7 @@ export default class TypeChecker { } } - const host = await createVirtualCompilerHost(this.#compilerOptions, files, sourceMaps); + const host = await createVirtualCompilerHost(this.#compilerOptions, files, sourceMaps, this.#context); const createProgramDone = taskStart("ts.createProgram", undefined, true); const program = ts.createProgram( diff --git a/src/linter/ui5Types/amdTranspiler/transpiler.ts b/src/linter/ui5Types/amdTranspiler/transpiler.ts index aead940cb..0331161e7 100644 --- a/src/linter/ui5Types/amdTranspiler/transpiler.ts +++ b/src/linter/ui5Types/amdTranspiler/transpiler.ts @@ -1,7 +1,8 @@ import ts from "typescript"; +import path from "node:path/posix"; import {getLogger} from "@ui5/logger"; import {taskStart} from "../../../utils/perf.js"; -import {TranspileResult} from "../../LinterContext.js"; +import LinterContext, {TranspileResult} from "../../LinterContext.js"; import {createTransformer} from "./tsTransformer.js"; import {UnsupportedModuleError} from "./util.js"; @@ -49,9 +50,11 @@ function createProgram(inputFileNames: string[], host: ts.CompilerHost): ts.Prog return ts.createProgram(inputFileNames, compilerOptions, host); } -export default function transpileAmdToEsm(fileName: string, content: string, strict?: boolean): TranspileResult { +export default function transpileAmdToEsm( + resourcePath: string, content: string, context: LinterContext, strict?: boolean +): TranspileResult { // This is heavily inspired by the TypesScript "transpileModule" API - + const fileName = path.basename(resourcePath); const taskDone = taskStart("Transpiling AMD to ESM", fileName, true); const sourceFile = ts.createSourceFile( fileName, @@ -71,7 +74,7 @@ export default function transpileAmdToEsm(fileName: string, content: string, str const program = createProgram([fileName], compilerHost); const transformers: ts.CustomTransformers = { - before: [createTransformer(program)], + before: [createTransformer(program, resourcePath, context)], }; try { diff --git a/src/linter/ui5Types/amdTranspiler/tsTransformer.ts b/src/linter/ui5Types/amdTranspiler/tsTransformer.ts index 19dab9ed4..6473d1bc6 100644 --- a/src/linter/ui5Types/amdTranspiler/tsTransformer.ts +++ b/src/linter/ui5Types/amdTranspiler/tsTransformer.ts @@ -9,6 +9,8 @@ import replaceNodeInParent, {NodeReplacement} from "./replaceNodeInParent.js"; import {toPosStr, UnsupportedModuleError} from "./util.js"; import rewriteExtendCall, {UnsupportedExtendCall} from "./rewriteExtendCall.js"; import insertNodesInParent from "./insertNodesInParent.js"; +import LinterContext from "../../LinterContext.js"; +import {findDirectives} from "../directives.js"; const log = getLogger("linter:ui5Types:amdTranspiler:TsTransformer"); @@ -44,21 +46,23 @@ function isBlockLike(node: ts.Node): node is ts.BlockLike { * error is thrown. In that case, the rest of the module is still processed. However it's possible that the result * will be equal to the input. */ -export function createTransformer(program: ts.Program): ts.TransformerFactory { - return function transformer(context: ts.TransformationContext) { +export function createTransformer( + program: ts.Program, resourcePath: string, context: LinterContext +): ts.TransformerFactory { + return function transformer(tContext: ts.TransformationContext) { return (sourceFile: ts.SourceFile): ts.SourceFile => { - return transform(program, sourceFile, context); + return transform(program, sourceFile, tContext, resourcePath, context); }; }; } function transform( - program: ts.Program, sourceFile: ts.SourceFile, context: ts.TransformationContext + program: ts.Program, sourceFile: ts.SourceFile, tContext: ts.TransformationContext, resourcePath: string, + context: LinterContext ): ts.SourceFile { - const resourcePath = sourceFile.fileName; log.verbose(`Transforming ${resourcePath}`); const checker = program.getTypeChecker(); - const {factory: nodeFactory} = context; + const {factory: nodeFactory} = tContext; const moduleDefinitions: ModuleDefinition[] = []; // TODO: Filter duplicate imports, maybe group by module definition const requireImports: ts.ImportDeclaration[] = []; @@ -96,9 +100,12 @@ function transform( insertions.push(nodeToBeInserted); } + const metadata = context.getMetadata(resourcePath); + findDirectives(sourceFile, metadata); + // Visit the AST depth-first and collect module definitions function visit(nodeIn: ts.Node): ts.VisitResult { - const node = ts.visitEachChild(nodeIn, visit, context); + const node = ts.visitEachChild(nodeIn, visit, tContext); if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) { if (matchPropertyAccessExpression(node.expression, "sap.ui.define")) { @@ -346,7 +353,7 @@ function transform( node = replaceNodeInParent(node, replacement, nodeFactory); } } - return ts.visitEachChild(node, applyModifications, context); + return ts.visitEachChild(node, applyModifications, tContext); } processedSourceFile = ts.visitNode(processedSourceFile, applyModifications) as ts.SourceFile; diff --git a/src/linter/ui5Types/directives.ts b/src/linter/ui5Types/directives.ts new file mode 100644 index 000000000..c67d8d224 --- /dev/null +++ b/src/linter/ui5Types/directives.ts @@ -0,0 +1,133 @@ +import ts from "typescript"; +import {LintMetadata} from "../LinterContext.js"; + +export function findDirectives(sourceFile: ts.SourceFile, metadata: LintMetadata) { + metadata.directives = new Set(); + + const possibleDirectives = collectPossibleDirectives(sourceFile); + if (possibleDirectives.size === 0) { + return; + } + const sourceText = sourceFile.getFullText(); + + traverseAndFindDirectives(sourceFile, sourceText, possibleDirectives, metadata.directives); +} + +function traverseAndFindDirectives( + node: ts.Node, sourceText: string, possibleDirectives: Set, confirmedDirectives: Set +) { + findDirectivesAroundNode(node, sourceText, possibleDirectives, confirmedDirectives); + node.getChildren().forEach((child) => { + traverseAndFindDirectives(child, sourceText, possibleDirectives, confirmedDirectives); + }); +} + +function findDirectivesAroundNode( + node: ts.Node, sourceText: string, possibleDirectives: Set, confirmedDirectives: Set +) { + /* + // This is a comment + // ui5lint-disable + myCallExpression() + // ui5lint-enable + // This is a comment + */ + for (const directive of possibleDirectives) { + if (directive.pos >= node.getFullStart() && directive.pos + directive.length <= node.getStart()) { + const leadingComments = ts.getLeadingCommentRanges(sourceText, node.getFullStart()); + if (leadingComments?.length) { + leadingComments.some((comment) => { + if (comment.pos === directive.pos) { + possibleDirectives.delete(directive); + confirmedDirectives.add(directive); + return true; + } + return false; + }); + break; + } + } else if (directive.pos > node.getEnd()) { + const trailingComments = ts.getTrailingCommentRanges(sourceText, node.getEnd()); + if (trailingComments?.length) { + trailingComments.some((comment) => { + if (comment.pos === directive.pos && comment.end === directive.pos + directive.length) { + possibleDirectives.delete(directive); + confirmedDirectives.add(directive); + return true; + } + return false; + }); + break; + } + } + } +} + +/* Match things like: + // ui5lint-disable-next-line no-deprecated-api, no-global + // ui5lint-enable-next-line + // ui5lint-enable-line + // ui5lint-enable-line -- my description + /* ui5lint-enable-line -- my description *\/ + /* ui5lint-disable + no-deprecated-api, + no-global + *\/ + + Must not match things like: + ```` + // ui5lint-disable-next-line -- my description + expression(); + ```` + The above is a single line comment with a description followed by some code in the next line. + + The regex below is designed to match single- and multi-line comments, however it splits both + cases into basically two regex combined with an OR operator. + If we would try to match the above with a single regex instead (matching /* and // simultaneously), + it would be impossible to know whether the code in the second line is part of the directive's description or not. +*/ +/* eslint-disable max-len */ +const directiveRegex = +/* | ----------------------------------------------- Multi-line comments -------------------------------------------- | ------------------------------------------ Single-line comments ------------------------------------| */ + /\/\*\s*ui5lint-(enable|disable)(?:-((?:next-)?line))?(\s+(?:[\w-]+\s*,\s*)*(?:\s*[\w-]+))?\s*,?\s*(?:--[\s\S]*?)?\*\/|\/\/\s*ui5lint-(enable|disable)(?:-((?:next-)?line))?([ \t]+(?:[\w-]+[ \t]*,[ \t]*)*(?:[ \t]*[\w-]+))?[ \t]*,?[ \t]*(?:--.*)?$/mg; +/* |CG #1: action | | CG #2: scope | CG #3: rules |Dangling,| Description | |CG #4: action | | CG #5: scope | CG #6: rules |Dangling,| Description | */ +/* eslint-enable max-len */ + +export type DirectiveAction = "enable" | "disable"; +export type DirectiveScope = "line" | "next-line" | undefined; +export interface Directive { + action: DirectiveAction; + scope: DirectiveScope; + ruleNames: string[]; + pos: number; + length: number; + line: number; + column: number; +} + +export function collectPossibleDirectives(sourceFile: ts.SourceFile) { + const text = sourceFile.getFullText(); + let match; + const comments = new Set(); + while ((match = directiveRegex.exec(text)) !== null) { + const action = (match[1] ?? match[4]) as DirectiveAction; + const scope = (match[2] ?? match[5]) as DirectiveScope; + const rules = match[3] ?? match[6]; + + const pos = match.index; + const length = match[0].length; + let ruleNames = rules?.split(",") ?? []; + ruleNames = ruleNames.map((rule) => rule.trim()); + + const {line, character: column} = sourceFile.getLineAndCharacterOfPosition(pos + length); + comments.add({ + action, + scope, ruleNames, + pos, length, + // Typescript positions are all zero-based + line: line + 1, + column: column + 1, + }); + } + return comments; +} diff --git a/src/linter/ui5Types/host.ts b/src/linter/ui5Types/host.ts index 96cff16a9..ffeffeb75 100644 --- a/src/linter/ui5Types/host.ts +++ b/src/linter/ui5Types/host.ts @@ -4,7 +4,7 @@ import posixPath from "node:path/posix"; import fs from "node:fs/promises"; import {createRequire} from "node:module"; import transpileAmdToEsm from "./amdTranspiler/transpiler.js"; -import {ResourcePath} from "../LinterContext.js"; +import LinterContext, {ResourcePath} from "../LinterContext.js"; import {getLogger} from "@ui5/logger"; const log = getLogger("linter:ui5Types:host"); const require = createRequire(import.meta.url); @@ -65,7 +65,8 @@ export type FileContents = Map string)>; export async function createVirtualCompilerHost( options: ts.CompilerOptions, - files: FileContents, sourceMaps: FileContents + files: FileContents, sourceMaps: FileContents, + context: LinterContext ): Promise { const silly = log.isLevelEnabled("silly"); @@ -113,25 +114,25 @@ export async function createVirtualCompilerHost( } } - function getFile(fileName: string): string | undefined { + function getFile(resourcePath: string): string | undefined { // NOTE: This function should be kept in sync with "fileExists" - if (files.has(fileName)) { - let fileContent = files.get(fileName); + if (files.has(resourcePath)) { + let fileContent = files.get(resourcePath); if (typeof fileContent === "function") { fileContent = fileContent(); } - if (fileContent && fileName.endsWith(".js") && !sourceMaps.get(fileName)) { + if (fileContent && resourcePath.endsWith(".js") && !sourceMaps.get(resourcePath)) { // No source map indicates no transpilation was done yet - const res = transpileAmdToEsm(path.basename(fileName), fileContent); - files.set(fileName, res.source); - sourceMaps.set(fileName, res.map); + const res = transpileAmdToEsm(resourcePath, fileContent, context); + files.set(resourcePath, res.source); + sourceMaps.set(resourcePath, res.map); fileContent = res.source; } return fileContent; } - if (fileName.startsWith("/types/")) { - const fsPath = mapToTypePath(fileName); + if (resourcePath.startsWith("/types/")) { + const fsPath = mapToTypePath(resourcePath); if (fsPath) { return ts.sys.readFile(fsPath); } diff --git a/test/fixtures/linter/rules/Directives/Directives.js b/test/fixtures/linter/rules/Directives/Directives.js new file mode 100644 index 000000000..84d280a2f --- /dev/null +++ b/test/fixtures/linter/rules/Directives/Directives.js @@ -0,0 +1,120 @@ +sap.ui.define([ + // ui5lint-disable-next-line no-deprecated-api + "sap/m/Button", "sap/m/DateTimeInput", "sap/base/util/includes", "sap/ui/Device", "sap/ui/core/library", "sap/ui/generic/app/navigation/service/NavigationHandler", + // ui5lint-disable-next-line no-deprecated-api + "sap/ui/table/Table", "sap/ui/table/plugins/MultiSelectionPlugin", "sap/ui/core/Configuration", "sap/m/library" +], function(Button, DateTimeInput, includes, Device, coreLib, NavigationHandler, Table, MultiSelectionPlugin, Configuration, mobileLib) { + + var dateTimeInput = new DateTimeInput(); // IGNORE: Control is deprecated. A finding only appears for the module dependency, not for the usage. + + var btn = new Button({ + blocked: true, // ui5lint-disable-line no-deprecated-api -- IGNORE: Property "blocked" is deprecated + // ui5lint-disable-next-line no-deprecated-api + tap: () => console.log("Tapped") // IGNORE: Event "tap" is deprecated + }); + + /* ui5lint-disable */ + btn.attachTap(function() { // IGNORE: Method "attachTap" is deprecated + console.log("Tapped"); + }); + /* ui5lint-enable */ + + btn.attachTap(function() { // REPORT + console.log("Tapped"); + }); + + /* ui5lint-disable + no-deprecated-api, + no-deprecated-library, + no-globals, + */ + var table = new Table({ + plugins: [ // IGNORE: Aggregation "plugins" is deprecated + new MultiSelectionPlugin() + ], + groupBy: "some-column" // IGNORE: Association "groupBy" is deprecated + }); + /* ui5lint-enable no-deprecated-library */ + + includes([1], 1); // IGNORE: Function "includes" is deprecated + new sap.m.Button(); // IGNORE: Global usage + + const getIncludesFunction = () => includes; + getIncludesFunction()([1], 1); // IGNORE: Function "includes" is deprecated + /* ui5lint-enable */ + + includes([1], 1); // REPORT: Function "includes" is deprecated + new sap.m.Button(); // REPORT: Global usage + + /* ui5lint-disable-next-line */ + Configuration.getCompatibilityVersion("sapMDialogWithPadding"); // IGNORE: Method "getCompatibilityVersion" is deprecated + /* ui5lint-disable-next-line */ + Configuration["getCompatibilityVersion"]("sapMDialogWithPadding"); // IGNORE: Method "getCompatibilityVersion" is deprecated + + /* ui5lint-disable-next-line no-deprecated-api */ + Device.browser.webview; // IGNORE: "webview" is deprecated + // ui5lint-disable-next-line no-deprecated-api + Device.browser["webview"]; // IGNORE: "webview" is deprecated + + Device.browser["webview"]; // REPORT: "webview" is deprecated + + // ui5lint-disable-next-line no-deprecated-api + Configuration.AnimationMode; // IGNORE: Property "AnimationMode" (Enum) is deprecated + + // ui5lint-disable-next-line no-deprecated-api + coreLib.MessageType; // IGNORE: Enum "MessageType" is deprecated + + // ui5lint-disable-next-line no-deprecated-api + coreLib.MessageType; // ui5lint-enable-line -- REPORT: Enum "MessageType" is deprecated + + // ui5lint-disable-next-line no-deprecated-api -- Followed by an intentionally Empty line + + coreLib.MessageType; // REPORT: Enum "MessageType" is deprecated + + // ui5lint-disable-next-line no-deprecated-api + let {BarColor, MessageType} = coreLib; // IGNORE: Enum "MessageType" is deprecated + // ui5lint-disable-next-line no-deprecated-api + ({MessageType} = coreLib); // IGNORE: Enum "MessageType" is deprecated + MessageType.Error; + + // ui5lint-disable-next-line no-deprecated-api + let {BarColor: bt, MessageType: mt} = coreLib; // IGNORE: Enum "MessageType" is deprecated + // ui5lint-disable-next-line no-deprecated-api + ({BarColor, MessageType: mt} = coreLib); // IGNORE: Enum "MessageType" is deprecated + mt.Error; + + /* + ui5lint-disable-next-line no-deprecated-api + + -- + + Descriptive comment + */ + mobileLib.InputType.Date; // IGNORE: Enum value "InputType.Date" is deprecated + + const navigationHandler = new NavigationHandler({}); + // ui5lint-disable-next-line no-deprecated-api, no-deprecated-api + navigationHandler.storeInnerAppState({}); // IGNORE: Method "storeInnerAppState" is deprecated + + + // ui5lint-disable no-deprecated-api, no-globals + new sap.m.Button(); // IGNORE: Global variable "sap" + new Button({ + blocked: true, // IGNORE: Property "blocked" is deprecated + tap: () => console.log("Tapped") // IGNORE: Event "tap" is deprecated + }); + // ui5lint-enable + + new sap.m.Button(); // REPORT: Global variable "sap" + new Button({ + blocked: true, // REPORT: Property "blocked" is deprecated + tap: () => console.log("Tapped") // REPORT: Event "tap" is deprecated + }); + return; + // ui5lint-disable +}); + +new sap.m.Button({ // IGNORE: Global variable "sap" + blocked: true, // IGNORE: Property "blocked" is deprecated + tap: () => console.log("Tapped") // IGNORE: Event "tap" is deprecated +}); diff --git a/test/fixtures/linter/rules/Directives/Directives.ts b/test/fixtures/linter/rules/Directives/Directives.ts new file mode 100644 index 000000000..fa3ac8aea --- /dev/null +++ b/test/fixtures/linter/rules/Directives/Directives.ts @@ -0,0 +1,119 @@ +import Button from "sap/m/Button"; +// ui5lint-disable-next-line no-deprecated-api +import DateTimeInput from "sap/m/DateTimeInput"; +// ui5lint-disable-next-line no-deprecated-api +import includes from "sap/base/util/includes"; +import Device from "sap/ui/Device"; +import coreLib from "sap/ui/core/library"; +// ui5lint-disable-next-line no-deprecated-api +import NavigationHandler from "sap/ui/generic/app/navigation/service/NavigationHandler"; +import Table from "sap/ui/table/Table"; +import MultiSelectionPlugin from "sap/ui/table/plugins/MultiSelectionPlugin"; +// ui5lint-disable-next-line no-deprecated-api +import Configuration from "sap/ui/core/Configuration"; +import mobileLib from "sap/m/library"; + +const dateTimeInput = new DateTimeInput(); // IGNORE: Control is deprecated. A finding only appears for the module dependency, not for the usage. + +const btn = new Button({ + blocked: true, // ui5lint-disable-line no-deprecated-api -- IGNORE: Property "blocked" is deprecated + // ui5lint-disable-next-line no-deprecated-api + tap: () => console.log("Tapped") // IGNORE: Event "tap" is deprecated +}); + +/* ui5lint-disable */ +btn.attachTap(function() { // IGNORE: Method "attachTap" is deprecated + console.log("Tapped"); +}); +/* ui5lint-enable */ + +btn.attachTap(function() { // REPORT + console.log("Tapped"); +}); + +/* ui5lint-disable + no-deprecated-api, + no-deprecated-library, + no-globals, +*/ +const table = new Table({ + plugins: [ // IGNORE: Aggregation "plugins" is deprecated + new MultiSelectionPlugin() + ], + groupBy: "some-column" // IGNORE: Association "groupBy" is deprecated +}); +/* ui5lint-enable no-deprecated-library */ + +includes([1], 1); // IGNORE: Function "includes" is deprecated +new sap.m.Button(); // IGNORE: Global usage + +const getIncludesFunction = () => includes; +getIncludesFunction()([1], 1); // IGNORE: Function "includes" is deprecated +/* ui5lint-enable */ + +includes([1], 1); // REPORT: Function "includes" is deprecated +new sap.m.Button(); // REPORT: Global usage + +/* ui5lint-disable-next-line */ +Configuration.getCompatibilityVersion("sapMDialogWithPadding"); // IGNORE: Method "getCompatibilityVersion" is deprecated +/* ui5lint-disable-next-line */ +Configuration["getCompatibilityVersion"]("sapMDialogWithPadding"); // IGNORE: Method "getCompatibilityVersion" is deprecated + +/* ui5lint-disable-next-line no-deprecated-api */ +Device.browser.webview; // IGNORE: "webview" is deprecated +// ui5lint-disable-next-line no-deprecated-api +Device.browser["webview"]; // IGNORE: "webview" is deprecated + +Device.browser["webview"]; // REPORT: "webview" is deprecated + +// ui5lint-disable-next-line no-deprecated-api +Configuration.AnimationMode; // IGNORE: Property "AnimationMode" (Enum) is deprecated + +// ui5lint-disable-next-line no-deprecated-api +coreLib.MessageType; // IGNORE: Enum "MessageType" is deprecated + +// ui5lint-disable-next-line no-deprecated-api +coreLib.MessageType; // ui5lint-enable-line -- REPORT: Enum "MessageType" is deprecated + +// ui5lint-disable-next-line no-deprecated-api -- Followed by an intentionally Empty line + +coreLib.MessageType; // REPORT: Enum "MessageType" is deprecated + +// ui5lint-disable-next-line no-deprecated-api +let {BarColor, MessageType} = coreLib; // IGNORE: Enum "MessageType" is deprecated +// ui5lint-disable-next-line no-deprecated-api +({MessageType} = coreLib); // IGNORE: Enum "MessageType" is deprecated +MessageType.Error; + +// ui5lint-disable-next-line no-deprecated-api +let {BarColor: bt, MessageType: mt} = coreLib; // IGNORE: Enum "MessageType" is deprecated +// ui5lint-disable-next-line no-deprecated-api +({BarColor, MessageType: mt} = coreLib); // IGNORE: Enum "MessageType" is deprecated +mt.Error; + +/* + ui5lint-disable-next-line no-deprecated-api + + -- + + Descriptive comment +*/ +mobileLib.InputType.Date; // IGNORE: Enum value "InputType.Date" is deprecated + +const navigationHandler = new NavigationHandler({}); +// ui5lint-disable-next-line no-deprecated-api, no-deprecated-api +navigationHandler.storeInnerAppState({}); // IGNORE: Method "storeInnerAppState" is deprecated + +// ui5lint-disable no-deprecated-api, no-globals +new sap.m.Button(); // IGNORE: Global variable "sap" +new Button({ + blocked: true, // IGNORE: Property "blocked" is deprecated + tap: () => console.log("Tapped") // IGNORE: Event "tap" is deprecated +}); +// ui5lint-enable + +new sap.m.Button(); // REPORT: Global variable "sap" +new Button({ + blocked: true, // REPORT: Property "blocked" is deprecated + tap: () => console.log("Tapped") // REPORT: Event "tap" is deprecated +}); diff --git a/test/lib/formatter/json.ts b/test/lib/formatter/json.ts index c4d8f56f6..91cb654aa 100644 --- a/test/lib/formatter/json.ts +++ b/test/lib/formatter/json.ts @@ -18,7 +18,7 @@ test.beforeEach((t) => { messageDetails: "(since 1.118) - Please use {@link sap.ui.core.Core.ready Core.ready} instead.", }], coverageInfo: [], - errorCount: 0, + errorCount: 1, fatalErrorCount: 0, warningCount: 0, }]; diff --git a/test/lib/formatter/snapshots/text.ts.md b/test/lib/formatter/snapshots/text.ts.md new file mode 100644 index 000000000..73c22065a --- /dev/null +++ b/test/lib/formatter/snapshots/text.ts.md @@ -0,0 +1,31 @@ +# Snapshot report for `test/lib/formatter/text.ts` + +The actual snapshot is saved in `text.ts.snap`. + +Generated by [AVA](https://avajs.dev). + +## Test Text Formatter (with '--details true') + +> Snapshot 1 + + `UI5 linter report:␊ + ␊ + /Test.js␊ + 5:1 error Call to deprecated function 'attachInit' of class 'Core'. Details: (since 1.118) - Please use {@link sap.ui.core.Core.ready Core.ready} instead. no-deprecated-api␊ + ␊ + 1 problems (1 errors, 0 warnings)␊ + ` + +## Test Text Formatter (with '--details false') + +> Snapshot 1 + + `UI5 linter report:␊ + ␊ + /Test.js␊ + 5:1 error Call to deprecated function 'attachInit' of class 'Core' no-deprecated-api␊ + ␊ + 1 problems (1 errors, 0 warnings)␊ + ␊ + Note: Use "ui5lint --details" to show more information about the findings␊ + ` diff --git a/test/lib/formatter/snapshots/text.ts.snap b/test/lib/formatter/snapshots/text.ts.snap new file mode 100644 index 000000000..597d46a1e Binary files /dev/null and b/test/lib/formatter/snapshots/text.ts.snap differ diff --git a/test/lib/formatter/text.ts b/test/lib/formatter/text.ts new file mode 100644 index 000000000..d9b0b87df --- /dev/null +++ b/test/lib/formatter/text.ts @@ -0,0 +1,44 @@ +import path from "path"; +import anyTest, {TestFn} from "ava"; +import {Text} from "../../../src/formatter/text.js"; +import {LintResult} from "../../../src/linter/LinterContext.js"; + +const test = anyTest as TestFn<{ + lintResults: LintResult[]; + fakePath: string; +}>; + +test.beforeEach((t) => { + t.context.fakePath = path.join("/", "tmp", "test"); + t.context.lintResults = [{ + filePath: "Test.js", + messages: [{ + ruleId: "no-deprecated-api", + severity: 2, + line: 5, + column: 1, + message: "Call to deprecated function 'attachInit' of class 'Core'", + messageDetails: "(since 1.118) - Please use {@link sap.ui.core.Core.ready Core.ready} instead.", + }], + coverageInfo: [], + errorCount: 1, + fatalErrorCount: 0, + warningCount: 0, + }]; +}); + +test("Test Text Formatter (with '--details true')", (t) => { + const {lintResults, fakePath} = t.context; + const textFormatter = new Text(fakePath); + let res = textFormatter.format(lintResults, true); + res = res.replaceAll(path.resolve(fakePath) + path.sep, "/"); + t.snapshot(res); +}); + +test("Test Text Formatter (with '--details false')", (t) => { + const {lintResults, fakePath} = t.context; + const textFormatter = new Text(fakePath); + let res = textFormatter.format(lintResults, false); + res = res.replaceAll(path.resolve(fakePath) + path.sep, "/"); + t.snapshot(res); +}); diff --git a/test/lib/linter/LinterContext.ts b/test/lib/linter/LinterContext.ts new file mode 100644 index 000000000..ab5db50a2 --- /dev/null +++ b/test/lib/linter/LinterContext.ts @@ -0,0 +1,278 @@ +import anyTest, {TestFn} from "ava"; +import sinonGlobal from "sinon"; +import LinterContext from "../../../src/linter/LinterContext.js"; +import {MESSAGE} from "../../../src/linter/messages.js"; + +const test = anyTest as TestFn<{ + sinon: sinonGlobal.SinonSandbox; + linterContext: LinterContext; +}>; + +test.before((t) => { + t.context.sinon = sinonGlobal.createSandbox(); + + t.context.linterContext = new LinterContext({ + rootDir: "/", + namespace: "namespace", + }); + + // Propagate with findings in every 10th row up to 100 + for (let i = 1; i <= 10; i++) { + t.context.linterContext.addLintingMessage("/foo.js", MESSAGE.DEPRECATED_API_ACCESS, { + apiName: "foo", + details: "bar", + }, { + line: i * 10, + column: 10, + }); + } +}); + +test.after.always((t) => { + t.context.sinon.restore(); +}); + +// Directives are generally assumed to be provided *in order* (= sorted by position) + +test("generateLintResult: Disable and enable", (t) => { + const {linterContext} = t.context; + linterContext.getMetadata("/foo.js").directives = new Set([ + { // Ignore line 10 finding + action: "disable", + scope: undefined, + ruleNames: ["no-deprecated-api"], + pos: -1, // Ignored + length: -1, // Ignored + line: 5, + column: 1, + }, + { // Report line 20 finding + action: "enable", + scope: undefined, + ruleNames: ["no-deprecated-api"], + pos: -1, // Ignored + length: -1, // Ignored + line: 15, + column: 1, + }, + { // Ignore all other findings + action: "disable", + scope: undefined, + ruleNames: ["no-deprecated-api"], + pos: -1, // Ignored + length: -1, // Ignored + line: 25, + column: 1, + }, + ]); + + const res = linterContext.generateLintResult("/foo.js"); + t.snapshot(res); +}); + +test("generateLintResult: Disable next line", (t) => { + const {linterContext} = t.context; + linterContext.getMetadata("/foo.js").directives = new Set([ + { // Ignore line 10 finding + action: "disable", + scope: "next-line", + ruleNames: ["no-deprecated-api"], + pos: -1, // Ignored + length: -1, // Ignored + line: 9, + column: 1, + }, + { // No finding in next line => should have no effect + action: "disable", + scope: "next-line", + ruleNames: ["no-deprecated-api"], + pos: -1, // Ignored + length: -1, // Ignored + line: 15, + column: 1, + }, + { // No finding in next line => should have no effect + action: "disable", + scope: "next-line", + ruleNames: ["no-deprecated-api"], + pos: -1, // Ignored + length: -1, // Ignored + line: 28, + column: 1, + }, + { // No finding in next line => should have no effect + action: "disable", + scope: "next-line", + ruleNames: ["no-deprecated-api"], + pos: -1, // Ignored + length: -1, // Ignored + line: 31, + column: 1, + }, + { // No finding in next line (but in the same line) => should have no effect + action: "disable", + scope: "next-line", + ruleNames: ["no-deprecated-api"], + pos: -1, // Ignored + length: -1, // Ignored + line: 40, + column: 1, + }, + ]); + + const res = linterContext.generateLintResult("/foo.js"); + t.snapshot(res); +}); + +test("generateLintResult: Disable line", (t) => { + const {linterContext} = t.context; + linterContext.getMetadata("/foo.js").directives = new Set([ + { // Ignore line 10 finding + action: "disable", + scope: "line", + ruleNames: ["no-deprecated-api"], + pos: -1, // Ignored + length: -1, // Ignored + line: 10, + column: 1, // Before finding + }, + { // Ignore line 20 finding + action: "disable", + scope: "line", + ruleNames: ["no-deprecated-api"], + pos: -1, // Ignored + length: -1, // Ignored + line: 20, + column: 100, // After finding + }, + { // Ignore line 30 finding + action: "disable", + scope: "line", + ruleNames: ["no-deprecated-api"], + pos: -1, // Ignored + length: -1, // Ignored + line: 30, + column: 100, // After finding + }, + { // Ignore line 40 finding + action: "disable", + scope: "line", + ruleNames: ["no-deprecated-api"], + pos: -1, // Ignored + length: -1, // Ignored + line: 40, + column: 1, + }, + { // Actually report line 40 finding + action: "enable", + scope: "line", + ruleNames: ["no-deprecated-api"], + pos: -1, // Ignored + length: -1, // Ignored + line: 40, + column: 2, + }, + { // Disable next-line before 50 + action: "disable", + scope: "next-line", + ruleNames: ["no-deprecated-api"], + pos: -1, // Ignored + length: -1, // Ignored + line: 49, + column: 1, + }, + { // Enable in line 50 + action: "enable", + scope: "line", + ruleNames: [], + pos: -1, // Ignored + length: -1, // Ignored + line: 50, + column: 100, + }, + ]); + + const res = linterContext.generateLintResult("/foo.js"); + t.snapshot(res); +}); + +test("generateLintResult: Multiple disables in same line", (t) => { + const {linterContext} = t.context; + linterContext.getMetadata("/foo.js").directives = new Set([ + { // Ignore line 10 finding + action: "disable", + scope: undefined, + ruleNames: ["no-deprecated-api"], + pos: -1, // Ignored + length: -1, // Ignored + line: 10, + column: 1, // Before finding + }, + { // Actually report it + action: "enable", + scope: undefined, + ruleNames: ["no-deprecated-api"], + pos: -1, // Ignored + length: -1, // Ignored + line: 10, + column: 2, // Before finding + }, + { // Ignore everything after + action: "disable", + scope: undefined, + ruleNames: ["no-deprecated-api"], + pos: -1, // Ignored + length: -1, // Ignored + line: 10, + column: 11, // After finding + }, + ]); + + const res = linterContext.generateLintResult("/foo.js"); + t.snapshot(res); +}); + +test("generateLintResult: Edge positions", (t) => { + const {linterContext} = t.context; + linterContext.getMetadata("/foo.js").directives = new Set([ + { // Ignore line 10 finding + action: "disable", + scope: undefined, + ruleNames: ["no-deprecated-api"], + pos: -1, // Ignored + length: -1, // Ignored + line: 10, + column: 9, // Right before finding + }, + { // Ignore line 10 finding + action: "enable", + scope: undefined, + ruleNames: ["no-deprecated-api"], + pos: -1, // Ignored + length: -1, // Ignored + line: 10, + column: 11, // Right after finding + }, + { // Ignore line 20 finding + action: "disable", + scope: "line", + ruleNames: ["no-deprecated-api"], + pos: -1, // Ignored + length: -1, // Ignored + line: 20, + column: 9, // Right before finding + }, + { // Ignore line 30 finding + action: "disable", + scope: "line", + ruleNames: ["no-deprecated-api"], + pos: -1, // Ignored + length: -1, // Ignored + line: 30, + column: 11, // Right after finding + }, + ]); + + const res = linterContext.generateLintResult("/foo.js"); + t.snapshot(res); +}); diff --git a/test/lib/linter/amdTranspiler/_helper.ts b/test/lib/linter/amdTranspiler/_helper.ts index 65fae1cdc..48fa0c4bf 100644 --- a/test/lib/linter/amdTranspiler/_helper.ts +++ b/test/lib/linter/amdTranspiler/_helper.ts @@ -5,6 +5,7 @@ import util from "util"; import {readdirSync} from "node:fs"; import fs from "node:fs/promises"; import transpileAmdToEsm from "../../../../src/linter/ui5Types/amdTranspiler/transpiler.js"; +import LinterContext from "../../../../src/linter/LinterContext.js"; util.inspect.defaultOptions.depth = 4; // Increase AVA's printing depth since coverageInfo objects are on level 4 @@ -41,7 +42,11 @@ export function createTestsForFixtures(fixturesPath: string) { defineTest(`Transpile ${testName}`, async (t) => { const filePath = path.join(fixturesPath, fileName); const fileContent = await fs.readFile(filePath); - const {source, map} = transpileAmdToEsm(testName, fileContent.toString(), true); + const context = new LinterContext({ + rootDir: "/", + namespace: "namespace", + }); + const {source, map} = transpileAmdToEsm(testName, fileContent.toString(), context); t.snapshot(source); t.snapshot(map && JSON.parse(map)); }); diff --git a/test/lib/linter/rules/Directives.ts b/test/lib/linter/rules/Directives.ts new file mode 100644 index 000000000..103ec0308 --- /dev/null +++ b/test/lib/linter/rules/Directives.ts @@ -0,0 +1,5 @@ +import {fileURLToPath} from "node:url"; +import {runLintRulesTests} from "../_linterHelper.js"; + +const filePath = fileURLToPath(import.meta.url); +runLintRulesTests(filePath); diff --git a/test/lib/linter/rules/snapshots/Directives.ts.md b/test/lib/linter/rules/snapshots/Directives.ts.md new file mode 100644 index 000000000..60652c68a --- /dev/null +++ b/test/lib/linter/rules/snapshots/Directives.ts.md @@ -0,0 +1,181 @@ +# Snapshot report for `test/lib/linter/rules/Directives.ts` + +The actual snapshot is saved in `Directives.ts.snap`. + +Generated by [AVA](https://avajs.dev). + +## General: Directives.js + +> Snapshot 1 + + [ + { + coverageInfo: [], + errorCount: 9, + fatalErrorCount: 0, + filePath: 'Directives.js', + messages: [ + { + column: 6, + line: 22, + message: 'Call to deprecated function \'attachTap\' of class \'Button\'', + messageDetails: 'Deprecated test message', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 2, + line: 46, + message: 'Call to deprecated function \'includes\'', + messageDetails: 'Deprecated test message', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 6, + line: 47, + message: 'Access of global variable \'sap\' (sap.m.Button)', + messageDetails: 'Do not use global variables to access UI5 modules or APIs. See Best Practices for Developers (https://ui5.sap.com/#/topic/28fcd55b04654977b63dacbee0552712)', + ruleId: 'no-globals', + severity: 2, + }, + { + column: 2, + line: 59, + message: 'Use of deprecated property \'webview\'', + messageDetails: 'Deprecated test message', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 2, + line: 68, + message: 'Use of deprecated property \'MessageType\'', + messageDetails: 'Deprecated test message', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 2, + line: 72, + message: 'Use of deprecated property \'MessageType\'', + messageDetails: 'Deprecated test message', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 6, + line: 108, + message: 'Access of global variable \'sap\' (sap.m.Button)', + messageDetails: 'Do not use global variables to access UI5 modules or APIs. See Best Practices for Developers (https://ui5.sap.com/#/topic/28fcd55b04654977b63dacbee0552712)', + ruleId: 'no-globals', + severity: 2, + }, + { + column: 3, + line: 110, + message: 'Use of deprecated property \'blocked\' of class \'Button\'', + messageDetails: 'Deprecated test message', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 3, + line: 111, + message: 'Use of deprecated property \'tap\' of class \'Button\'', + messageDetails: 'Deprecated test message', + ruleId: 'no-deprecated-api', + severity: 2, + }, + ], + warningCount: 0, + }, + ] + +## General: Directives.ts + +> Snapshot 1 + + [ + { + coverageInfo: [], + errorCount: 9, + fatalErrorCount: 0, + filePath: 'Directives.ts', + messages: [ + { + column: 5, + line: 30, + message: 'Call to deprecated function \'attachTap\' of class \'Button\'', + messageDetails: 'Deprecated test message', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 1, + line: 54, + message: 'Call to deprecated function \'includes\'', + messageDetails: 'Deprecated test message', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 5, + line: 55, + message: 'Access of global variable \'sap\' (sap.m.Button)', + messageDetails: 'Do not use global variables to access UI5 modules or APIs. See Best Practices for Developers (https://ui5.sap.com/#/topic/28fcd55b04654977b63dacbee0552712)', + ruleId: 'no-globals', + severity: 2, + }, + { + column: 1, + line: 67, + message: 'Use of deprecated property \'webview\'', + messageDetails: 'Deprecated test message', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 1, + line: 76, + message: 'Use of deprecated property \'MessageType\'', + messageDetails: 'Deprecated test message', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 1, + line: 80, + message: 'Use of deprecated property \'MessageType\'', + messageDetails: 'Deprecated test message', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 5, + line: 115, + message: 'Access of global variable \'sap\' (sap.m.Button)', + messageDetails: 'Do not use global variables to access UI5 modules or APIs. See Best Practices for Developers (https://ui5.sap.com/#/topic/28fcd55b04654977b63dacbee0552712)', + ruleId: 'no-globals', + severity: 2, + }, + { + column: 5, + line: 117, + message: 'Use of deprecated property \'blocked\' of class \'Button\'', + messageDetails: 'Deprecated test message', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 5, + line: 118, + message: 'Use of deprecated property \'tap\' of class \'Button\'', + messageDetails: 'Deprecated test message', + ruleId: 'no-deprecated-api', + severity: 2, + }, + ], + warningCount: 0, + }, + ] diff --git a/test/lib/linter/rules/snapshots/Directives.ts.snap b/test/lib/linter/rules/snapshots/Directives.ts.snap new file mode 100644 index 000000000..fa260af46 Binary files /dev/null and b/test/lib/linter/rules/snapshots/Directives.ts.snap differ diff --git a/test/lib/linter/snapshots/LinterContext.ts.md b/test/lib/linter/snapshots/LinterContext.ts.md new file mode 100644 index 000000000..7de628490 --- /dev/null +++ b/test/lib/linter/snapshots/LinterContext.ts.md @@ -0,0 +1,250 @@ +# Snapshot report for `test/lib/linter/LinterContext.ts` + +The actual snapshot is saved in `LinterContext.ts.snap`. + +Generated by [AVA](https://avajs.dev). + +## generateLintResult: Disable and enable + +> Snapshot 1 + + { + coverageInfo: [], + errorCount: 1, + fatalErrorCount: 0, + filePath: '/foo.js', + messages: [ + { + column: 10, + line: 20, + message: 'Use of deprecated API \'foo\'', + ruleId: 'no-deprecated-api', + severity: 2, + }, + ], + warningCount: 0, + } + +## generateLintResult: Disable next line + +> Snapshot 1 + + { + coverageInfo: [], + errorCount: 9, + fatalErrorCount: 0, + filePath: '/foo.js', + messages: [ + { + column: 10, + line: 20, + message: 'Use of deprecated API \'foo\'', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 10, + line: 30, + message: 'Use of deprecated API \'foo\'', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 10, + line: 40, + message: 'Use of deprecated API \'foo\'', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 10, + line: 50, + message: 'Use of deprecated API \'foo\'', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 10, + line: 60, + message: 'Use of deprecated API \'foo\'', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 10, + line: 70, + message: 'Use of deprecated API \'foo\'', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 10, + line: 80, + message: 'Use of deprecated API \'foo\'', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 10, + line: 90, + message: 'Use of deprecated API \'foo\'', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 10, + line: 100, + message: 'Use of deprecated API \'foo\'', + ruleId: 'no-deprecated-api', + severity: 2, + }, + ], + warningCount: 0, + } + +## generateLintResult: Disable line + +> Snapshot 1 + + { + coverageInfo: [], + errorCount: 7, + fatalErrorCount: 0, + filePath: '/foo.js', + messages: [ + { + column: 10, + line: 40, + message: 'Use of deprecated API \'foo\'', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 10, + line: 50, + message: 'Use of deprecated API \'foo\'', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 10, + line: 60, + message: 'Use of deprecated API \'foo\'', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 10, + line: 70, + message: 'Use of deprecated API \'foo\'', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 10, + line: 80, + message: 'Use of deprecated API \'foo\'', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 10, + line: 90, + message: 'Use of deprecated API \'foo\'', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 10, + line: 100, + message: 'Use of deprecated API \'foo\'', + ruleId: 'no-deprecated-api', + severity: 2, + }, + ], + warningCount: 0, + } + +## generateLintResult: Multiple disables in same line + +> Snapshot 1 + + { + coverageInfo: [], + errorCount: 1, + fatalErrorCount: 0, + filePath: '/foo.js', + messages: [ + { + column: 10, + line: 10, + message: 'Use of deprecated API \'foo\'', + ruleId: 'no-deprecated-api', + severity: 2, + }, + ], + warningCount: 0, + } + +## generateLintResult: Edge positions + +> Snapshot 1 + + { + coverageInfo: [], + errorCount: 7, + fatalErrorCount: 0, + filePath: '/foo.js', + messages: [ + { + column: 10, + line: 40, + message: 'Use of deprecated API \'foo\'', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 10, + line: 50, + message: 'Use of deprecated API \'foo\'', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 10, + line: 60, + message: 'Use of deprecated API \'foo\'', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 10, + line: 70, + message: 'Use of deprecated API \'foo\'', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 10, + line: 80, + message: 'Use of deprecated API \'foo\'', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 10, + line: 90, + message: 'Use of deprecated API \'foo\'', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 10, + line: 100, + message: 'Use of deprecated API \'foo\'', + ruleId: 'no-deprecated-api', + severity: 2, + }, + ], + warningCount: 0, + } diff --git a/test/lib/linter/snapshots/LinterContext.ts.snap b/test/lib/linter/snapshots/LinterContext.ts.snap new file mode 100644 index 000000000..16e43728c Binary files /dev/null and b/test/lib/linter/snapshots/LinterContext.ts.snap differ diff --git a/test/lib/linter/ui5Types/directives.ts b/test/lib/linter/ui5Types/directives.ts new file mode 100644 index 000000000..33dfe9ef2 --- /dev/null +++ b/test/lib/linter/ui5Types/directives.ts @@ -0,0 +1,401 @@ +import test from "ava"; +import ts from "typescript"; +import {findDirectives, collectPossibleDirectives} from + "../../../../src/linter/ui5Types/directives.js"; +import {LintMetadata} from "../../../../src/linter/LinterContext.js"; + +test("collectPossibleDirectives should find directives in source file", (t) => { + const sourceCode = `// ui5lint-disable no-deprecated-api +const foo = 'bar'; +// ui5lint-enable no-deprecated-api + `; + const sourceFile = ts.createSourceFile("test.ts", sourceCode, ts.ScriptTarget.ESNext, true); + const directives = collectPossibleDirectives(sourceFile); + + t.is(directives.size, 2); + const d = Array.from(directives); + t.deepEqual(d[0], { + action: "disable", + scope: undefined, + ruleNames: ["no-deprecated-api"], + pos: 0, + length: 36, + line: 1, + column: 37, + }); + t.deepEqual(d[1], { + action: "enable", + scope: undefined, + ruleNames: ["no-deprecated-api"], + pos: 56, + length: 35, + line: 3, + column: 36, + }); +}); + +test("collectPossibleDirectives should find same-line directives in source file", (t) => { + const sourceCode = + `/* ui5lint-disable */const foo = 'bar';/*ui5lint-enable*/`; + const sourceFile = ts.createSourceFile("test.ts", sourceCode, ts.ScriptTarget.ESNext, true); + const directives = collectPossibleDirectives(sourceFile); + + t.is(directives.size, 2); + const d = Array.from(directives); + t.deepEqual(d[0], { + action: "disable", + scope: undefined, + ruleNames: [], + pos: 0, + length: 21, + line: 1, + column: 22, + }); + t.deepEqual(d[1], { + action: "enable", + scope: undefined, + ruleNames: [], + pos: 39, + length: 18, + line: 1, + column: 58, + }); +}); + +test("collectPossibleDirectives should find multi-line directives in source file", (t) => { + const sourceCode = + `/* +ui5lint-disable-next-line no-deprecated-api + +-- + +De$criptive / comment * +*/ +const foo = 'bar';`; + const sourceFile = ts.createSourceFile("test.ts", sourceCode, ts.ScriptTarget.ESNext, true); + const directives = collectPossibleDirectives(sourceFile); + + t.is(directives.size, 1); + const d = Array.from(directives); + t.deepEqual(d[0], { + action: "disable", + scope: "next-line", + ruleNames: ["no-deprecated-api"], + pos: 0, + length: 78, + line: 7, + column: 3, + }); +}); + +test("collectPossibleDirectives should ignore line breaks after single-line directive in source file", (t) => { + const sourceCode = + `const foo = 'bar'; // ui5lint-disable-line -- description + + +`; + const sourceFile = ts.createSourceFile("test.ts", sourceCode, ts.ScriptTarget.ESNext, true); + const directives = collectPossibleDirectives(sourceFile); + + t.is(directives.size, 1); + const d = Array.from(directives); + t.deepEqual(d[0], { + action: "disable", + scope: "line", + ruleNames: [], + pos: 19, + length: 38, + line: 1, + column: 58, + }); +}); + +runTests("js"); +runTests("ts"); + +function runTests(suffix: "js" | "ts") { + test(`findDirectives: Case 1 (${suffix})`, (t) => { + const sourceCode = `// ui5lint-disable no-deprecated-api +const foo = 'bar'; +// ui5lint-enable no-deprecated-api`; + const sourceFile = ts.createSourceFile(`test.${suffix}`, sourceCode, ts.ScriptTarget.ESNext, true); + const metadata = {} as LintMetadata; + findDirectives(sourceFile, metadata); + t.is(metadata.directives.size, 2); + t.snapshot(metadata.directives); + }); + + test(`findDirectives: Case 2 (${suffix})`, (t) => { + const sourceCode = `// ui5lint-disable no-deprecated-api +const foo = 'bar'; +// ui5lint-enable no-deprecated-api`; + const sourceFile = ts.createSourceFile(`test.${suffix}`, sourceCode, ts.ScriptTarget.ESNext, true); + const metadata = {} as LintMetadata; + findDirectives(sourceFile, metadata); + t.is(metadata.directives.size, 2); + t.snapshot(metadata.directives); + }); + + test(`findDirectives: Case 3 (${suffix})`, (t) => { + const sourceCode = `// ui5lint-disable no-deprecated-api +function foo() { + // Some code +} +// ui5lint-enable no-deprecated-api, no-globals`; + + const sourceFile = ts.createSourceFile(`test.${suffix}`, sourceCode, ts.ScriptTarget.ESNext, true); + const metadata = {} as LintMetadata; + findDirectives(sourceFile, metadata); + t.is(metadata.directives.size, 2); + t.snapshot(metadata.directives); + }); + + test(`findDirectives: Case 4 (${suffix})`, (t) => { + const sourceCode = ` +function someFunction() { + // ui5lint-disable-next-line no-deprecated-api + someFunction2(); +}`; + const sourceFile = ts.createSourceFile(`test.${suffix}`, sourceCode, ts.ScriptTarget.ESNext, true); + const metadata = {} as LintMetadata; + findDirectives(sourceFile, metadata); + t.is(metadata.directives.size, 1); + t.snapshot(metadata.directives); + }); + + test(`findDirectives: Case 5 (${suffix})`, (t) => { + const sourceCode = ` +function someFunction() { + someFunction2(); // ui5lint-disable-line no-deprecated-api +}`; + const sourceFile = ts.createSourceFile(`test.${suffix}`, sourceCode, ts.ScriptTarget.ESNext, true); + const metadata = {} as LintMetadata; + findDirectives(sourceFile, metadata); + t.is(metadata.directives.size, 1); + t.snapshot(metadata.directives); + }); + + test(`findDirectives: Case 6 (${suffix})`, (t) => { + const sourceCode = ` +/* + ui5lint-disable no-deprecated-api + */ +function someFunction() { + // Some code +} +/* + ui5lint-enable no-deprecated-api, no-globals + */ +`; + const sourceFile = ts.createSourceFile(`test.${suffix}`, sourceCode, ts.ScriptTarget.ESNext, true); + const metadata = {} as LintMetadata; + findDirectives(sourceFile, metadata); + t.is(metadata.directives.size, 2); + t.snapshot(metadata.directives); + }); + + test(`findDirectives: Case 7 (${suffix})`, (t) => { + const sourceCode = ` +function someFunction() { + /* + ui5lint-disable-next-line no-deprecated-api + */ + someFunction2(); +}`; + const sourceFile = ts.createSourceFile(`test.${suffix}`, sourceCode, ts.ScriptTarget.ESNext, true); + const metadata = {} as LintMetadata; + findDirectives(sourceFile, metadata); + t.is(metadata.directives.size, 1); + t.snapshot(metadata.directives); + }); + + test(`findDirectives: Case 8 (${suffix})`, (t) => { + const sourceCode = ` +// ui5lint-disable no-deprecated-api +function someFunction1() { + // Some code +} +function someFunction2() { + // Some code +} +// ui5lint-enable no-deprecated-api, no-globals +function someFunction3() { + // Some code +}`; + const sourceFile = ts.createSourceFile(`test.${suffix}`, sourceCode, ts.ScriptTarget.ESNext, true); + const metadata = {} as LintMetadata; + findDirectives(sourceFile, metadata); + t.is(metadata.directives.size, 2); + t.snapshot(metadata.directives); + }); + + test(`findDirectives: Case 9 (${suffix})`, (t) => { + const sourceCode = ` +function someFunction() { + someFunction2(); /* ui5lint-disable-line no-deprecated-api */ +}`; + const sourceFile = ts.createSourceFile(`test.${suffix}`, sourceCode, ts.ScriptTarget.ESNext, true); + const metadata = {} as LintMetadata; + findDirectives(sourceFile, metadata); + t.is(metadata.directives.size, 1); + t.snapshot(metadata.directives); + }); + + test(`findDirectives: Case 10 (${suffix})`, (t) => { + const sourceCode = ` +/* + ui5lint-disable-next-line no-deprecated-api + */ +function deprecatedFunction9() { + // Some code +} + +/* + ui5lint-enable no-deprecated-api, no-globals + */`; + const sourceFile = ts.createSourceFile(`test.${suffix}`, sourceCode, ts.ScriptTarget.ESNext, true); + const metadata = {} as LintMetadata; + findDirectives(sourceFile, metadata); + t.is(metadata.directives.size, 2); + t.snapshot(metadata.directives); + }); + + test(`findDirectives: Case 11 (${suffix})`, (t) => { + const sourceCode = ` +// ui5lint-enable no-deprecated-api -- my comment +/* + ui5lint-disable-next-line no-deprecated-api -- my comment + + */ +function deprecatedFunction9() { + // Some code + return; + /* + ui5lint-enable no-deprecated-api, no-globals -- my other even longer + multiline comment with special * char$ + */ +} +`; + const sourceFile = ts.createSourceFile(`test.${suffix}`, sourceCode, ts.ScriptTarget.ESNext, true); + const metadata = {} as LintMetadata; + findDirectives(sourceFile, metadata); + t.is(metadata.directives.size, 3); + t.snapshot(metadata.directives); + }); + + test(`findDirectives: Case 12 (${suffix})`, (t) => { + const sourceCode = ` +/* ui5lint-disable-next-line no-deprecated-api, + no-globals +*/ +function deprecatedFunction9() { + // Some code +}`; + const sourceFile = ts.createSourceFile(`test.${suffix}`, sourceCode, ts.ScriptTarget.ESNext, true); + const metadata = {} as LintMetadata; + findDirectives(sourceFile, metadata); + t.is(metadata.directives.size, 1); + t.snapshot(metadata.directives); + }); + + test(`findDirectives: Case 13 (${suffix})`, (t) => { + const sourceCode = ` +// ui5lint-disable-next-line no-deprecated-api, +// ui5lint-disable no-deprecated-api,no-globals +// ui5lint-enable no-deprecated-api ,no-globals +// ui5lint-disable no-deprecated-api , no-globals +function deprecatedFunction9() { + // Some code +}`; + const sourceFile = ts.createSourceFile(`test.${suffix}`, sourceCode, ts.ScriptTarget.ESNext, true); + const metadata = {} as LintMetadata; + findDirectives(sourceFile, metadata); + t.is(metadata.directives.size, 4); + t.snapshot(metadata.directives); + }); + + test(`findDirectives: Case 14 (${suffix})`, (t) => { + const sourceCode = ` +// ui5lint-disable no-deprecated-api +/* + ui5lint-enable-next-line no-deprecated-api -- my comment + + */ +function deprecatedFunction9() { + // Some code + return; +} +`; + const sourceFile = ts.createSourceFile(`test.${suffix}`, sourceCode, ts.ScriptTarget.ESNext, true); + const metadata = {} as LintMetadata; + findDirectives(sourceFile, metadata); + t.is(metadata.directives.size, 2); + t.snapshot(metadata.directives); + }); + + test(`findDirectives: Case 15 (${suffix})`, (t) => { + const sourceCode = ` +// ui5lint-disable-next-line no-deprecated-api -- my comment +foo(); // ui5lint-enable-line +`; + const sourceFile = ts.createSourceFile(`test.${suffix}`, sourceCode, ts.ScriptTarget.ESNext, true); + const metadata = {} as LintMetadata; + findDirectives(sourceFile, metadata); + t.is(metadata.directives.size, 2); + t.snapshot(metadata.directives); + }); + + test(`findDirectives: Negative case 1 (${suffix})`, (t) => { + // Directive must not be preceded by asterisk any non-whitespace characters + const sourceCode = ` +/* + * ui5lint-disable-next-line no-deprecated-api + */ +function someFunction() { + someFunction2(); +} +`; + const sourceFile = ts.createSourceFile(`test.${suffix}`, sourceCode, ts.ScriptTarget.ESNext, true); + const metadata = {} as LintMetadata; + findDirectives(sourceFile, metadata); + t.is(metadata.directives.size, 0); + }); + + test(`findDirectives: Negative case 2 (${suffix})`, (t) => { + // Three slashes are invalid + const sourceCode = ` +/// ui5lint-disable-next-line no-deprecated-api +function someFunction() { + someFunction2(); +} +`; + const sourceFile = ts.createSourceFile(`test.${suffix}`, sourceCode, ts.ScriptTarget.ESNext, true); + const metadata = {} as LintMetadata; + findDirectives(sourceFile, metadata); + t.is(metadata.directives.size, 0); + }); + + test(`findDirectives: Negative case 3 (${suffix})`, (t) => { + // Incorrect prefix or action + const sourceCode = ` +// ui5-lint-disable +// ui5-linter-disable +// ui5-linter-disable +// ui5linter-disable +// lint-disable +// ui5-disable +// ui5lint-activate +// ui5lint disable +// ui5lint-disable-previous-line +// ui5lint-disable-line-next +// ui5lint-disable-next +function someFunction() { + someFunction2(); +} +`; + const sourceFile = ts.createSourceFile(`test.${suffix}`, sourceCode, ts.ScriptTarget.ESNext, true); + const metadata = {} as LintMetadata; + findDirectives(sourceFile, metadata); + t.is(metadata.directives.size, 0); + }); +} diff --git a/test/lib/linter/ui5Types/snapshots/directives.ts.md b/test/lib/linter/ui5Types/snapshots/directives.ts.md new file mode 100644 index 000000000..8c81dfaf2 --- /dev/null +++ b/test/lib/linter/ui5Types/snapshots/directives.ts.md @@ -0,0 +1,845 @@ +# Snapshot report for `test/lib/linter/ui5Types/directives.ts` + +The actual snapshot is saved in `directives.ts.snap`. + +Generated by [AVA](https://avajs.dev). + +## findDirectives: Case 1 (js) + +> Snapshot 1 + + Set { + { + action: 'disable', + column: 37, + length: 36, + line: 1, + pos: 0, + ruleNames: [ + 'no-deprecated-api', + ], + scope: undefined, + }, + { + action: 'enable', + column: 36, + length: 35, + line: 3, + pos: 56, + ruleNames: [ + 'no-deprecated-api', + ], + scope: undefined, + }, + } + +## findDirectives: Case 2 (js) + +> Snapshot 1 + + Set { + { + action: 'disable', + column: 37, + length: 36, + line: 1, + pos: 0, + ruleNames: [ + 'no-deprecated-api', + ], + scope: undefined, + }, + { + action: 'enable', + column: 36, + length: 35, + line: 3, + pos: 56, + ruleNames: [ + 'no-deprecated-api', + ], + scope: undefined, + }, + } + +## findDirectives: Case 3 (js) + +> Snapshot 1 + + Set { + { + action: 'disable', + column: 37, + length: 36, + line: 1, + pos: 0, + ruleNames: [ + 'no-deprecated-api', + ], + scope: undefined, + }, + { + action: 'enable', + column: 48, + length: 47, + line: 5, + pos: 73, + ruleNames: [ + 'no-deprecated-api', + 'no-globals', + ], + scope: undefined, + }, + } + +## findDirectives: Case 4 (js) + +> Snapshot 1 + + Set { + { + action: 'disable', + column: 51, + length: 46, + line: 3, + pos: 31, + ruleNames: [ + 'no-deprecated-api', + ], + scope: 'next-line', + }, + } + +## findDirectives: Case 5 (js) + +> Snapshot 1 + + Set { + { + action: 'disable', + column: 63, + length: 41, + line: 3, + pos: 48, + ruleNames: [ + 'no-deprecated-api', + ], + scope: 'line', + }, + } + +## findDirectives: Case 6 (js) + +> Snapshot 1 + + Set { + { + action: 'disable', + column: 4, + length: 41, + line: 4, + pos: 1, + ruleNames: [ + 'no-deprecated-api', + ], + scope: undefined, + }, + { + action: 'enable', + column: 4, + length: 52, + line: 10, + pos: 88, + ruleNames: [ + 'no-deprecated-api', + 'no-globals', + ], + scope: undefined, + }, + } + +## findDirectives: Case 7 (js) + +> Snapshot 1 + + Set { + { + action: 'disable', + column: 8, + length: 60, + line: 5, + pos: 31, + ruleNames: [ + 'no-deprecated-api', + ], + scope: 'next-line', + }, + } + +## findDirectives: Case 8 (js) + +> Snapshot 1 + + Set { + { + action: 'disable', + column: 37, + length: 36, + line: 2, + pos: 1, + ruleNames: [ + 'no-deprecated-api', + ], + scope: undefined, + }, + { + action: 'enable', + column: 48, + length: 47, + line: 9, + pos: 130, + ruleNames: [ + 'no-deprecated-api', + 'no-globals', + ], + scope: undefined, + }, + } + +## findDirectives: Case 9 (js) + +> Snapshot 1 + + Set { + { + action: 'disable', + column: 66, + length: 44, + line: 3, + pos: 48, + ruleNames: [ + 'no-deprecated-api', + ], + scope: 'line', + }, + } + +## findDirectives: Case 10 (js) + +> Snapshot 1 + + Set { + { + action: 'disable', + column: 4, + length: 51, + line: 4, + pos: 1, + ruleNames: [ + 'no-deprecated-api', + ], + scope: 'next-line', + }, + { + action: 'enable', + column: 4, + length: 52, + line: 11, + pos: 106, + ruleNames: [ + 'no-deprecated-api', + 'no-globals', + ], + scope: undefined, + }, + } + +## findDirectives: Case 11 (js) + +> Snapshot 1 + + Set { + { + action: 'enable', + column: 50, + length: 49, + line: 2, + pos: 1, + ruleNames: [ + 'no-deprecated-api', + ], + scope: undefined, + }, + { + action: 'disable', + column: 4, + length: 66, + line: 6, + pos: 51, + ruleNames: [ + 'no-deprecated-api', + ], + scope: 'next-line', + }, + { + action: 'enable', + column: 5, + length: 119, + line: 13, + pos: 184, + ruleNames: [ + 'no-deprecated-api', + 'no-globals', + ], + scope: undefined, + }, + } + +## findDirectives: Case 12 (js) + +> Snapshot 1 + + Set { + { + action: 'disable', + column: 3, + length: 62, + line: 4, + pos: 1, + ruleNames: [ + 'no-deprecated-api', + 'no-globals', + ], + scope: 'next-line', + }, + } + +## findDirectives: Case 13 (js) + +> Snapshot 1 + + Set { + { + action: 'disable', + column: 48, + length: 47, + line: 2, + pos: 1, + ruleNames: [ + 'no-deprecated-api', + ], + scope: 'next-line', + }, + { + action: 'disable', + column: 48, + length: 47, + line: 3, + pos: 49, + ruleNames: [ + 'no-deprecated-api', + 'no-globals', + ], + scope: undefined, + }, + { + action: 'enable', + column: 48, + length: 47, + line: 4, + pos: 97, + ruleNames: [ + 'no-deprecated-api', + 'no-globals', + ], + scope: undefined, + }, + { + action: 'disable', + column: 50, + length: 49, + line: 5, + pos: 145, + ruleNames: [ + 'no-deprecated-api', + 'no-globals', + ], + scope: undefined, + }, + } + +## findDirectives: Case 14 (js) + +> Snapshot 1 + + Set { + { + action: 'disable', + column: 37, + length: 36, + line: 2, + pos: 1, + ruleNames: [ + 'no-deprecated-api', + ], + scope: undefined, + }, + { + action: 'enable', + column: 4, + length: 65, + line: 6, + pos: 38, + ruleNames: [ + 'no-deprecated-api', + ], + scope: 'next-line', + }, + } + +## findDirectives: Case 15 (js) + +> Snapshot 1 + + Set { + { + action: 'disable', + column: 61, + length: 60, + line: 2, + pos: 1, + ruleNames: [ + 'no-deprecated-api', + ], + scope: 'next-line', + }, + { + action: 'enable', + column: 30, + length: 22, + line: 3, + pos: 69, + ruleNames: [], + scope: 'line', + }, + } + +## findDirectives: Case 1 (ts) + +> Snapshot 1 + + Set { + { + action: 'disable', + column: 37, + length: 36, + line: 1, + pos: 0, + ruleNames: [ + 'no-deprecated-api', + ], + scope: undefined, + }, + { + action: 'enable', + column: 36, + length: 35, + line: 3, + pos: 56, + ruleNames: [ + 'no-deprecated-api', + ], + scope: undefined, + }, + } + +## findDirectives: Case 2 (ts) + +> Snapshot 1 + + Set { + { + action: 'disable', + column: 37, + length: 36, + line: 1, + pos: 0, + ruleNames: [ + 'no-deprecated-api', + ], + scope: undefined, + }, + { + action: 'enable', + column: 36, + length: 35, + line: 3, + pos: 56, + ruleNames: [ + 'no-deprecated-api', + ], + scope: undefined, + }, + } + +## findDirectives: Case 3 (ts) + +> Snapshot 1 + + Set { + { + action: 'disable', + column: 37, + length: 36, + line: 1, + pos: 0, + ruleNames: [ + 'no-deprecated-api', + ], + scope: undefined, + }, + { + action: 'enable', + column: 48, + length: 47, + line: 5, + pos: 73, + ruleNames: [ + 'no-deprecated-api', + 'no-globals', + ], + scope: undefined, + }, + } + +## findDirectives: Case 4 (ts) + +> Snapshot 1 + + Set { + { + action: 'disable', + column: 51, + length: 46, + line: 3, + pos: 31, + ruleNames: [ + 'no-deprecated-api', + ], + scope: 'next-line', + }, + } + +## findDirectives: Case 5 (ts) + +> Snapshot 1 + + Set { + { + action: 'disable', + column: 63, + length: 41, + line: 3, + pos: 48, + ruleNames: [ + 'no-deprecated-api', + ], + scope: 'line', + }, + } + +## findDirectives: Case 6 (ts) + +> Snapshot 1 + + Set { + { + action: 'disable', + column: 4, + length: 41, + line: 4, + pos: 1, + ruleNames: [ + 'no-deprecated-api', + ], + scope: undefined, + }, + { + action: 'enable', + column: 4, + length: 52, + line: 10, + pos: 88, + ruleNames: [ + 'no-deprecated-api', + 'no-globals', + ], + scope: undefined, + }, + } + +## findDirectives: Case 7 (ts) + +> Snapshot 1 + + Set { + { + action: 'disable', + column: 8, + length: 60, + line: 5, + pos: 31, + ruleNames: [ + 'no-deprecated-api', + ], + scope: 'next-line', + }, + } + +## findDirectives: Case 8 (ts) + +> Snapshot 1 + + Set { + { + action: 'disable', + column: 37, + length: 36, + line: 2, + pos: 1, + ruleNames: [ + 'no-deprecated-api', + ], + scope: undefined, + }, + { + action: 'enable', + column: 48, + length: 47, + line: 9, + pos: 130, + ruleNames: [ + 'no-deprecated-api', + 'no-globals', + ], + scope: undefined, + }, + } + +## findDirectives: Case 9 (ts) + +> Snapshot 1 + + Set { + { + action: 'disable', + column: 66, + length: 44, + line: 3, + pos: 48, + ruleNames: [ + 'no-deprecated-api', + ], + scope: 'line', + }, + } + +## findDirectives: Case 10 (ts) + +> Snapshot 1 + + Set { + { + action: 'disable', + column: 4, + length: 51, + line: 4, + pos: 1, + ruleNames: [ + 'no-deprecated-api', + ], + scope: 'next-line', + }, + { + action: 'enable', + column: 4, + length: 52, + line: 11, + pos: 106, + ruleNames: [ + 'no-deprecated-api', + 'no-globals', + ], + scope: undefined, + }, + } + +## findDirectives: Case 11 (ts) + +> Snapshot 1 + + Set { + { + action: 'enable', + column: 50, + length: 49, + line: 2, + pos: 1, + ruleNames: [ + 'no-deprecated-api', + ], + scope: undefined, + }, + { + action: 'disable', + column: 4, + length: 66, + line: 6, + pos: 51, + ruleNames: [ + 'no-deprecated-api', + ], + scope: 'next-line', + }, + { + action: 'enable', + column: 5, + length: 119, + line: 13, + pos: 184, + ruleNames: [ + 'no-deprecated-api', + 'no-globals', + ], + scope: undefined, + }, + } + +## findDirectives: Case 12 (ts) + +> Snapshot 1 + + Set { + { + action: 'disable', + column: 3, + length: 62, + line: 4, + pos: 1, + ruleNames: [ + 'no-deprecated-api', + 'no-globals', + ], + scope: 'next-line', + }, + } + +## findDirectives: Case 13 (ts) + +> Snapshot 1 + + Set { + { + action: 'disable', + column: 48, + length: 47, + line: 2, + pos: 1, + ruleNames: [ + 'no-deprecated-api', + ], + scope: 'next-line', + }, + { + action: 'disable', + column: 48, + length: 47, + line: 3, + pos: 49, + ruleNames: [ + 'no-deprecated-api', + 'no-globals', + ], + scope: undefined, + }, + { + action: 'enable', + column: 48, + length: 47, + line: 4, + pos: 97, + ruleNames: [ + 'no-deprecated-api', + 'no-globals', + ], + scope: undefined, + }, + { + action: 'disable', + column: 50, + length: 49, + line: 5, + pos: 145, + ruleNames: [ + 'no-deprecated-api', + 'no-globals', + ], + scope: undefined, + }, + } + +## findDirectives: Case 14 (ts) + +> Snapshot 1 + + Set { + { + action: 'disable', + column: 37, + length: 36, + line: 2, + pos: 1, + ruleNames: [ + 'no-deprecated-api', + ], + scope: undefined, + }, + { + action: 'enable', + column: 4, + length: 65, + line: 6, + pos: 38, + ruleNames: [ + 'no-deprecated-api', + ], + scope: 'next-line', + }, + } + +## findDirectives: Case 15 (ts) + +> Snapshot 1 + + Set { + { + action: 'disable', + column: 61, + length: 60, + line: 2, + pos: 1, + ruleNames: [ + 'no-deprecated-api', + ], + scope: 'next-line', + }, + { + action: 'enable', + column: 30, + length: 22, + line: 3, + pos: 69, + ruleNames: [], + scope: 'line', + }, + } diff --git a/test/lib/linter/ui5Types/snapshots/directives.ts.snap b/test/lib/linter/ui5Types/snapshots/directives.ts.snap new file mode 100644 index 000000000..c7a2ab4da Binary files /dev/null and b/test/lib/linter/ui5Types/snapshots/directives.ts.snap differ