-
-
Notifications
You must be signed in to change notification settings - Fork 661
Add custom processor to lint JSDoc codeblocks #1300
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
30 commits
Select commit
Hold shift + click to select a range
bb5f895
feat: add custom processor to lint JSDoc codeblocks
som-sm 9acd9b2
feat: handle indented JSDoc comments
som-sm f5eba7f
refactor: improve indented JSDoc codeblocks handling
som-sm 1146158
fix: lint issues in JSDoc codeblocks
som-sm 523017c
fix: non auto-fixable lint issues in JSDoc codeblocks
som-sm 30cb611
fix: incorrect conversion of `interface` to `type`
som-sm b7dea22
fix: adjust fixer positioning for editor suggestions
som-sm b7695d7
fix: disable `allowJs`
som-sm 150f369
fix: incorrect newlines in `indent` capturing group
som-sm 61ef5f3
chore: enable back `allowJs`
som-sm 55a48cc
fix: fixer position adjustment for indented codeblocks
som-sm 3f4d9d7
fix: remove unnecessary check
som-sm c4290b3
chore: add comment
som-sm 004e742
fix: indent calculation when `index` is `0`
som-sm 79961bf
test: setup tests for `jsdoc-codeblocks` processor
som-sm 053b6cb
refactor: replace `assert.partialDeepStrictEqual` with manual check
som-sm e968878
refactor: cleanup code
som-sm 73428af
fix: remove spaces, use tabs
som-sm a255bf7
test: refactor assertion so that error messages are more readable
som-sm 7000982
test: add more cases
som-sm 290f14e
test: improve test cases
som-sm 6bdd968
test: add valid cases
som-sm 2c50aff
test: add more cases
som-sm 927cbbf
test: add case covering errors ending at first column
som-sm a47aecd
refactor: improve types and naming
som-sm da3bd0d
refactor: improve code
som-sm e9440ac
refactor: move code samples to utils
som-sm 0820fe5
test: improve test cases
som-sm d7a0f1c
chore: merge branch 'main'
som-sm 5c94733
fix: update `tsc` command in `ts-canary` workflow
som-sm File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This config file is used to lint the test cases. And, this is present inside the repo instead of the temp directory because it needs certain imports. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| import tseslint from 'typescript-eslint'; | ||
| import {defineConfig} from 'eslint/config'; | ||
| import {jsdocCodeblocksProcessor} from '../jsdoc-codeblocks.js'; | ||
|
|
||
| const errorEndingAtFirstColumnRule = { | ||
| create(context) { | ||
| return { | ||
| 'TSTypeAliasDeclaration Literal'(node) { | ||
| if (node.value !== 'error_ending_at_first_column') { | ||
| return; | ||
| } | ||
|
|
||
| context.report({ | ||
| loc: { | ||
| start: { | ||
| line: node.loc.start.line, | ||
| column: 0, | ||
| }, | ||
| end: { | ||
| line: node.loc.start.line + 1, | ||
| column: 0, | ||
| }, | ||
| }, | ||
| message: 'Error ending at first column', | ||
| }); | ||
| }, | ||
| }; | ||
| }, | ||
| }; | ||
|
|
||
| const config = defineConfig( | ||
| tseslint.configs.recommended, | ||
| tseslint.configs.stylistic, | ||
| { | ||
| rules: { | ||
| '@typescript-eslint/no-unused-vars': 'off', | ||
| '@typescript-eslint/consistent-type-definitions': [ | ||
| 'error', | ||
| 'type', | ||
| ], | ||
| 'test/error-ending-at-first-column': 'error', | ||
| }, | ||
| }, | ||
| { | ||
| plugins: { | ||
| test: { | ||
| processors: { | ||
| 'jsdoc-codeblocks': jsdocCodeblocksProcessor, | ||
| }, | ||
| rules: { | ||
| 'error-ending-at-first-column': errorEndingAtFirstColumnRule, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| files: ['**/*.d.ts'], | ||
| processor: 'test/jsdoc-codeblocks', | ||
| }, | ||
| ); | ||
|
|
||
| export default config; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,172 @@ | ||
| // @ts-check | ||
| import tsParser from '@typescript-eslint/parser'; | ||
|
|
||
| /** | ||
| @import {Linter} from 'eslint'; | ||
| */ | ||
|
|
||
| const CODEBLOCK_REGEX = /(?<openingFence>(?<indent>^[ \t]*)```(?:ts|typescript)?\n)(?<code>[\s\S]*?)\n\s*```/gm; | ||
| /** | ||
| @typedef {{lineOffset: number, characterOffset: number, indent: string, unindentedText: string}} CodeblockData | ||
| @type {Map<string, CodeblockData[]>} | ||
| */ | ||
| const codeblockDataPerFile = new Map(); | ||
|
|
||
| /** | ||
| @param {string} text | ||
| @param {number} index | ||
| @param {string} indent | ||
| @returns {number} | ||
| */ | ||
| function indentsUptoIndex(text, index, indent) { | ||
| let i = 0; | ||
| let indents = 0; | ||
|
|
||
| for (const line of text.split('\n')) { | ||
| if (i > index) { | ||
| break; | ||
| } | ||
|
|
||
| if (line === '') { | ||
| i += 1; // +1 for the newline | ||
| continue; | ||
| } | ||
|
|
||
| i += line.length + 1; // +1 for the newline | ||
| i -= indent.length; // Because `text` is unindented but `index` corresponds to dedented text | ||
| indents += indent.length; | ||
| } | ||
|
|
||
| return indents; | ||
| } | ||
|
|
||
| export const jsdocCodeblocksProcessor = { | ||
| supportsAutofix: true, | ||
|
|
||
| /** | ||
| @param {string} text | ||
| @param {string} filename | ||
| @returns {(string | Linter.ProcessorFile)[]} | ||
| */ | ||
| preprocess(text, filename) { | ||
| const ast = tsParser.parse(text); | ||
|
|
||
| const jsdocComments = ast.comments.filter( | ||
| comment => comment.type === 'Block' && comment.value.startsWith('*'), | ||
| ); | ||
|
|
||
| /** @type {(string | Linter.ProcessorFile)[]} */ | ||
| const files = [text]; // First entry is for the entire file | ||
| /** @type {CodeblockData[]} */ | ||
| const allCodeblocksData = []; | ||
|
|
||
| // Loop over all JSDoc comments in the file | ||
| for (const comment of jsdocComments) { | ||
| // Loop over all codeblocks in the JSDoc comment | ||
| for (const match of comment.value.matchAll(CODEBLOCK_REGEX)) { | ||
| const {code, openingFence, indent} = match.groups ?? {}; | ||
|
|
||
| // Skip empty code blocks | ||
| if (!code || !openingFence || indent === undefined) { | ||
| continue; | ||
| } | ||
|
|
||
| const codeLines = code.split('\n'); | ||
| const indentSize = indent.length; | ||
|
|
||
| // Skip comments that are not consistently indented | ||
| if (!codeLines.every(line => line === '' || line.startsWith(indent))) { | ||
| continue; | ||
| } | ||
|
|
||
| const dedentedCode = codeLines | ||
| .map(line => line.slice(indentSize)) | ||
| .join('\n'); | ||
|
|
||
| files.push({ | ||
| text: dedentedCode, | ||
| filename: `${files.length}.ts`, // Final filename example: `/path/to/type-fest/source/and.d.ts/1_1.ts` | ||
| }); | ||
|
|
||
| const linesBeforeMatch = comment.value.slice(0, match.index).split('\n').length - 1; | ||
| allCodeblocksData.push({ | ||
| lineOffset: comment.loc.start.line + linesBeforeMatch, | ||
| characterOffset: comment.range[0] + match.index + openingFence.length + 2, // +2 because `comment.value` doesn't include the starting `/*` | ||
| indent, | ||
| unindentedText: code, | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| codeblockDataPerFile.set(filename, allCodeblocksData); | ||
|
|
||
| return files; | ||
| }, | ||
|
|
||
| /** | ||
| @param {import('eslint').Linter.LintMessage[][]} messages | ||
| @param {string} filename | ||
| @returns {import('eslint').Linter.LintMessage[]} | ||
| */ | ||
| postprocess(messages, filename) { | ||
| const codeblocks = codeblockDataPerFile.get(filename) || []; | ||
| codeblockDataPerFile.delete(filename); | ||
|
|
||
| const normalizedMessages = [...(messages[0] ?? [])]; // First entry contains errors for the entire file, and it doesn't need any adjustments | ||
|
|
||
| for (const [index, codeblockMessages] of messages.slice(1).entries()) { | ||
| const codeblockData = codeblocks[index]; | ||
|
|
||
| if (!codeblockData) { | ||
| // This should ideally never happen | ||
| continue; | ||
| } | ||
|
|
||
| const {lineOffset, characterOffset, indent, unindentedText} = codeblockData; | ||
|
|
||
| for (const message of codeblockMessages) { | ||
| message.line += lineOffset; | ||
| message.column += indent.length; | ||
|
|
||
| if (typeof message.endColumn === 'number' && message.endColumn > 1) { | ||
| // An `endColumn` of `1` indicates the error actually ended on the previous line since it's exclusive. | ||
| // So, adding `indent.length` in this case would incorrectly move the error marker into the indentation. | ||
| // Therefore, the indentation length is only added when `endColumn` is greater than `1`. | ||
| message.endColumn += indent.length; | ||
| } | ||
|
|
||
| if (typeof message.endLine === 'number') { | ||
| message.endLine += lineOffset; | ||
| } | ||
|
|
||
| if (message.fix) { | ||
| message.fix.text = message.fix.text.split('\n').join(`\n${indent}`); | ||
|
|
||
| const indentsBeforeFixStart = indentsUptoIndex(unindentedText, message.fix.range[0], indent); | ||
| const indentsBeforeFixEnd = indentsUptoIndex(unindentedText, message.fix.range[1] - 1, indent); // -1 because range end is exclusive | ||
|
|
||
| message.fix.range = [ | ||
| message.fix.range[0] + characterOffset + indentsBeforeFixStart, | ||
| message.fix.range[1] + characterOffset + indentsBeforeFixEnd, | ||
| ]; | ||
| } | ||
|
|
||
| for (const {fix} of (message.suggestions ?? [])) { | ||
| fix.text = fix.text.split('\n').join(`\n${indent}`); | ||
|
|
||
| const indentsBeforeFixStart = indentsUptoIndex(unindentedText, fix.range[0], indent); | ||
| const indentsBeforeFixEnd = indentsUptoIndex(unindentedText, fix.range[1] - 1, indent); // -1 because range end is exclusive | ||
|
|
||
| fix.range = [ | ||
| fix.range[0] + characterOffset + indentsBeforeFixStart, | ||
| fix.range[1] + characterOffset + indentsBeforeFixEnd, | ||
| ]; | ||
| } | ||
|
|
||
| normalizedMessages.push(message); | ||
| } | ||
| } | ||
|
|
||
| return normalizedMessages; | ||
| }, | ||
| }; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This bump is needed because the number of files in the TS program has increased from
792to839. The default limit seems to be 4GB, and increasing it by 1GB works fine.The extra files come from the new
typescript-eslintpackage added in this PR.If this is a problem, we can disable
allowJSintsconfig.jsonfor now, which will bring down the files to514and then come back to this separately.