Skip to content

Commit 5ff1142

Browse files
committed
fix: object literal completion now showing for optional objects (but not arrays!). Also fix showing string suggestion for just undefined type
1 parent 5911765 commit 5ff1142

File tree

3 files changed

+33
-13
lines changed

3 files changed

+33
-13
lines changed

typescript/src/completions/objectLiteralCompletions.ts

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,18 @@ export default (
2020
const typeChecker = languageService.getProgram()!.getTypeChecker()!
2121
const objType = typeChecker.getContextualType(node)
2222
if (!objType) return
23-
// its doesn't return all actual properties in some cases e.g. it would be more correct to use symbols from entries, but there is a block from TS
24-
const properties = objType.getProperties()
23+
const types = objType.isUnion() ? objType.types : [objType]
24+
const properties = types.flatMap(type => {
25+
if (isFunctionType(type, typeChecker)) return []
26+
if (isObjectCompletion(type, typeChecker)) return typeChecker.getPropertiesOfType(type)
27+
return []
28+
})
2529
for (const property of properties) {
2630
const entry = entries.find(({ name }) => name === property.name)
2731
if (!entry) continue
2832
const type = typeChecker.getTypeOfSymbolAtLocation(property, node)
2933
if (!type) continue
30-
if (isMethodCompletionCall(type, typeChecker)) {
34+
if (isFunctionType(type, typeChecker)) {
3135
if (['above', 'remove'].includes(keepOriginal) && preferences.includeCompletionsWithObjectLiteralMethodSnippets) {
3236
const methodEntryIndex = entries.findIndex(e => e.name === entry.name && isObjectLiteralMethodSnippet(e))
3337
const methodEntry = entries[methodEntryIndex]
@@ -79,35 +83,43 @@ const isObjectLiteralMethodSnippet = (entry: ts.CompletionEntry) => {
7983
return detail?.startsWith('(') && detail.split('\n')[0]!.trimEnd().endsWith(')')
8084
}
8185

82-
const isMethodCompletionCall = (type: ts.Type, checker: ts.TypeChecker) => {
86+
const isFunctionType = (type: ts.Type, checker: ts.TypeChecker) => {
8387
if (checker.getSignaturesOfType(type, ts.SignatureKind.Call).length > 0) return true
84-
if (type.isUnion()) return type.types.some(type => isMethodCompletionCall(type, checker))
88+
if (type.isUnion()) return type.types.some(type => isFunctionType(type, checker))
89+
}
90+
91+
const isEverySubtype = (type: ts.UnionType, predicate: (type: ts.Type) => boolean): boolean => {
92+
// union cannot consist of only undefined types
93+
return type.types.every(type => {
94+
if (type.flags & ts.TypeFlags.Undefined) return true
95+
return predicate(type)
96+
})
8597
}
8698

8799
const isStringCompletion = (type: ts.Type) => {
88-
if (type.flags & ts.TypeFlags.Undefined) return true
100+
if (type.flags & ts.TypeFlags.Undefined) return false
89101
if (type.flags & ts.TypeFlags.StringLike) return true
90-
if (type.isUnion()) return type.types.every(type => isStringCompletion(type))
102+
if (type.isUnion()) return isEverySubtype(type, type => isStringCompletion(type))
91103
return false
92104
}
93105

94106
const isArrayCompletion = (type: ts.Type, checker: ts.TypeChecker) => {
95107
if (type.flags & ts.TypeFlags.Any) return false
96-
if (type.flags & ts.TypeFlags.Undefined) return true
108+
if (type.flags & ts.TypeFlags.Undefined) return false
97109
if (checker['isArrayLikeType'](type)) return true
98-
if (type.isUnion()) return type.types.every(type => isArrayCompletion(type, checker))
110+
if (type.isUnion()) return isEverySubtype(type, type => isArrayCompletion(type, checker))
99111
return false
100112
}
101113

102114
const isObjectCompletion = (type: ts.Type, checker: ts.TypeChecker) => {
103-
if (type.flags & ts.TypeFlags.Undefined) return true
115+
if (type.flags & ts.TypeFlags.Undefined) return false
104116
if (checker['isArrayLikeType'](type)) return false
105117
if (type.flags & ts.TypeFlags.Object) {
106118
if ((type as ts.ObjectType).objectFlags & ts.ObjectFlags.Class) return false
107119
// complete with regexp?
108120
if (type.symbol?.escapedName === 'RegExp') return false
109121
return true
110122
}
111-
if (type.isUnion()) return type.types.every(type => isObjectCompletion(type, checker))
123+
if (type.isUnion()) return isEverySubtype(type, type => isObjectCompletion(type, checker))
112124
return false
113125
}

typescript/src/completions/objectLiteralHelpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export default (node: ts.Node, entries: ts.CompletionEntry[]): ts.CompletionEntr
55
if (ts.isObjectLiteralExpression(node) && isArrayLike(entries)) {
66
return [
77
{
8-
name: '(array)',
8+
name: '<array methods>',
99
kind: ts.ScriptElementKind.label,
1010
sortText: '07',
1111
insertText: '[]',

typescript/test/completions.spec.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,9 +306,10 @@ test('Object Literal Completions', () => {
306306
foo?: boolean
307307
}
308308
plugins: Array<{ name: string, setup(build) }>
309+
undefinedOption: undefined
309310
}
310311
311-
const makeDay = (options: Options) => {}
312+
const makeDay = (options?: Options) => {}
312313
makeDay({
313314
usedOption,
314315
/*1*/
@@ -338,6 +339,13 @@ test('Object Literal Completions', () => {
338339
},
339340
"name": "plugins",
340341
},
342+
{
343+
"insertText": "undefinedOption",
344+
"isSnippet": true,
345+
"kind": "property",
346+
"kindModifiers": "",
347+
"name": "undefinedOption",
348+
},
341349
{
342350
"insertText": "additionalOptions",
343351
"isSnippet": true,

0 commit comments

Comments
 (0)