Skip to content

Commit 1e46d5f

Browse files
Merge pull request #10 from microbit-foundation/improve-errors
Improve common student error messages Add a new simplified message bundle with simplified text of error message after review with the education team. In simplified mode, types aren't referenced in common messages as students in our scenarios are best focussed on docs and examples to fix their errors. New messages: - booleanIsLowercase special-cases "true" and "false" in name errors as forgetting to uppercase is common - expectedEqualityOperator adds a new case for `if a = b` scenarios Code-level tweaks to existing messages: - add module names to importSymbolUnknown and moduleUnknownMember (potentially upstreamable)
2 parents 510bfee + 80361a3 commit 1e46d5f

File tree

9 files changed

+158
-25
lines changed

9 files changed

+158
-25
lines changed

packages/browser-pyright/src/browser-server.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
MarkupKind,
4242
} from 'vscode-languageserver';
4343
import { apiDocsRequestType } from 'pyright-internal/apidocsProtocol';
44+
import { setMessageStyle } from 'pyright-internal/localization/localize';
4445

4546
const maxAnalysisTimeInForeground = { openFilesTimeInMs: 50, noOpenFilesTimeInMs: 200 };
4647

@@ -125,6 +126,8 @@ export class PyrightServer extends LanguageServerBase {
125126
this._initialFiles = files as InitialFiles;
126127
(this._serverOptions.fileSystem as TestFileSystem).apply(files);
127128
}
129+
// Hack to enable simplified error messages (locale is set in super.initialize).
130+
setMessageStyle('simplified');
128131
return super.initialize(params, supportedCommands, supportedCodeActions);
129132
}
130133

@@ -361,6 +364,10 @@ export class BrowserBackgroundAnalysis extends BackgroundAnalysisBase {
361364
export class BrowserBackgroundAnalysisRunner extends BackgroundAnalysisRunnerBase {
362365
constructor(initialData: InitializationData) {
363366
super(parentPort(), initialData);
367+
368+
// Hack to enable simplified error messages in the background thread.
369+
// Ideally we'd route this via initialData.
370+
setMessageStyle('simplified');
364371
}
365372
createRealFileSystem(): FileSystem {
366373
return new TestFileSystem(false, {

packages/pyright-internal/src/analyzer/typeEvaluator.ts

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { DiagnosticRule } from '../common/diagnosticRules';
2525
import { convertOffsetsToRange } from '../common/positionUtils';
2626
import { PythonVersion } from '../common/pythonVersion';
2727
import { TextRange } from '../common/textRange';
28-
import { Localizer } from '../localization/localize';
28+
import { Localizer, optionalAddendum } from '../localization/localize';
2929
import {
3030
ArgumentCategory,
3131
AssignmentNode,
@@ -2311,7 +2311,8 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
23112311
addDiagnostic(
23122312
AnalyzerNodeInfo.getFileInfo(errorNode).diagnosticRuleSet.reportGeneralTypeIssues,
23132313
DiagnosticRule.reportGeneralTypeIssues,
2314-
Localizer.Diagnostic.typeNotIterable().format({ type: printType(subtype) }) + diag.getString(),
2314+
Localizer.Diagnostic.typeNotIterable().format({ type: printType(subtype) }) +
2315+
optionalAddendum(diag),
23152316
errorNode
23162317
);
23172318
}
@@ -3789,8 +3790,20 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
37893790
}
37903791
}
37913792
} else {
3793+
// Handle the special case of booleans.
3794+
if (name === 'true' || name === 'false') {
3795+
const nameSplit = name.split('');
3796+
nameSplit[0] = nameSplit[0].toUpperCase();
3797+
const booleanName = nameSplit.join('');
3798+
addDiagnostic(
3799+
fileInfo.diagnosticRuleSet.reportUndefinedVariable,
3800+
DiagnosticRule.reportUndefinedVariable,
3801+
Localizer.Diagnostic.booleanIsLowerCase().format({ name, booleanName }),
3802+
node
3803+
);
3804+
}
37923805
// Handle the special case of "reveal_type" and "reveal_locals".
3793-
if (name !== 'reveal_type' && name !== 'reveal_locals') {
3806+
else if (name !== 'reveal_type' && name !== 'reveal_locals') {
37943807
addDiagnostic(
37953808
fileInfo.diagnosticRuleSet.reportUndefinedVariable,
37963809
DiagnosticRule.reportUndefinedVariable,
@@ -4588,7 +4601,10 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
45884601
addDiagnostic(
45894602
fileInfo.diagnosticRuleSet.reportGeneralTypeIssues,
45904603
DiagnosticRule.reportGeneralTypeIssues,
4591-
Localizer.Diagnostic.moduleUnknownMember().format({ name: memberName }),
4604+
Localizer.Diagnostic.moduleUnknownMember().format({
4605+
name: memberName,
4606+
module: baseType.moduleName,
4607+
}),
45924608
node.memberName
45934609
);
45944610
}
@@ -10030,7 +10046,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
1003010046
addDiagnostic(
1003110047
fileInfo.diagnosticRuleSet.reportGeneralTypeIssues,
1003210048
DiagnosticRule.reportGeneralTypeIssues,
10033-
message + diag.getString(),
10049+
message + optionalAddendum(diag),
1003410050
argParam.errorNode
1003510051
);
1003610052
}
@@ -10872,7 +10888,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
1087210888
operator: ParseTreeUtils.printOperator(node.operator),
1087310889
leftType: printType(leftType),
1087410890
rightType: printType(rightType),
10875-
}) + diag.getString(),
10891+
}) + optionalAddendum(diag),
1087610892
node
1087710893
);
1087810894
}
@@ -11035,7 +11051,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
1103511051
operator: ParseTreeUtils.printOperator(node.operator),
1103611052
leftType: printType(leftType),
1103711053
rightType: printType(rightType),
11038-
}) + diag.getString(),
11054+
}) + optionalAddendum(diag),
1103911055
node
1104011056
);
1104111057
}
@@ -16170,7 +16186,10 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
1617016186
addDiagnostic(
1617116187
fileInfo.diagnosticRuleSet.reportGeneralTypeIssues,
1617216188
DiagnosticRule.reportGeneralTypeIssues,
16173-
Localizer.Diagnostic.importSymbolUnknown().format({ name: node.name.value }),
16189+
Localizer.Diagnostic.importSymbolUnknown().format({
16190+
name: node.name.value,
16191+
moduleName: importInfo.importName,
16192+
}),
1617416193
node.name
1617516194
);
1617616195
}

packages/pyright-internal/src/localization/localize.ts

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ import ruStrings = require('./package.nls.ru.json');
1818
import zhCnStrings = require('./package.nls.zh-cn.json');
1919
import zhTwStrings = require('./package.nls.zh-tw.json');
2020

21+
import enUsSimplified = require('./simplified.nls.en-us.json');
22+
import { DiagnosticAddendum } from '../common/diagnostic';
23+
2124
export class ParameterizedString<T extends {}> {
2225
constructor(private _formatString: string) {}
2326

@@ -34,17 +37,31 @@ export class ParameterizedString<T extends {}> {
3437
}
3538
}
3639

40+
function mergeStrings(a: any, b: any): any {
41+
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
42+
const result: any = {};
43+
for (const k of keys) {
44+
result[k] = {
45+
...a[k],
46+
...b[k],
47+
};
48+
}
49+
return result;
50+
}
51+
52+
type MessageStyle = 'default' | 'simplified';
53+
54+
let messageStyle: MessageStyle = 'default';
55+
56+
export function setMessageStyle(style: MessageStyle) {
57+
messageStyle = style;
58+
}
59+
60+
export function optionalAddendum(diag: DiagnosticAddendum) {
61+
return messageStyle === 'simplified' ? '' : diag.toString();
62+
}
63+
3764
const defaultLocale = 'en-us';
38-
const stringMapsByLocale: Map<string, any> = new Map([
39-
['de', deStrings],
40-
['en-us', enUsStrings],
41-
['es', esStrings],
42-
['fr', frStrings],
43-
['ja', jaStrings],
44-
['ru', ruStrings],
45-
['zh-cn', zhCnStrings],
46-
['zh-tw', zhTwStrings],
47-
]);
4865

4966
type StringLookupMap = { [key: string]: string | StringLookupMap };
5067
let localizedStrings: StringLookupMap | undefined = undefined;
@@ -166,7 +183,26 @@ function loadStringsForLocale(locale: string): StringLookupMap {
166183
}
167184

168185
function loadStringsFromJsonFile(locale: string): StringLookupMap | undefined {
169-
return stringMapsByLocale.get(locale);
186+
switch (locale) {
187+
case 'de':
188+
return deStrings;
189+
case 'en-us':
190+
return messageStyle === 'simplified' ? mergeStrings(enUsStrings, enUsSimplified) : enUsStrings;
191+
case 'es':
192+
return esStrings;
193+
case 'fr':
194+
return frStrings;
195+
case 'ja':
196+
return jaStrings;
197+
case 'ru':
198+
return ruStrings;
199+
case 'zh-cn':
200+
return zhCnStrings;
201+
case 'zh-tw':
202+
return zhTwStrings;
203+
default:
204+
return undefined;
205+
}
170206
}
171207

172208
export namespace Localizer {
@@ -231,6 +267,10 @@ export namespace Localizer {
231267
new ParameterizedString<{ type: string; methodName: string; paramName: string }>(
232268
getRawString('Diagnostic.bindTypeMismatch')
233269
);
270+
export const booleanIsLowerCase = () =>
271+
new ParameterizedString<{ name: string; booleanName: string }>(
272+
getRawString('Diagnostic.booleanIsLowerCase')
273+
);
234274
export const breakOutsideLoop = () => getRawString('Diagnostic.breakOutsideLoop');
235275
export const callableExtraArgs = () => getRawString('Diagnostic.callableExtraArgs');
236276
export const callableFirstArg = () => getRawString('Diagnostic.callableFirstArg');
@@ -363,6 +403,7 @@ export namespace Localizer {
363403
export const expectedDecoratorNewline = () => getRawString('Diagnostic.expectedDecoratorNewline');
364404
export const expectedDelExpr = () => getRawString('Diagnostic.expectedDelExpr');
365405
export const expectedElse = () => getRawString('Diagnostic.expectedElse');
406+
export const expectedEqualityOperator = () => getRawString('Diagnostic.expectedEqualityOperator');
366407
export const expectedExceptionClass = () => getRawString('Diagnostic.expectedExceptionClass');
367408
export const expectedExceptionObj = () => getRawString('Diagnostic.expectedExceptionObj');
368409
export const expectedExpr = () => getRawString('Diagnostic.expectedExpr');
@@ -440,7 +481,9 @@ export namespace Localizer {
440481
export const importSourceResolveFailure = () =>
441482
new ParameterizedString<{ importName: string }>(getRawString('Diagnostic.importSourceResolveFailure'));
442483
export const importSymbolUnknown = () =>
443-
new ParameterizedString<{ name: string }>(getRawString('Diagnostic.importSymbolUnknown'));
484+
new ParameterizedString<{ name: string; moduleName: string }>(
485+
getRawString('Diagnostic.importSymbolUnknown')
486+
);
444487
export const incompatibleMethodOverride = () =>
445488
new ParameterizedString<{ name: string; className: string }>(
446489
getRawString('Diagnostic.incompatibleMethodOverride')
@@ -518,7 +561,7 @@ export namespace Localizer {
518561
export const moduleAsType = () => getRawString('Diagnostic.moduleAsType');
519562
export const moduleNotCallable = () => getRawString('Diagnostic.moduleNotCallable');
520563
export const moduleUnknownMember = () =>
521-
new ParameterizedString<{ name: string }>(getRawString('Diagnostic.moduleUnknownMember'));
564+
new ParameterizedString<{ name: string; module: string }>(getRawString('Diagnostic.moduleUnknownMember'));
522565
export const namedExceptAfterCatchAll = () => getRawString('Diagnostic.namedExceptAfterCatchAll');
523566
export const namedParamAfterParamSpecArgs = () =>
524567
new ParameterizedString<{ name: string }>(getRawString('Diagnostic.namedParamAfterParamSpecArgs'));

packages/pyright-internal/src/localization/package.nls.en-us.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"baseClassMethodTypeIncompatible": "Base classes for class \"{classType}\" define method \"{name}\" in incompatible way",
3636
"baseClassUnknown": "Base class type is unknown, obscuring type of derived class",
3737
"bindTypeMismatch": "Could not bind method \"{methodName}\" because \"{type}\" is not assignable to parameter \"{paramName}\"",
38+
"booleanIsLowerCase": "\"{name}\" is not defined, did you mean \"{booleanName}\"?",
3839
"breakOutsideLoop": "\"break\" can be used only within a loop",
3940
"callableExtraArgs": "Expected only two type arguments to \"Callable\"",
4041
"callableFirstArg": "Expected parameter type list or \"...\"",
@@ -125,6 +126,7 @@
125126
"expectedDecoratorNewline": "Expected new line at end of decorator",
126127
"expectedDelExpr": "Expected expression after \"del\"",
127128
"expectedElse": "Expected \"else\"",
129+
"expectedEqualityOperator": "Expected equality operator, did you mean \"==\"?",
128130
"expectedExceptionClass": "Invalid exception class or object",
129131
"expectedExceptionObj": "Expected exception object, exception class or None",
130132
"expectedExpr": "Expected expression",
@@ -185,7 +187,7 @@
185187
"importDepthExceeded": "Import chain depth exceeded {depth}",
186188
"importResolveFailure": "Import \"{importName}\" could not be resolved",
187189
"importSourceResolveFailure": "Import \"{importName}\" could not be resolved from source",
188-
"importSymbolUnknown": "\"{name}\" is unknown import symbol",
190+
"importSymbolUnknown": "\"{name}\" is unknown import symbol in module \"{moduleName}\"",
189191
"incompatibleMethodOverride": "Method \"{name}\" overrides class \"{className}\" in an incompatible manner",
190192
"inconsistentIndent": "Unindent amount does not match previous indent",
191193
"initMustReturnNone": "Return type of \"__init__\" must be None",
@@ -232,7 +234,7 @@
232234
"missingSuperCall": "Method \"{methodName}\" does not call the method of the same name in parent class",
233235
"moduleAsType": "Module cannot be used as a type",
234236
"moduleNotCallable": "Module is not callable",
235-
"moduleUnknownMember": "\"{name}\" is not a known member of module",
237+
"moduleUnknownMember": "\"{name}\" is not a known member of module \"{module}\"",
236238
"namedExceptAfterCatchAll": "A named except clause cannot appear after catch-all except clause",
237239
"namedParamAfterParamSpecArgs": "Keyword parameter \"{name}\" cannot appear in signature after ParamSpec args parameter",
238240
"namedTupleEmptyName": "Names within a named tuple cannot be empty",
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"Diagnostic": {
3+
"argAssignmentParam": "Argument does not match parameter type for parameter \"{paramName}\"",
4+
"argAssignmentParamFunction": "Argument does not match parameter type for parameter \"{paramName}\"",
5+
"argMissingForParam": "Argument missing for parameter {name}",
6+
"argMissingForParams": "Arguments missing for parameters {names}",
7+
"argMorePositionalExpectedCount": "Expected {expected} more positional arguments",
8+
"booleanIsLowerCase": "\"{name}\" is not defined, did you mean \"{booleanName}\"?",
9+
"breakOutsideLoop": "\"break\" can be used only within a loop",
10+
"continueOutsideLoop": "\"continue\" can be used only within a loop",
11+
"expectedAssignRightHandExpr": "Expected expression to the right of \"=\"",
12+
"expectedCloseBrace": "Missing closing bracket \"}\"",
13+
"expectedCloseBracket": "Missing closing bracket \"]\"",
14+
"expectedCloseParen": "Missing closing bracket \")\"",
15+
"expectedColon": "Missing colon \":\"",
16+
"expectedEqualityOperator": "Expected equality operator, did you mean \"==\"?",
17+
"expectedExpr": "Missing expression",
18+
"expectedFunctionName": "Missing function name after \"def\"",
19+
"expectedIndentedBlock": "Indentation missing",
20+
"expectedNewlineOrSemicolon": "Unexpected extra content\nStatements must be one per line",
21+
"importResolveFailure": "Module \"{importName}\" could not be found",
22+
"importSymbolUnknown": "\"{name}\" not found in module \"{moduleName}\"",
23+
"inconsistentIndent": "Indentation does not match the previous line",
24+
"moduleUnknownMember": "\"{name}\" is not a known member of module \"{module}\"",
25+
"stringUnterminated": "String is not closed — missing quotation mark",
26+
"typeNotIterable": "Type is not iterable",
27+
"typeNotSupportBinaryOperator": "Operator \"{operator}\" not supported for this combination of types",
28+
"typeNotSupportBinaryOperatorBidirectional": "Operator \"{operator}\" not supported for this combination of types",
29+
"unaccessedClass": "Class \"{name}\" is unused",
30+
"unaccessedFunction": "Function \"{name}\" is unused",
31+
"unaccessedImport": "Import \"{name}\" is unused",
32+
"unaccessedSymbol": "\"{name}\" is unused",
33+
"unaccessedVariable": "Variable \"{name}\" is unused",
34+
"unexpectedIndent": "Unexpected indentation",
35+
"unreachableCode": "Code is unreachable\nThe logic of your program means this code will never run"
36+
}
37+
}

packages/pyright-internal/src/parser/parser.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1209,8 +1209,13 @@ export class Parser {
12091209
const suite = SuiteNode.create(nextToken);
12101210

12111211
if (!this._consumeTokenIfType(TokenType.Colon)) {
1212-
this._addError(Localizer.Diagnostic.expectedColon(), nextToken);
1213-
1212+
if (nextToken.type === TokenType.Operator) {
1213+
if (this._peekOperatorType() === OperatorType.Assign) {
1214+
this._addError(Localizer.Diagnostic.expectedEqualityOperator(), nextToken);
1215+
}
1216+
} else {
1217+
this._addError(Localizer.Diagnostic.expectedColon(), nextToken);
1218+
}
12141219
// Try to perform parse recovery by consuming tokens.
12151220
if (this._consumeTokensUntilType([TokenType.NewLine, TokenType.Colon])) {
12161221
if (this._peekTokenType() === TokenType.Colon) {

packages/pyright-internal/src/tests/parser.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,20 @@ test('SuiteExpectedColon3', () => {
5757
assert.strictEqual(diagSink.getErrors().length, 1);
5858
});
5959

60+
test('SuiteExpectedColon4', () => {
61+
const diagSink = new DiagnosticSink();
62+
TestUtils.parseSampleFile('suiteExpectedColon4.py', diagSink);
63+
assert.strictEqual(diagSink.getErrors().length, 1);
64+
assert.strictEqual(diagSink.getErrors()[0].message, 'Expected equality operator, did you mean "=="?');
65+
});
66+
67+
test('SuiteExpectedColon5', () => {
68+
const diagSink = new DiagnosticSink();
69+
TestUtils.parseSampleFile('suiteExpectedColon5.py', diagSink);
70+
assert.strictEqual(diagSink.getErrors().length, 1);
71+
assert.strictEqual(diagSink.getErrors()[0].message, 'Expected ":"');
72+
});
73+
6074
test('ExpressionWrappedInParens', () => {
6175
const diagSink = new DiagnosticSink();
6276
const parseResults = TestUtils.parseText('(str)', diagSink);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
a = 1
2+
if a = 1:
3+
pass
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
a = 1
2+
if a == 1
3+
pass

0 commit comments

Comments
 (0)