Revision f24f74eb8908ceb955b013547908dc83b0231772 authored by Babak K. Shandiz on 16 August 2022, 16:27:29 UTC, committed by GitHub on 16 August 2022, 16:27:29 UTC
* ⚗️ Add test to verify code fix works when implementing a mapped type with indirect keyof

Signed-off-by: Babak K. Shandiz <babak.k.shandiz@gmail.com>

* 🔨 Add property as implementation for symbols that has no declaration

Signed-off-by: Babak K. Shandiz <babak.k.shandiz@gmail.com>

diff --git a/src/services/codefixes/helpers.ts b/src/services/codefixes/helpers.ts
index 94b64e57..a4c11fd5 100644
--- a/src/services/codefixes/helpers.ts
+++ b/src/services/codefixes/helpers.ts
@@ -60,21 +60,19 @@ namespace ts.codefix {
         isAmbient = false,
     ): void {
         const declarations = symbol.getDeclarations();
-        if (!(declarations && declarations.length)) {
-            return undefined;
-        }
+        const declaration = declarations ? declarations[0] : undefined;
         const checker = context.program.getTypeChecker();
         const scriptTarget = getEmitScriptTarget(context.program.getCompilerOptions());
-        const declaration = declarations[0];
+        const kind = declaration?.kind ?? SyntaxKind.PropertySignature;
         const name = getSynthesizedDeepClone(getNameOfDeclaration(declaration), /*includeTrivia*/ false) as PropertyName;
-        const visibilityModifier = createVisibilityModifier(getEffectiveModifierFlags(declaration));
+        const visibilityModifier = createVisibilityModifier(declaration ? getEffectiveModifierFlags(declaration) : ModifierFlags.None);
         const modifiers = visibilityModifier ? factory.createNodeArray([visibilityModifier]) : undefined;
         const type = checker.getWidenedType(checker.getTypeOfSymbolAtLocation(symbol, enclosingDeclaration));
         const optional = !!(symbol.flags & SymbolFlags.Optional);
         const ambient = !!(enclosingDeclaration.flags & NodeFlags.Ambient) || isAmbient;
         const quotePreference = getQuotePreference(sourceFile, preferences);

-        switch (declaration.kind) {
+        switch (kind) {
             case SyntaxKind.PropertySignature:
             case SyntaxKind.PropertyDeclaration:
                 const flags = quotePreference === QuotePreference.Single ? NodeBuilderFlags.UseSingleQuotesForStringLiteralType : undefined;
@@ -88,13 +86,16 @@ namespace ts.codefix {
                 }
                 addClassElement(factory.createPropertyDeclaration(
                     modifiers,
-                    name,
+                    declaration ? name : symbol.getName(),
                     optional && (preserveOptional & PreserveOptionalFlags.Property) ? factory.createToken(SyntaxKind.QuestionToken) : undefined,
                     typeNode,
                     /*initializer*/ undefined));
                 break;
             case SyntaxKind.GetAccessor:
             case SyntaxKind.SetAccessor: {
+                if (!declarations) {
+                    break;
+                }
                 let typeNode = checker.typeToTypeNode(type, enclosingDeclaration, /*flags*/ undefined, getNoopSymbolTrackerWithResolver(context));
                 const allAccessors = getAllAccessorDeclarations(declarations, declaration as AccessorDeclaration);
                 const orderedAccessors = allAccessors.secondAccessor
@@ -138,6 +139,10 @@ namespace ts.codefix {
                 // If there is more than one overload but no implementation signature
                 // (eg: an abstract method or interface declaration), there is a 1-1
                 // correspondence of declarations and signatures.
+                if (!declarations) {
+                    break;
+                }
+
                 const signatures = checker.getSignaturesOfType(type, SignatureKind.Call);
                 if (!some(signatures)) {
                     break;

* 🔨 Improve code readability

Signed-off-by: Babak K. Shandiz <babak.k.shandiz@gmail.com>

diff --git a/src/services/codefixes/helpers.ts b/src/services/codefixes/helpers.ts
index 2f5c8703ab..aea0206a8a 100644
--- a/src/services/codefixes/helpers.ts
+++ b/src/services/codefixes/helpers.ts
@@ -60,7 +60,7 @@ namespace ts.codefix {
         isAmbient = false,
     ): void {
         const declarations = symbol.getDeclarations();
-        const declaration = declarations ? declarations[0] : undefined;
+        const declaration = declarations?.[0];
         const checker = context.program.getTypeChecker();
         const scriptTarget = getEmitScriptTarget(context.program.getCompilerOptions());
         const kind = declaration?.kind ?? SyntaxKind.PropertySignature;
@@ -93,9 +93,7 @@ namespace ts.codefix {
                 break;
             case SyntaxKind.GetAccessor:
             case SyntaxKind.SetAccessor: {
-                if (!declarations) {
-                    break;
-                }
+                Debug.assertIsDefined(declarations);
                 let typeNode = checker.typeToTypeNode(type, enclosingDeclaration, /*flags*/ undefined, getNoopSymbolTrackerWithResolver(context));
                 const allAccessors = getAllAccessorDeclarations(declarations, declaration as AccessorDeclaration);
                 const orderedAccessors = allAccessors.secondAccessor
@@ -139,10 +137,7 @@ namespace ts.codefix {
                 // If there is more than one overload but no implementation signature
                 // (eg: an abstract method or interface declaration), there is a 1-1
                 // correspondence of declarations and signatures.
-                if (!declarations) {
-                    break;
-                }
-
+                Debug.assertIsDefined(declarations);
                 const signatures = type.isUnion() ? flatMap(type.types, t => t.getCallSignatures()) : type.getCallSignatures();
                 if (!some(signatures)) {
                     break;

* 📜 Add comment regarding mapped type children's  missing declaration

Signed-off-by: Babak K. Shandiz <babak.k.shandiz@gmail.com>

Signed-off-by: Babak K. Shandiz <babak.k.shandiz@gmail.com>
Co-authored-by: Andrew Branch <andrew@wheream.io>
1 parent 90fa1c7
Raw File
harnessUtils.ts
namespace Utils {
    export function encodeString(s: string): string {
        return ts.sys.bufferFrom!(s).toString("utf8");
    }

    export function byteLength(s: string, encoding?: string): number {
        // stub implementation if Buffer is not available (in-browser case)
        return Buffer.byteLength(s, encoding as ts.BufferEncoding | undefined);
    }

    export function evalFile(fileContents: string, fileName: string, nodeContext?: any) {
        const vm = require("vm");
        if (nodeContext) {
            vm.runInNewContext(fileContents, nodeContext, fileName);
        }
        else {
            vm.runInThisContext(fileContents, fileName);
        }
    }

    /** Splits the given string on \r\n, or on only \n if that fails, or on only \r if *that* fails. */
    export function splitContentByNewlines(content: string) {
        // Split up the input file by line
        // Note: IE JS engine incorrectly handles consecutive delimiters here when using RegExp split, so
        // we have to use string-based splitting instead and try to figure out the delimiting chars
        let lines = content.split("\r\n");
        if (lines.length === 1) {
            lines = content.split("\n");

            if (lines.length === 1) {
                lines = content.split("\r");
            }
        }
        return lines;
    }

    /** Reads a file under /tests */
    export function readTestFile(path: string) {
        if (path.indexOf("tests") < 0) {
            path = "tests/" + path;
        }

        let content: string | undefined;
        try {
            content = Harness.IO.readFile(Harness.userSpecifiedRoot + path);
        }
        catch (err) {
            return undefined;
        }

        return content;
    }

    export function memoize<T extends ts.AnyFunction>(f: T, memoKey: (...anything: any[]) => string): T {
        const cache = new ts.Map<string, any>();

        return (function (this: any, ...args: any[]) {
            const key = memoKey(...args);
            if (cache.has(key)) {
                return cache.get(key);
            }
            else {
                const value = f.apply(this, args);
                cache.set(key, value);
                return value;
            }
        } as any);
    }

    export const canonicalizeForHarness = ts.createGetCanonicalFileName(/*caseSensitive*/ false); // This is done so tests work on windows _and_ linux

    export function assertInvariants(node: ts.Node | undefined, parent: ts.Node | undefined) {
        const queue: [ts.Node | undefined, ts.Node | undefined][] = [[node, parent]];
        for (const [node, parent] of queue) {
            assertInvariantsWorker(node, parent);
        }

        function assertInvariantsWorker(node: ts.Node | undefined, parent: ts.Node | undefined): void {
            if (node) {
                assert.isFalse(node.pos < 0, "node.pos < 0");
                assert.isFalse(node.end < 0, "node.end < 0");
                assert.isFalse(node.end < node.pos, "node.end < node.pos");
                assert.equal(node.parent, parent, "node.parent !== parent");

                if (parent) {
                    // Make sure each child is contained within the parent.
                    assert.isFalse(node.pos < parent.pos, "node.pos < parent.pos");
                    assert.isFalse(node.end > parent.end, "node.end > parent.end");
                }

                ts.forEachChild(node, child => {
                    queue.push([child, node]);
                });

                // Make sure each of the children is in order.
                let currentPos = 0;
                ts.forEachChild(node,
                    child => {
                        assert.isFalse(child.pos < currentPos, "child.pos < currentPos");
                        currentPos = child.end;
                    },
                    array => {
                        assert.isFalse(array.pos < node.pos, "array.pos < node.pos");
                        assert.isFalse(array.end > node.end, "array.end > node.end");
                        assert.isFalse(array.pos < currentPos, "array.pos < currentPos");

                        for (const item of array) {
                            assert.isFalse(item.pos < currentPos, "array[i].pos < currentPos");
                            currentPos = item.end;
                        }

                        currentPos = array.end;
                    });

                const childNodesAndArrays: any[] = [];
                ts.forEachChild(node, child => {
                    childNodesAndArrays.push(child);
                }, array => {
                    childNodesAndArrays.push(array);
                });

                for (const childName in node) {
                    if (childName === "parent" ||
                        childName === "nextContainer" ||
                        childName === "modifiers" ||
                        childName === "externalModuleIndicator" ||
                        // for now ignore jsdoc comments
                        childName === "jsDocComment" ||
                        childName === "checkJsDirective" ||
                        childName === "commonJsModuleIndicator" ||
                        // ignore nodes added only to report grammar errors
                        childName === "illegalInitializer" ||
                        childName === "illegalDecorators" ||
                        childName === "illegalModifiers" ||
                        childName === "illegalQuestionToken" ||
                        childName === "illegalExclamationToken" ||
                        childName === "illegalTypeParameters" ||
                        childName === "illegalType") {
                        continue;
                    }
                    const child = (node as any)[childName];
                    if (isNodeOrArray(child)) {
                        assert.isFalse(childNodesAndArrays.indexOf(child) < 0,
                            "Missing child when forEach'ing over node: " + ts.Debug.formatSyntaxKind(node.kind) + "-" + childName);
                    }
                }
            }
        }
    }

    function isNodeOrArray(a: any): boolean {
        return a !== undefined && typeof a.pos === "number";
    }

    export function convertDiagnostics(diagnostics: readonly ts.Diagnostic[]) {
        return diagnostics.map(convertDiagnostic);
    }

    function convertDiagnostic(diagnostic: ts.Diagnostic) {
        return {
            start: diagnostic.start,
            length: diagnostic.length,
            messageText: ts.flattenDiagnosticMessageText(diagnostic.messageText, Harness.IO.newLine()),
            category: ts.diagnosticCategoryName(diagnostic, /*lowerCase*/ false),
            code: diagnostic.code
        };
    }

    export function sourceFileToJSON(file: ts.Node): string {
        return JSON.stringify(file, (_, v) => isNodeOrArray(v) ? serializeNode(v) : v, "    ");

        function getKindName(k: number | string | undefined): string | undefined {
            if (k === undefined || ts.isString(k)) {
                return k;
            }
            return ts.Debug.formatSyntaxKind(k);
        }

        function getNodeFlagName(f: number) {
            return ts.Debug.formatNodeFlags(f);
        }

        function serializeNode(n: ts.Node): any {
            const o: any = { kind: getKindName(n.kind) };
            if (ts.containsParseError(n)) {
                o.containsParseError = true;
            }

            for (const propertyName of Object.getOwnPropertyNames(n) as readonly (keyof ts.SourceFile | keyof ts.Identifier)[]) {
                switch (propertyName) {
                    case "parent":
                    case "symbol":
                    case "locals":
                    case "localSymbol":
                    case "kind":
                    case "id":
                    case "nodeCount":
                    case "symbolCount":
                    case "identifierCount":
                    case "scriptSnapshot":
                        // Blocklist of items we never put in the baseline file.
                        break;

                    case "originalKeywordKind":
                        o[propertyName] = getKindName((n as any)[propertyName]);
                        break;

                    case "flags":
                        // Clear the flags that are produced by aggregating child values. That is ephemeral
                        // data we don't care about in the dump. We only care what the parser set directly
                        // on the AST.
                        const flags = n.flags & ~(ts.NodeFlags.JavaScriptFile | ts.NodeFlags.HasAggregatedChildData);
                        if (flags) {
                            o[propertyName] = getNodeFlagName(flags);
                        }
                        break;

                    case "parseDiagnostics":
                        o[propertyName] = convertDiagnostics((n as any)[propertyName]);
                        break;

                    case "nextContainer":
                        if (n.nextContainer) {
                            o[propertyName] = { kind: n.nextContainer.kind, pos: n.nextContainer.pos, end: n.nextContainer.end };
                        }
                        break;

                    case "text":
                        // Include 'text' field for identifiers/literals, but not for source files.
                        if (n.kind !== ts.SyntaxKind.SourceFile) {
                            o[propertyName] = (n as any)[propertyName];
                        }
                        break;

                    default:
                        o[propertyName] = (n as any)[propertyName];
                }
            }

            return o;
        }
    }

    export function assertDiagnosticsEquals(array1: readonly ts.Diagnostic[], array2: readonly ts.Diagnostic[]) {
        if (array1 === array2) {
            return;
        }

        assert(array1, "array1");
        assert(array2, "array2");

        assert.equal(array1.length, array2.length, "array1.length !== array2.length");

        for (let i = 0; i < array1.length; i++) {
            const d1 = array1[i];
            const d2 = array2[i];

            assert.equal(d1.start, d2.start, "d1.start !== d2.start");
            assert.equal(d1.length, d2.length, "d1.length !== d2.length");
            assert.equal(
                ts.flattenDiagnosticMessageText(d1.messageText, Harness.IO.newLine()),
                ts.flattenDiagnosticMessageText(d2.messageText, Harness.IO.newLine()), "d1.messageText !== d2.messageText");
            assert.equal(d1.category, d2.category, "d1.category !== d2.category");
            assert.equal(d1.code, d2.code, "d1.code !== d2.code");
        }
    }

    export function assertStructuralEquals(node1: ts.Node, node2: ts.Node) {
        if (node1 === node2) {
            return;
        }

        assert(node1, "node1");
        assert(node2, "node2");
        assert.equal(node1.pos, node2.pos, "node1.pos !== node2.pos");
        assert.equal(node1.end, node2.end, "node1.end !== node2.end");
        assert.equal(node1.kind, node2.kind, "node1.kind !== node2.kind");

        // call this on both nodes to ensure all propagated flags have been set (and thus can be
        // compared).
        assert.equal(ts.containsParseError(node1), ts.containsParseError(node2));
        assert.equal(node1.flags & ~ts.NodeFlags.ReachabilityAndEmitFlags, node2.flags & ~ts.NodeFlags.ReachabilityAndEmitFlags, "node1.flags !== node2.flags");

        ts.forEachChild(node1,
            child1 => {
                const childName = findChildName(node1, child1);
                const child2: ts.Node = (node2 as any)[childName];

                assertStructuralEquals(child1, child2);
            },
            array1 => {
                const childName = findChildName(node1, array1);
                const array2: ts.NodeArray<ts.Node> = (node2 as any)[childName];

                assertArrayStructuralEquals(array1, array2);
            });
    }

    function assertArrayStructuralEquals(array1: ts.NodeArray<ts.Node>, array2: ts.NodeArray<ts.Node>) {
        if (array1 === array2) {
            return;
        }

        assert(array1, "array1");
        assert(array2, "array2");
        assert.equal(array1.pos, array2.pos, "array1.pos !== array2.pos");
        assert.equal(array1.end, array2.end, "array1.end !== array2.end");
        assert.equal(array1.length, array2.length, "array1.length !== array2.length");

        for (let i = 0; i < array1.length; i++) {
            assertStructuralEquals(array1[i], array2[i]);
        }
    }

    function findChildName(parent: any, child: any) {
        for (const name in parent) {
            if (parent.hasOwnProperty(name) && parent[name] === child) {
                return name;
            }
        }

        throw new Error("Could not find child in parent");
    }

    const maxHarnessFrames = 1;

    export function filterStack(error: Error, stackTraceLimit = Infinity) {
        const stack = (error as any).stack as string;
        if (stack) {
            const lines = stack.split(/\r\n?|\n/g);
            const filtered: string[] = [];
            let frameCount = 0;
            let harnessFrameCount = 0;
            for (let line of lines) {
                if (isStackFrame(line)) {
                    if (frameCount >= stackTraceLimit
                        || isMocha(line)
                        || isNode(line)) {
                        continue;
                    }

                    if (isHarness(line)) {
                        if (harnessFrameCount >= maxHarnessFrames) {
                            continue;
                        }

                        harnessFrameCount++;
                    }

                    line = line.replace(/\bfile:\/\/\/(.*?)(?=(:\d+)*($|\)))/, (_, path) => ts.sys.resolvePath(path));
                    frameCount++;
                }

                filtered.push(line);
            }

            (error as any).stack = filtered.join(Harness.IO.newLine());
        }

        return error;
    }

    function isStackFrame(line: string) {
        return /^\s+at\s/.test(line);
    }

    function isMocha(line: string) {
        return /[\\/](node_modules|components)[\\/]mocha(js)?[\\/]|[\\/]mocha\.js/.test(line);
    }

    function isNode(line: string) {
        return /\((timers|events|node|module)\.js:/.test(line);
    }

    function isHarness(line: string) {
        return /[\\/]src[\\/]harness[\\/]|[\\/]run\.js/.test(line);
    }
}
back to top