diff --git a/packages/core/src/transform/diagnostics/augmentation.ts b/packages/core/src/transform/diagnostics/augmentation.ts index 97228a457..37c9103ef 100644 --- a/packages/core/src/transform/diagnostics/augmentation.ts +++ b/packages/core/src/transform/diagnostics/augmentation.ts @@ -1,9 +1,9 @@ import type ts from 'typescript'; import { Diagnostic } from './index.js'; import GlimmerASTMappingTree, { MappingSource } from '../template/glimmer-ast-mapping-tree.js'; - +import TransformedModule from '../template/transformed-module.js'; export function augmentDiagnostics( - transformedModule: any, + transformedModule: TransformedModule, diagnostics: T[], ): T[] { const mappingForDiagnostic = (diagnostic: ts.Diagnostic): GlimmerASTMappingTree | null => { @@ -26,8 +26,31 @@ export function augmentDiagnostics( return rangeWithMappingAndSource.mapping || null; }; - // @ts-expect-error not sure how to fix - return diagnostics.map((diagnostic) => rewriteMessageText(diagnostic, mappingForDiagnostic)); + const augmentedDiagnostics: T[] = []; + + for (const diagnostic of diagnostics) { + const augmentedDiagnostic = rewriteMessageText(diagnostic, mappingForDiagnostic); + + const mapping = mappingForDiagnostic(diagnostic); + + if (mapping) { + const appliedDirective = transformedModule.directives.find( + (directive) => + directive.areaOfEffect.start <= augmentedDiagnostic.start! && + directive.areaOfEffect.end > augmentedDiagnostic.start!, + ); + + if (appliedDirective) { + // Filter out this diagnostic; its covered by a directive. + continue; + } + } + + // @ts-expect-error not sure how to fix + augmentedDiagnostics.push(augmentedDiagnostic); + } + + return augmentedDiagnostics; } type DiagnosticHandler = ( diff --git a/packages/core/src/transform/index.ts b/packages/core/src/transform/index.ts index a86007739..3577bf97d 100644 --- a/packages/core/src/transform/index.ts +++ b/packages/core/src/transform/index.ts @@ -2,4 +2,3 @@ export type { Directive, default as TransformedModule } from './template/transfo export type { Diagnostic } from './diagnostics/index.js'; export { rewriteModule } from './template/rewrite-module.js'; -// export { rewriteDiagnostic, createTransformDiagnostic } from './diagnostics/index.js'; diff --git a/packages/core/src/transform/template/inlining/tagged-strings.ts b/packages/core/src/transform/template/inlining/tagged-strings.ts index 5f3353596..f4ead45c2 100644 --- a/packages/core/src/transform/template/inlining/tagged-strings.ts +++ b/packages/core/src/transform/template/inlining/tagged-strings.ts @@ -91,6 +91,15 @@ export function calculateTaggedTemplateSpans( } if (transformedTemplate.result) { + for (let { kind, location, areaOfEffect } of transformedTemplate.result.directives) { + directives.push({ + kind: kind, + source: script, + location: addOffset(location, templateLocation.start), + areaOfEffect: addOffset(areaOfEffect, templateLocation.start), + }); + } + partialSpans.push({ originalFile: script, originalStart: templateLocation.start, diff --git a/packages/core/src/transform/template/map-template-contents.ts b/packages/core/src/transform/template/map-template-contents.ts index b20ee503d..1044afb30 100644 --- a/packages/core/src/transform/template/map-template-contents.ts +++ b/packages/core/src/transform/template/map-template-contents.ts @@ -29,16 +29,17 @@ export type Mapper = { */ rangeForNode: (node: AST.Node, span?: AST.Node['loc']) => Range; + /** + * Given a 0-based line number, returns the corresponding start and + * end offsets for that line. + */ + rangeForLine: (line: number) => Range; + /** * Captures the existence of a directive specified by the given source * node and affecting the given range of text. */ - directive: ( - commentNode: AST.CommentStatement | AST.MustacheCommentStatement, - type: DirectiveKind, - ) => void; - - // directiveTerminatingExpression: (location: Range) => void; + directive: (type: DirectiveKind, location: Range, areaOfEffect: Range) => void; /** * Records an error at the given location. @@ -87,14 +88,6 @@ export type Mapper = { callback: () => void, codeFeaturesForNode?: CodeInformation, ): void; - - /** - * This needs to be called after any node that "consumes" a `glint-expect-error` directive. - * This essentially marks the end of the area of effect for the directive; this helps us - * filter out the "unused ts-expect-error" placeholder diagnostic if, in fact, an error - * diagnostic was reported within the directive's area of effect. - */ - terminateDirectiveAreaOfEffect(endStr: string): void; }; type LocalDirective = Omit; @@ -184,13 +177,6 @@ export function mapTemplateContents( }); let ignoreErrors = false; - let isNoCheckDirectivePresent = false; - let expectErrorToken: - | { - numErrors: number; - commentNode: AST.CommentStatement | AST.MustacheCommentStatement; - } - | undefined; // Associates all content emitted during the given callback with the // given range in the template source and corresponding AST node. @@ -269,23 +255,6 @@ export function mapTemplateContents( verification: false, }; } - - if (expectErrorToken) { - // We are currently in a region of code covered by a @glint-expect-error directive. We need to - // keep track of the number of errors encountered within this region so that we can know whether - // we will need to propagate an "unused ts-expect-error" diagnostic back to the original - // .gts file or not. - const token = expectErrorToken; - return { - ...features, - verification: { - shouldReport: () => { - token.numErrors++; - return false; - }, - }, - }; - } } return features; } @@ -335,112 +304,16 @@ export function mapTemplateContents( errors.push({ message, location }); }, - directive( - commentNode: AST.CommentStatement | AST.MustacheCommentStatement, - kind: DirectiveKind, - ) { - if (kind === 'expect-error') { - if (!expectErrorToken) { - mapper.text(`// @glint-expect-error BEGIN AREA_OF_EFFECT`); - mapper.newline(); - } - - expectErrorToken = { - numErrors: 0, - commentNode, - }; - } - - if (kind === 'ignore') { - ignoreErrors = true; - mapper.text(`// @glint-ignore BEGIN AREA_OF_EFFECT`); - mapper.newline(); - } - - if (kind === 'nocheck') { - ignoreErrors = true; - isNoCheckDirectivePresent = true; - mapper.text(`// @glint-nocheck`); - mapper.newline(); - } - - directives.push({ kind }); - }, - - terminateDirectiveAreaOfEffect(endStr: string) { - if (expectErrorToken) { - // There is an active "@glint-expect-error" directive whose - // are of effect we need to terminate. - // - // There is a somewhat delicate order in which everything below needs to happen, - // but here is an outline: - // - // 1. Volar will call the `shouldReport` function of the `verification` object - // of the `CodeInformation` object that we pass along with each mapping for - // every diagnostic reported by TS within the transformed region of code. - // - // 2. This callback's main job is to return a boolean indicating whether we - // should propagate TS diagnostics within the transformed region of code - // back to search (e.g. the original .gts file). But in addition to that we are somewhat - // hackishly using `shouldReport` to track the number of errors encountered - // within the directive's area of effect so that we can later determine - // whether to filter out the "unused ts-expect-error" placeholder diagnostic - // that we emit below. - // - // 3. The first `shouldReport` that gets called by Volar is in `resolveCodeFeatures`; - // this implementation of `shouldReport` increments `numErrors` for each diagnostic - // found in the region. - // - // 4. The second `shouldReport` that gets called is below: we emit a - // `// @ts-expect-error GLINT_PLACEHOLDER` diagnostic that is always triggering - // within the transformed code, and we use `shouldReport` to decide whether - // to filter out that diagnostic or not. - // - // This approach was taken from Vue tooling; it is complicated but it solves the problem - // of keeping the code transform static while keeping all of the dynamic/stateful - // error tracking and filtering logic in `shouldReport` callbacks. - const token = expectErrorToken; - - mapper.newline(); - - // 1. Emit a ts-expect-error this is guaranteed to trigger within the generated TS code - // because we immediately follow it up with an empty semi-colon statement. - // 2. Map it back to the original `{{ @glint-expect-error }}` comment node in the source. - mapper.forNode( - token.commentNode, - () => { - mapper.text(`// @ts-expect-error GLINT_PLACEHOLDER`); - }, - { - verification: { - // If no diagnostic errors were encountered within the area of effect, - // then filter out the "unused ts-expect-error" diagnostic triggered by our - // placeholder @ts-expect-error - shouldReport: () => token.numErrors === 0, - }, - }, - ); - - // Make the above placeholder diagnostic trigger an "unused ts-expect-error" diagnostic - // by introducing an error-less empty semi-colon statement. - mapper.newline(); - mapper.text(';'); - mapper.newline(); - - expectErrorToken = undefined; - - mapper.text(`// @glint-expect-error END AREA_OF_EFFECT for ${endStr}`); - mapper.newline(); - } - - if (ignoreErrors && !isNoCheckDirectivePresent) { - ignoreErrors = false; - mapper.text(`// @glint-ignore END AREA_OF_EFFECT for ${endStr}`); - mapper.newline(); - } + directive(kind: DirectiveKind, location: Range, areaOfEffect: Range) { + directives.push({ kind, location, areaOfEffect }); }, rangeForNode: buildRangeForNode(lineOffsets), + + rangeForLine: (line: number): Range => ({ + start: lineOffsets[line], + end: lineOffsets[line + 1] ?? template.length, + }), }; callback(ast, mapper); diff --git a/packages/core/src/transform/template/template-to-typescript.ts b/packages/core/src/transform/template/template-to-typescript.ts index 8b6781e51..d71d95970 100644 --- a/packages/core/src/transform/template/template-to-typescript.ts +++ b/packages/core/src/transform/template/template-to-typescript.ts @@ -4,8 +4,6 @@ import { EmbeddingSyntax, mapTemplateContents, RewriteResult } from './map-templ import ScopeStack from './scope-stack.js'; import { GlintEmitMetadata, GlintSpecialForm } from '@glint/core/config-types'; import { TextContent } from './glimmer-ast-mapping-tree.js'; -import { Directive } from './transformed-module.js'; -import { DirectiveKind } from './transformed-module.js'; const SPLATTRIBUTES = '...attributes'; @@ -68,10 +66,6 @@ export function templateToTypescript( } }); - // Ensure any "@glint-expect-error" directives at the end of the template - // trigger "unused @glint-expect-error" diagnostics. - mapper.terminateDirectiveAreaOfEffect('endOfTemplate'); - return; function emitTopLevelStatement(node: AST.TopLevelStatement): void { @@ -169,7 +163,6 @@ export function templateToTypescript( return mapper.nothing(node); } - // here emitDirective(match, node); } @@ -180,11 +173,9 @@ export function templateToTypescript( let kind = match[1]; let location = rangeForNode(node); if (kind === 'ignore' || kind === 'expect-error') { - // Push to the directives array on the record - mapper.directive(node, kind); + mapper.directive(kind, location, mapper.rangeForLine(node.loc.end.line + 1)); } else if (kind === 'nocheck') { - // Push to the directives array on the record - mapper.directive(node, 'nocheck'); + mapper.directive('ignore', location, { start: 0, end: template.length - 1 }); } else if (kind === 'in-svg') { inHtmlContext = 'svg'; } else if (kind === 'in-mathml') { @@ -201,7 +192,6 @@ export function templateToTypescript( emitMustacheStatement(node, 'top-level'); mapper.text(';'); mapper.newline(); - mapper.terminateDirectiveAreaOfEffect('topLevelMustacheStatement'); } // Captures the context in which a given invocation (i.e. a mustache or @@ -622,77 +612,13 @@ export function templateToTypescript( } } - /** - * Given an ElementNode, return an array of attributes, args, and modifiers - * in the order they appear in the element open tag, and filter out any - * comments, while also detecting any directives (e.g. `@glint-expect-error`) - * and assigning them to the next non-comment/non-directive piece. - * - * This is useful for implementing logic for supporting `@glint-expect-error` (and similar) - * directives that appear inline within the opening tag of an element, e.g. - * - * - */ - function assignDirectivesToElementOpenTagPieces( - node: AST.ElementNode, - ): WeakMap { - let pieces = [...node.attributes, ...node.modifiers, ...node.comments].sort( - (a, b) => a.loc.getStart().offset! - b.loc.getStart().offset!, - ); - - let activeDirective: DirectiveKind | null = null; - - const result: WeakMap = new WeakMap(); - - for (let piece of pieces) { - if (piece.type === 'MustacheCommentStatement') { - // this needs to categorize directives. But which while do we do that in? this file is template-to-typescript - // activeDirective = piece.value.includes('@glint-expect-error') ? 'expect-error' : null; - - const directiveRegex = /^@glint-([a-z-]+)/i; - let text = piece.value.trim(); - let match = directiveRegex.exec(text); - if (!match) { - // Just a comment, not a directive. Skip. - continue; - } - - let directive = match[1]; - switch (directive) { - case 'expect-error': - activeDirective = 'expect-error'; - break; - case 'ignore': - activeDirective = 'ignore'; - break; - default: - // TODO: should this be an error? - continue; - } - } else if (piece.type === 'ElementModifierStatement' || piece.type === 'AttrNode') { - if (activeDirective) { - // Assign the directive to this modifier. - result.set(piece, activeDirective); - activeDirective = null; - } - } else { - throw new Error('Unknown piece type'); - } - } - - return result; - } - function emitComponent(node: AST.ElementNode): void { mapper.forNode(node, () => { let { start, path, kind } = tagNameToPathContents(node); - const directivesWeakMap = assignDirectivesToElementOpenTagPieces(node); + for (let comment of node.comments) { + emitComment(comment); + } mapper.text('{'); mapper.newline(); @@ -721,13 +647,6 @@ export function templateToTypescript( mapper.forNode(attr, () => { mapper.newline(); - // If this attribute has an inline directive, emit the corresponding `@ts-...` directive. - const directive = directivesWeakMap.get(attr); - if (directive) { - mapper.text(`// @ts-${directive}`); - mapper.newline(); - } - start = template.indexOf(attr.name, start + 1); emitHashKey(attr.name.slice(1), start + 1); mapper.text(': '); @@ -758,28 +677,14 @@ export function templateToTypescript( mapper.text('));'); mapper.newline(); - emitAttributesAndModifiers(node, directivesWeakMap); - - // terminate @glint-expect-error directives after opening tag; any - // diagnostics due to attributes or modifiers are covered by the directive - mapper.terminateDirectiveAreaOfEffect('emitComponent - end of opening tag'); + emitAttributesAndModifiers(node); if (!node.selfClosing) { let blocks = determineBlockChildren(node); if (blocks.type === 'named') { for (const child of blocks.children) { if (child.type === 'CommentStatement' || child.type === 'MustacheCommentStatement') { - /** - * TODO: figure out what needs to be reinstate here for glint-expect-error, e.g. - * - * - * {{!@glint-expect-error this component isn't typed to provide block params but definitely does }} - * <:footer as |footerArgs|> - * {{footerArgs.something}} - * - * - */ - // emitComment(child); + emitComment(child); continue; } @@ -883,8 +788,6 @@ export function templateToTypescript( function emitPlainElement(node: AST.ElementNode): void { mapper.forNode(node, () => { - const directivesWeakMap = assignDirectivesToElementOpenTagPieces(node); - if (node.tag === 'svg') { inHtmlContext = 'svg'; } @@ -893,6 +796,10 @@ export function templateToTypescript( inHtmlContext = 'math'; } + for (let comment of node.comments) { + emitComment(comment); + } + mapper.text('{'); mapper.newline(); mapper.indent(); @@ -910,11 +817,7 @@ export function templateToTypescript( mapper.text('");'); mapper.newline(); - emitAttributesAndModifiers(node, directivesWeakMap); - - // terminate @glint-expect-error directives after opening tag; any - // diagnostics due to attributes or modifiers are covered by the directive - mapper.terminateDirectiveAreaOfEffect('emitPlainElement - end of opening tag'); + emitAttributesAndModifiers(node); for (let child of node.children) { emitTopLevelStatement(child); @@ -930,19 +833,13 @@ export function templateToTypescript( }); } - function emitAttributesAndModifiers( - node: AST.ElementNode, - directivesWeakMap: WeakMap, - ): void { + function emitAttributesAndModifiers(node: AST.ElementNode): void { emitSplattributes(node); - emitPlainAttributes(node, directivesWeakMap); - emitModifiers(node, directivesWeakMap); + emitPlainAttributes(node); + emitModifiers(node); } - function emitPlainAttributes( - node: AST.ElementNode, - directivesWeakMap: WeakMap, - ): void { + function emitPlainAttributes(node: AST.ElementNode): void { let attributes = node.attributes.filter( (attr) => !attr.name.startsWith('@') && attr.name !== SPLATTRIBUTES, ); @@ -975,8 +872,6 @@ export function templateToTypescript( mapper.text(','); mapper.newline(); }); - - mapper.terminateDirectiveAreaOfEffect('emitPlainAttributes'); } mapper.newline(); }); @@ -1000,14 +895,9 @@ export function templateToTypescript( }); mapper.newline(); - - mapper.terminateDirectiveAreaOfEffect('emitSplattributes'); } - function emitModifiers( - node: AST.ElementNode, - directivesWeakMap: WeakMap, - ): void { + function emitModifiers(node: AST.ElementNode): void { for (let modifier of node.modifiers) { mapper.forNode(modifier, () => { // TODO: implement for modifiers @@ -1019,8 +909,6 @@ export function templateToTypescript( mapper.text('));'); mapper.newline(); }); - - mapper.terminateDirectiveAreaOfEffect('emitModifiers'); } } diff --git a/packages/core/src/transform/template/transformed-module.ts b/packages/core/src/transform/template/transformed-module.ts index dfa68a341..7649fe1ac 100644 --- a/packages/core/src/transform/template/transformed-module.ts +++ b/packages/core/src/transform/template/transformed-module.ts @@ -30,6 +30,8 @@ export type DirectiveKind = 'ignore' | 'expect-error' | 'nocheck'; export type Directive = { kind: DirectiveKind; source: SourceFile; + location: Range; + areaOfEffect: Range; }; export type TransformError = { diff --git a/test-packages/package-test-core/__tests__/transform/offset-mapping.test.ts b/test-packages/package-test-core/__tests__/transform/offset-mapping.test.ts deleted file mode 100644 index 382bfef9a..000000000 --- a/test-packages/package-test-core/__tests__/transform/offset-mapping.test.ts +++ /dev/null @@ -1,634 +0,0 @@ -import { rewriteModule, TransformedModule } from '@glint/core/transform/index'; -import { stripIndent } from 'common-tags'; -import { describe, test, expect } from 'vitest'; -import { Range, SourceFile } from '@glint/core/transform/template/transformed-module'; -import * as ts from 'typescript'; -import { assert } from '@glint/core/transform/util'; -import { GlintEnvironment } from '@glint/core/config/index'; - -const emberLooseEnvironment = GlintEnvironment.load('ember-loose'); -const emberTemplateImportsEnvironment = GlintEnvironment.load('ember-template-imports'); - -// Skipping because Volar source mapping dictates this move elsewhere, not sure how to do it yet. -describe.skip('Transform: Source-to-source offset mapping', () => { - type RewrittenTestModule = { - source: SourceFile; - transformedModule: TransformedModule; - }; - - function rewriteInlineTemplate({ contents }: { contents: string }): RewrittenTestModule { - let script = { - filename: 'test.gts', - contents: stripIndent` - import Component from '@glimmer/component'; - - export default class MyComponent extends Component { - - } - `, - }; - - let transformedModule = rewriteModule(ts, { script }, emberTemplateImportsEnvironment); - if (!transformedModule) { - throw new Error('Expected module to have rewritten contents'); - } - - return { source: script, transformedModule }; - } - - function rewriteCompanionTemplate({ - contents, - backing, - }: { - contents: string; - backing: 'class' | 'opaque' | 'none'; - }): RewrittenTestModule { - let script = { - filename: 'script.ts', - contents: - backing === 'class' - ? 'export default class MyComponent {}' - : backing === 'opaque' - ? 'export default templateOnly();' - : '', - }; - - let template = { - filename: 'test.hbs', - contents: contents, - }; - - let transformedModule = rewriteModule(ts, { script, template }, emberLooseEnvironment); - if (!transformedModule) { - throw new Error('Expected module to have rewritten contents'); - } - - return { source: template, transformedModule }; - } - - function findOccurrence(haystack: string, needle: string, occurrence: number): number { - let offset = haystack.indexOf('function(__glintRef__'); - for (let i = 0; i < occurrence + 1; i++) { - offset = haystack.indexOf(needle, offset + 1); - - if (offset === -1) { - throw new Error(`Couldn't find occurrence #${i} of ${needle}`); - } - } - return offset; - } - - function expectTokenMapping( - rewrittenTestModule: RewrittenTestModule, - originalToken: string, - { transformedToken = originalToken, occurrence = 0 } = {}, - ): void { - let originalSource = rewrittenTestModule.source.contents; - let transformedContents = rewrittenTestModule.transformedModule.transformedContents; - let originalOffset = findOccurrence(originalSource, originalToken, occurrence); - let transformedOffset = findOccurrence(transformedContents, transformedToken, occurrence); - - expectRangeMapping( - rewrittenTestModule, - { - start: originalOffset, - end: originalOffset + originalToken.length, - }, - { - start: transformedOffset, - end: transformedOffset + transformedToken.length, - }, - ); - } - - function expectRangeMapping( - rewrittenTestModule: RewrittenTestModule, - originalRange: Range, - transformedRange: Range, - ): void { - let { transformedModule, source } = rewrittenTestModule; - expect(transformedModule.getOriginalOffset(transformedRange.start)).toEqual({ - offset: originalRange.start, - source, - }); - expect(transformedModule.getTransformedOffset(source.filename, originalRange.start)).toEqual( - transformedRange.start, - ); - - let calculatedTransformedRange = transformedModule.getTransformedRange( - source.filename, - originalRange.start, - originalRange.end, - ); - - expect(calculatedTransformedRange.start).toEqual(transformedRange.start); - expect(calculatedTransformedRange.end).toEqual(transformedRange.end); - - let calculatedOriginalRange = transformedModule.getOriginalRange( - transformedRange.start, - transformedRange.end, - ); - - expect(calculatedOriginalRange.source).toBe(rewrittenTestModule.source); - expect(calculatedOriginalRange.start).toEqual(originalRange.start); - expect(calculatedOriginalRange.end).toEqual(originalRange.end); - } - - describe('standalone companion template', () => { - describe('path segments', () => { - test('simple path', () => { - let module = rewriteCompanionTemplate({ backing: 'class', contents: '{{foo.bar}}' }); - expectTokenMapping(module, 'foo'); - expectTokenMapping(module, 'bar'); - }); - - test('paths with repeated subsequences', () => { - let module = rewriteCompanionTemplate({ - backing: 'class', - contents: '{{this.tabState.tab}}', - }); - expectTokenMapping(module, 'this'); - expectTokenMapping(module, 'tabState'); - expectTokenMapping(module, 'tab', { occurrence: 1 }); - }); - }); - - test.each(['class', 'opaque', 'none'] as const)('with backing expression: %s', (backing) => { - let module = rewriteCompanionTemplate({ backing, contents: '{{@foo}}' }); - expectTokenMapping(module, 'foo'); - }); - - test('Windows line endings', () => { - let module = rewriteCompanionTemplate({ - backing: 'none', - contents: `Hello, !\r\n\r\n{{this.foo}}\r\n`, - }); - - expectTokenMapping(module, 'World'); - expectTokenMapping(module, 'foo'); - }); - }); - - describe('inline template', () => { - describe('path segments', () => { - test('simple in-scope paths', () => { - let module = rewriteInlineTemplate({ - contents: '{{foo.bar}}', - }); - expectTokenMapping(module, 'foo'); - expectTokenMapping(module, 'bar'); - }); - - test('simple out-of-scope paths', () => { - let module = rewriteInlineTemplate({ contents: '{{foo.bar}}' }); - expectTokenMapping(module, 'foo'); - expectTokenMapping(module, 'bar'); - }); - - test('arg paths', () => { - let module = rewriteInlineTemplate({ contents: '{{@foo.bar}}' }); - expectTokenMapping(module, 'foo'); - expectTokenMapping(module, 'bar'); - }); - - test('this paths', () => { - let module = rewriteInlineTemplate({ contents: '{{this.foo.bar}}' }); - expectTokenMapping(module, 'foo'); - expectTokenMapping(module, 'bar'); - expectTokenMapping(module, 'this'); - }); - - test('paths with repeated subsequences', () => { - let module = rewriteInlineTemplate({ contents: '{{this.tabState.tab}}' }); - expectTokenMapping(module, 'this'); - expectTokenMapping(module, 'tabState'); - expectTokenMapping(module, 'tab', { occurrence: 1 }); - }); - }); - - describe('keys', () => { - test('named params to mustaches', () => { - let module = rewriteInlineTemplate({ - contents: '{{foo bar=hello}}', - }); - expectTokenMapping(module, 'foo'); - expectTokenMapping(module, 'bar'); - expectTokenMapping(module, 'hello'); - }); - - test('named spinal-case params to mustaches', () => { - let module = rewriteInlineTemplate({ - contents: '{{foo bar-baz=hello}}', - }); - expectTokenMapping(module, 'foo'); - expectTokenMapping(module, 'bar-baz'); - expectTokenMapping(module, 'hello'); - }); - - test('component args', () => { - let module = rewriteInlineTemplate({ - contents: '', - }); - expectTokenMapping(module, 'Foo'); - expectTokenMapping(module, 'bar'); - expectTokenMapping(module, 'hello'); - }); - - test('spinal-case component args', () => { - let module = rewriteInlineTemplate({ - contents: '', - }); - expectTokenMapping(module, 'Foo'); - expectTokenMapping(module, 'bar-baz'); - expectTokenMapping(module, 'hello'); - }); - - test('named blocks', () => { - let module = rewriteInlineTemplate({ - contents: '<:blockName>hi', - }); - expectTokenMapping(module, 'Foo'); - expectTokenMapping(module, 'blockName'); - }); - - test('spinal-case named blocks', () => { - let module = rewriteInlineTemplate({ - contents: '<:block-name>hi', - }); - expectTokenMapping(module, 'Foo'); - expectTokenMapping(module, 'block-name'); - }); - }); - - describe('block params', () => { - test('curly params', () => { - let module = rewriteInlineTemplate({ - contents: stripIndent` - {{#each this.items as |num|}} - #{{num}} - {{/each}} - `, - }); - - expectTokenMapping(module, 'each', { occurrence: 0 }); - expectTokenMapping(module, 'this'); - expectTokenMapping(module, 'items'); - expectTokenMapping(module, 'num', { occurrence: 0 }); - expectTokenMapping(module, 'num', { occurrence: 1 }); - expectTokenMapping(module, 'each', { occurrence: 1 }); - }); - - test('angle bracket params', () => { - let module = rewriteInlineTemplate({ - contents: '', - }); - expectTokenMapping(module, 'Foo'); - expectTokenMapping(module, 'bar'); - expectTokenMapping(module, 'baz'); - }); - }); - - describe('block mustaches', () => { - test('simple identifiers', () => { - let module = rewriteInlineTemplate({ - contents: '{{#foo}}{{/foo}}', - }); - expectTokenMapping(module, 'foo', { occurrence: 0 }); - expectTokenMapping(module, 'foo', { occurrence: 1 }); - }); - - test('simple paths', () => { - let module = rewriteInlineTemplate({ - contents: '{{#foo.bar}}{{/foo.bar}}', - }); - expectTokenMapping(module, 'foo', { occurrence: 0 }); - expectTokenMapping(module, 'foo', { occurrence: 1 }); - expectTokenMapping(module, 'bar', { occurrence: 0 }); - expectTokenMapping(module, 'bar', { occurrence: 1 }); - }); - - test('arg paths', () => { - let module = rewriteInlineTemplate({ contents: '{{#@foo.bar}}{{/@foo.bar}}' }); - expectTokenMapping(module, 'foo', { occurrence: 0 }); - expectTokenMapping(module, 'foo', { occurrence: 1 }); - expectTokenMapping(module, 'bar', { occurrence: 0 }); - expectTokenMapping(module, 'bar', { occurrence: 1 }); - }); - - test('this paths', () => { - let module = rewriteInlineTemplate({ contents: '{{#this.bar}}{{/this.bar}}' }); - expectTokenMapping(module, 'this', { occurrence: 0 }); - expectTokenMapping(module, 'this', { occurrence: 1 }); - expectTokenMapping(module, 'bar', { occurrence: 0 }); - expectTokenMapping(module, 'bar', { occurrence: 1 }); - }); - }); - - describe('angle bracket components', () => { - test('simple identifiers', () => { - let module = rewriteInlineTemplate({ - contents: '', - }); - expectTokenMapping(module, 'Foo', { occurrence: 0 }); - expectTokenMapping(module, 'Foo', { occurrence: 1 }); - }); - - test('simple paths', () => { - let module = rewriteInlineTemplate({ - contents: '', - }); - expectTokenMapping(module, 'foo', { occurrence: 0 }); - expectTokenMapping(module, 'foo', { occurrence: 1 }); - expectTokenMapping(module, 'bar', { occurrence: 0 }); - expectTokenMapping(module, 'bar', { occurrence: 1 }); - }); - - test('arg paths', () => { - let module = rewriteInlineTemplate({ contents: '<@foo.bar>' }); - expectTokenMapping(module, 'foo', { occurrence: 0 }); - expectTokenMapping(module, 'foo', { occurrence: 1 }); - expectTokenMapping(module, 'bar', { occurrence: 0 }); - expectTokenMapping(module, 'bar', { occurrence: 1 }); - }); - - test('this paths', () => { - let module = rewriteInlineTemplate({ contents: '' }); - expectTokenMapping(module, 'this', { occurrence: 0 }); - expectTokenMapping(module, 'this', { occurrence: 1 }); - expectTokenMapping(module, 'bar', { occurrence: 0 }); - expectTokenMapping(module, 'bar', { occurrence: 1 }); - }); - - test('args with repeated subsequences', () => { - let module = rewriteInlineTemplate({ - contents: '', - }); - - expectTokenMapping(module, 'Foo'); - expectTokenMapping(module, 'nameThatIsLong'); - expectTokenMapping(module, 'name', { occurrence: 1 }); - }); - }); - - describe('location inference stress tests', () => { - test('matching in/out arg names', () => { - let { source, transformedModule } = - rewriteInlineTemplate({ - contents: stripIndent` - - `, - }) ?? {}; - - let passedArgSourceOffset = source.contents.indexOf('{{@a}}') + '{{@'.length; - let passedArgTransformedOffset = - transformedModule.transformedContents.indexOf('args.a') + 'args.'.length; - - expect(transformedModule.getOriginalOffset(passedArgTransformedOffset).offset).toBe( - passedArgSourceOffset, - ); - - expect(transformedModule.getTransformedOffset(source.filename, passedArgSourceOffset)).toBe( - passedArgTransformedOffset, - ); - - let argNameSourceOffset = source.contents.indexOf('@a=') + '@'.length; - let argNameTransformedOffset = - transformedModule.transformedContents.indexOf(', a:') + ', '.length; - - expect(transformedModule.getOriginalOffset(argNameTransformedOffset).offset).toBe( - argNameSourceOffset, - ); - - expect(transformedModule.getTransformedOffset(source.filename, argNameSourceOffset)).toBe( - argNameTransformedOffset, - ); - }); - - test('arg name subsequences', () => { - let { source, transformedModule } = - rewriteInlineTemplate({ contents: `{{foo aa="qwe" a="qwe"}}` }) ?? {}; - - let doubleASourceOFfset = source.contents.indexOf('aa='); - let doubleATransformedOffset = transformedModule.transformedContents.indexOf('aa:'); - - expect(transformedModule.getOriginalOffset(doubleATransformedOffset).offset).toBe( - doubleASourceOFfset, - ); - - expect(transformedModule.getTransformedOffset(source.filename, doubleASourceOFfset)).toBe( - doubleATransformedOffset, - ); - - let singleASourceOffset = source.contents.indexOf(' a=') + ' '.length; - let singleATransformedOFfset = - transformedModule.transformedContents.indexOf(' a:') + ' '.length; - - expect(transformedModule.getOriginalOffset(singleATransformedOFfset).offset).toBe( - singleASourceOffset, - ); - - expect(transformedModule.getTransformedOffset(source.filename, singleASourceOffset)).toBe( - singleATransformedOFfset, - ); - }); - }); - - test('Windows line endings', () => { - let module = rewriteInlineTemplate({ - contents: `Hello, !\r\n\r\n{{this.foo}}\r\n`, - }); - - expectTokenMapping(module, 'World'); - expectTokenMapping(module, 'foo'); - }); - }); - - describe('spans outside of mapped segments', () => { - const source = { - filename: 'test.gts', - contents: stripIndent` - import Component from '@glimmer/component'; - - // start - export default class MyComponent extends Component { - - } - // end - - export class Greeting extends Component { - - } - `, - }; - - const rewritten = rewriteModule(ts, { script: source }, emberTemplateImportsEnvironment)!; - - test('bounds that cross a rewritten span', () => { - let originalStart = source.contents.indexOf('// start'); - let originalEnd = source.contents.indexOf('// end'); - - let transformedStart = rewritten.transformedContents.indexOf('// start'); - let transformedEnd = rewritten.transformedContents.indexOf('// end'); - - expect(rewritten.getOriginalRange(transformedStart, transformedEnd)).toEqual({ - start: originalStart, - end: originalEnd, - source, - }); - - expect(rewritten.getTransformedRange(source.filename, originalStart, originalEnd)).toEqual({ - start: transformedStart, - end: transformedEnd, - }); - }); - - test('full file bounds', () => { - let originalEnd = source.contents.length - 1; - let transformedEnd = rewritten.transformedContents.length - 1; - - expect(rewritten.getOriginalOffset(transformedEnd)).toEqual({ - source, - offset: originalEnd, - }); - expect(rewritten.getOriginalRange(0, transformedEnd)).toEqual({ - start: 0, - end: originalEnd, - source, - }); - - expect(rewritten.getTransformedOffset(source.filename, originalEnd)).toEqual(transformedEnd); - expect(rewritten.getTransformedRange(source.filename, 0, originalEnd)).toEqual({ - start: 0, - end: transformedEnd, - }); - }); - }); -}); - -describe.skip('Diagnostic offset mapping', () => { - const transformedContentsFile = { fileName: 'transformed' } as ts.SourceFile; - const source = { - filename: 'test.gts', - contents: stripIndent` - import Component from '@glimmer/component'; - export default class MyComponent extends Component { - - } - `, - }; - - const transformedModule = rewriteModule(ts, { script: source }, emberTemplateImportsEnvironment); - assert(transformedModule); - - test('without related information', () => { - let category = ts.DiagnosticCategory.Error; - let messageText = '`foo` is no good'; - let code = 1234; - - let original: ts.DiagnosticWithLocation = { - category, - code, - messageText, - file: transformedContentsFile, - start: transformedModule.transformedContents.indexOf('foo'), - length: 3, - }; - - let rewritten = rewriteDiagnostic(ts, original, () => transformedModule); - - expect(rewritten).toMatchObject({ - category, - code, - messageText, - start: source.contents.indexOf('foo'), - length: 3, - }); - }); - - test('with related information', () => { - let category = ts.DiagnosticCategory.Error; - let messageText = '`bar` is no good'; - let relatedMessageText = '`bar` was defined here'; - let code = 1234; - - let original: ts.DiagnosticWithLocation = { - category, - code, - file: transformedContentsFile, - start: transformedModule.transformedContents.indexOf('(bar') + 1, - length: 3, - messageText, - relatedInformation: [ - { - category, - code, - file: transformedContentsFile, - messageText: relatedMessageText, - start: transformedModule.transformedContents.indexOf('[bar]') + 1, - length: 3, - }, - ], - }; - - let rewritten = rewriteDiagnostic(ts, original, () => transformedModule); - - expect(rewritten).toMatchObject({ - category, - code, - messageText, - start: source.contents.indexOf(' bar') + 1, - length: 3, - relatedInformation: [ - { - category, - code, - messageText: relatedMessageText, - start: source.contents.indexOf('|bar|') + 1, - length: 3, - }, - ], - }); - }); - - test('with a companion template', () => { - let script = { filename: 'test.ts', contents: '' }; - let template = { - filename: 'test.hbs', - contents: stripIndent` - {{foo-bar type 'in'}} - `, - }; - - let transformedModule = rewriteModule(ts, { script, template }, emberLooseEnvironment)!; - let category = ts.DiagnosticCategory.Error; - let messageText = '`foo-bar` is no good'; - let code = 1234; - - let original: ts.DiagnosticWithLocation = { - category, - code, - messageText, - file: { fileName: 'test.ts' } as ts.SourceFile, - start: transformedModule?.transformedContents.indexOf('foo-bar'), - length: 7, - }; - - let rewritten = rewriteDiagnostic(ts, original, () => transformedModule); - - expect(rewritten).toMatchObject({ - category, - code, - messageText, - start: template.contents.indexOf('foo-bar'), - length: 7, - }); - }); -});