From 6782fd7bf08c38c8e6279f116a55bafa0027944a Mon Sep 17 00:00:00 2001 From: StyleShit <32631382+StyleShit@users.noreply.github.com> Date: Sun, 27 Apr 2025 22:00:17 +0300 Subject: [PATCH 1/3] feat: support eslint directives and inline configs Closes #9 --- .changeset/poor-keys-sniff.md | 5 + .../__tests__/php-source-code.test.ts | 123 ++++++++++++ src/language/php-source-code.ts | 18 +- .../text-source-code-with-comments.ts | 184 ++++++++++++++++++ 4 files changed, 316 insertions(+), 14 deletions(-) create mode 100644 .changeset/poor-keys-sniff.md create mode 100644 src/language/text-source-code-with-comments.ts diff --git a/.changeset/poor-keys-sniff.md b/.changeset/poor-keys-sniff.md new file mode 100644 index 0000000..e058dc6 --- /dev/null +++ b/.changeset/poor-keys-sniff.md @@ -0,0 +1,5 @@ +--- +'eslint-plugin-php': minor +--- + +Support eslint directives and inline configs diff --git a/src/language/__tests__/php-source-code.test.ts b/src/language/__tests__/php-source-code.test.ts index d8a0aed..aa3db58 100644 --- a/src/language/__tests__/php-source-code.test.ts +++ b/src/language/__tests__/php-source-code.test.ts @@ -56,6 +56,129 @@ describe('PHPSourceCode', () => { // Assert. expect(offset).toBe(52); }); + + it('should return inline config', () => { + // Arrange. + const code = ` { + // Arrange. + const code = ` ({ ...d }))).toStrictEqual([ + { + type: 'disable', + value: 'php/test-rule', + justification: 'ok here', + node: allComments[1], + }, + { + type: 'enable', + value: '', + justification: '', + node: allComments[2], + }, + { + type: 'disable-next-line', + value: 'php/test-rule', + justification: '', + node: allComments[3], + }, + { + type: 'disable-line', + value: 'php/test-rule', + justification: 'ok here', + node: allComments[4], + }, + { + type: 'disable-line', + value: 'php/test-rule', + justification: 'ok here', + node: allComments[5], + }, + ]); + + expect(problems).toStrictEqual([ + { + ruleId: null, + loc: allComments[7]?.loc, + message: + 'eslint-disable-line comment should not span multiple lines.', + }, + ]); + }); }); function createSourceCode(code: string) { diff --git a/src/language/php-source-code.ts b/src/language/php-source-code.ts index 4e25d2d..1ea08b6 100644 --- a/src/language/php-source-code.ts +++ b/src/language/php-source-code.ts @@ -1,27 +1,17 @@ -import type { Location, Node, Program } from 'php-parser'; +import type { Location, Node } from 'php-parser'; import { simpleTraverse } from '../utils/simple-traverse'; import { type SourceRange, type TraversalStep, - TextSourceCodeBase, VisitNodeStep, } from '@eslint/plugin-kit'; -import { Position } from '@eslint/core'; +import type { Position } from '@eslint/core'; import { LINE_START } from './php-language'; +import { TextSourceCodeWithComments } from './text-source-code-with-comments'; -export class PHPSourceCode extends TextSourceCodeBase { +export class PHPSourceCode extends TextSourceCodeWithComments { #parents = new WeakMap(); - public override ast: Program; - public override text: string; - - constructor({ ast, text }: { ast: Program; text: string }) { - super({ ast, text }); - - this.ast = ast; - this.text = text; - } - override getParent(node: Node) { return this.#parents.get(node); } diff --git a/src/language/text-source-code-with-comments.ts b/src/language/text-source-code-with-comments.ts new file mode 100644 index 0000000..ab0e555 --- /dev/null +++ b/src/language/text-source-code-with-comments.ts @@ -0,0 +1,184 @@ +import type { + DirectiveType, + FileProblem, + RulesConfig, + SourceLocation, +} from '@eslint/core'; +import { + ConfigCommentParser, + Directive, + TextSourceCodeBase, +} from '@eslint/plugin-kit'; +import type { Comment, Location, Program } from 'php-parser'; + +type NormalizedComment = { + value: string; + loc: Location | null; +}; + +type InlineConfigElement = { + config: { rules: RulesConfig }; + loc: SourceLocation; +}; + +const INLINE_CONFIG = + /^\s*(?:eslint(?:-enable|-disable(?:(?:-next)?-line)?)?)(?:\s|$)/u; + +const ESLINT_DIRECTIVES = [ + 'eslint-disable', + 'eslint-enable', + 'eslint-disable-next-line', + 'eslint-disable-line', +]; + +const commentParser = new ConfigCommentParser(); + +/** + * This class is heavily inspired by `@eslint/json` & `@eslint/css`: + * + * @see https://github.com/eslint/json/blob/42cca7789/src/languages/json-source-code.js + * @see https://github.com/eslint/css/blob/fa391df2c/src/languages/css-source-code.js + */ +export abstract class TextSourceCodeWithComments extends TextSourceCodeBase { + #inlineConfigComments: NormalizedComment[] | null = null; + + public override ast: Program; + public override text: string; + public comments: NormalizedComment[]; + + constructor({ ast, text }: { ast: Program; text: string }) { + super({ ast, text }); + + this.ast = ast; + this.text = text; + this.comments = this.normalizeComments(ast.comments ?? []); + } + + private normalizeComments(comments: Comment[]): NormalizedComment[] { + return comments.reduce((acc, comment) => { + if (!comment.loc) { + return acc; + } + + if (comment.kind === 'commentblock') { + acc.push({ + value: comment.value + .replace(/^\/\*\s*(.*?)\s*\*\/$/gm, '$1') + .trim(), + loc: comment.loc, + }); + } + + if (comment.kind === 'commentline') { + // `php-parser` sets the end line wrong. + comment.loc.end.line = comment.loc.start.line; + + acc.push({ + value: comment.value.replace(/^\/\/\s*/gm, '').trim(), + loc: comment.loc, + }); + } + + return acc; + }, []); + } + + getInlineConfigNodes() { + if (!this.#inlineConfigComments) { + this.#inlineConfigComments = this.comments.filter((comment) => + INLINE_CONFIG.test(comment.value), + ); + } + + return this.#inlineConfigComments; + } + + getDisableDirectives() { + const problems: FileProblem[] = []; + const directives: Directive[] = []; + + this.getInlineConfigNodes().forEach((comment) => { + const parsedComment = commentParser.parseDirective(comment.value); + + if (!parsedComment || !comment.loc) { + return; + } + + const { label, value, justification } = parsedComment; + + // `eslint-disable-line` directives are not allowed to span multiple lines as it would be confusing to which lines they apply + if ( + label === 'eslint-disable-line' && + comment.loc.start.line !== comment.loc.end.line + ) { + const message = `${label} comment should not span multiple lines.`; + + problems.push({ + ruleId: null, + message, + loc: comment.loc, + }); + + return; + } + + if (ESLINT_DIRECTIVES.includes(label)) { + const directiveType = label.replace(/^eslint-/, ''); + + directives.push( + new Directive({ + type: directiveType as DirectiveType, + node: comment, + value, + justification, + }), + ); + } + }); + + return { problems, directives }; + } + + applyInlineConfig() { + const problems: FileProblem[] = []; + const configs: InlineConfigElement[] = []; + + this.getInlineConfigNodes().forEach((comment) => { + const parsedComment = commentParser.parseDirective(comment.value); + + if (!parsedComment || !comment.loc) { + return; + } + + const { label, value } = parsedComment; + + if (label !== 'eslint') { + return; + } + + const parseResult = commentParser.parseJSONLikeConfig(value); + + if (parseResult.ok) { + configs.push({ + config: { + rules: parseResult.config, + }, + loc: comment.loc, + }); + + return; + } + + problems.push({ + ruleId: null, + message: parseResult.error.message, + loc: comment.loc, + }); + }); + + return { + configs, + problems, + }; + } +} From d4b82289e696c80ccdab83bc6f5a2ff772a2996d Mon Sep 17 00:00:00 2001 From: StyleShit <32631382+StyleShit@users.noreply.github.com> Date: Sun, 27 Apr 2025 22:01:23 +0300 Subject: [PATCH 2/3] wip --- src/language/__tests__/php-source-code.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/language/__tests__/php-source-code.test.ts b/src/language/__tests__/php-source-code.test.ts index aa3db58..d4d473b 100644 --- a/src/language/__tests__/php-source-code.test.ts +++ b/src/language/__tests__/php-source-code.test.ts @@ -57,7 +57,7 @@ describe('PHPSourceCode', () => { expect(offset).toBe(52); }); - it('should return inline config', () => { + it('should return inline configs', () => { // Arrange. const code = ` Date: Sun, 27 Apr 2025 22:01:58 +0300 Subject: [PATCH 3/3] wip --- src/language/__tests__/php-source-code.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/language/__tests__/php-source-code.test.ts b/src/language/__tests__/php-source-code.test.ts index d4d473b..f3aebf8 100644 --- a/src/language/__tests__/php-source-code.test.ts +++ b/src/language/__tests__/php-source-code.test.ts @@ -68,7 +68,6 @@ describe('PHPSourceCode', () => { /* Invalid rule config comments: */ /* eslint php/test-rule: [error */ /* eslint php/test-rule: [1, { allow: ["foo"] ] */ - `; const sourceCode = createSourceCode(code);