diff --git a/CHANGELOG.md b/CHANGELOG.md index 686c8555..0a615470 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - (Rust) Support for r# raw strings with step definition patterns ([#176](https://github.com/cucumber/language-service/pull/176)) - (Rust) Line continuation characters in rust step definition patterns ([#179](https://github.com/cucumber/language-service/pull/179)) - (Python) Unexpected spaces and commas in generated step definitions [#160](https://github.com/cucumber/language-service/issues/160) +- (TypeScript) Tree sitter parser failing on class decorators ([#186](https://github.com/cucumber/language-service/pull/186)) ### Added - (Python) Support for u-strings with step definition patterns ([#173](https://github.com/cucumber/language-service/pull/173)) diff --git a/src/language/SourceAnalyzer.ts b/src/language/SourceAnalyzer.ts index ce015de1..076f5855 100644 --- a/src/language/SourceAnalyzer.ts +++ b/src/language/SourceAnalyzer.ts @@ -1,7 +1,12 @@ import { RegExps, StringOrRegExp } from '@cucumber/cucumber-expressions' import { LocationLink } from 'vscode-languageserver-types' -import { createLocationLink, makeParameterType, syntaxNode } from './helpers.js' +import { + createLocationLink, + makeParameterType, + stripBlacklistedExpressions, + syntaxNode, +} from './helpers.js' import { getLanguage } from './languages.js' import { Language, @@ -136,7 +141,10 @@ language: ${source.languageName} private parse(source: Source): TreeSitterTree { let tree: TreeSitterTree | undefined = this.treeByContent.get(source) if (!tree) { - this.treeByContent.set(source, (tree = this.parserAdapter.parser.parse(source.content))) + // This is currently necessary since the tree-sitter parser currently errors on certain expressions + const content = stripBlacklistedExpressions(source.content, source.languageName) + tree = this.parserAdapter.parser.parse(content) + this.treeByContent.set(source, tree) } return tree } diff --git a/src/language/helpers.ts b/src/language/helpers.ts index 69c12bf4..6dadfca7 100644 --- a/src/language/helpers.ts +++ b/src/language/helpers.ts @@ -1,7 +1,13 @@ import { ParameterType, RegExps } from '@cucumber/cucumber-expressions' import { DocumentUri, LocationLink, Range } from 'vscode-languageserver-types' -import { Link, NodePredicate, TreeSitterQueryMatch, TreeSitterSyntaxNode } from './types' +import { + LanguageName, + Link, + NodePredicate, + TreeSitterQueryMatch, + TreeSitterSyntaxNode, +} from './types' export function syntaxNode(match: TreeSitterQueryMatch, name: string): TreeSitterSyntaxNode | null { const nodes = syntaxNodes(match, name) @@ -70,3 +76,66 @@ export function filter( function flatten(node: TreeSitterSyntaxNode): TreeSitterSyntaxNode[] { return node.children.reduce((r, o) => [...r, ...flatten(o)], [node]) } + +/** + * + * This constant represents a record of language names that contain lists + * of regular expressions that should be stripped from the content of a file. + */ +export const BLACKLISTED_EXPRESSIONS: { + [key in LanguageName]: RegExp[] +} = { + tsx: [ + /* + * This regular expression matches sequences of decorators applied to a class, + * potentially including type parameters and arguments. + * The regex supports matching these patterns preceding + * an optionally exported class definition. + */ + /(@(\w+)(?:<[^>]+>)?\s*(?:\([^)]*\))?\s*)*(?=\s*(export)*\s+class)/g, + ], + java: [], + c_sharp: [], + php: [], + python: [], + ruby: [], + rust: [], + javascript: [], +} + +/** + * + * Strips blacklisted expressions from the given content. + * + * @param content The content to strip blacklisted expressions from. + * @param languageName The name of the language to use for stripping. + * + * @returns The content with blacklisted expressions stripped. + * + * @example + * + * ```typescript + * const content = + * "@decorator\n" ++ + * "export class Foo {\n" ++ + * "@decorator\n" ++ + * "public bar() { }\n" ++ + * "}" + * + * const strippedContent = stripBlacklistedExpressions(content, 'tsx') + * console.log(strippedContent) + * + * // Output: + * "export class Foo {\n" ++ + * "@decorator\n" ++ + * "public bar() { }\n" ++ + * "}" + * + * ``` + */ +export function stripBlacklistedExpressions(content: string, languageName: LanguageName): string { + return BLACKLISTED_EXPRESSIONS[languageName].reduce( + (acc, regExp) => acc.replace(regExp, ''), + content + ) +}