Skip to content

feat(developer): kmc convert typesafety 😎#15860

Open
SabineSIL wants to merge 31 commits into
feat/developer/kmc-convertfrom
feat/developer/kmc-convert-Typesafety
Open

feat(developer): kmc convert typesafety 😎#15860
SabineSIL wants to merge 31 commits into
feat/developer/kmc-convertfrom
feat/developer/kmc-convert-Typesafety

Conversation

@SabineSIL

@SabineSIL SabineSIL commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

In several places in kmc-convert: keylayout->kmn we did not care about return type null or undefined for function return values or parameters. Therefore we got several warnings about possible return types not being assignable to symbols.

Even though they did not affect the code running correctly they have been specified more clearly in this PR.

@keymanapp-test-bot skip

see also:

@keymanapp-test-bot

keymanapp-test-bot Bot commented Apr 20, 2026

Copy link
Copy Markdown

User Test Results

Test specification and instructions

User tests are not required

Test Artifacts

@keymanapp-test-bot keymanapp-test-bot Bot changed the title feat/developer/kmc convert typesafety feat/developer/kmc convert typesafety 😎 Apr 20, 2026
@SabineSIL SabineSIL changed the title feat/developer/kmc convert typesafety 😎 feat(developer):kmc convert typesafety 😎 Apr 21, 2026
@SabineSIL SabineSIL changed the title feat(developer):kmc convert typesafety 😎 feat(developer): kmc convert typesafety 😎 Apr 21, 2026
@SabineSIL SabineSIL marked this pull request as ready for review April 21, 2026 15:44
@keyman-server keyman-server modified the milestones: A19S27, A19S28 Apr 24, 2026
@keyman-server keyman-server modified the milestones: A19S28, A19S29 May 11, 2026
@SabineSIL SabineSIL moved this from Todo to Code Review in Keyman May 18, 2026
SabineSIL and others added 5 commits May 19, 2026 09:57
…vert-Typesafety

# Conflicts:
#	developer/src/kmc-convert/src/keylayout-to-kmn/keylayout-to-kmn-converter.ts
#	developer/src/kmc-convert/src/keylayout-to-kmn/kmn-file-writer.ts
#	developer/src/kmc-convert/test/keylayout-to-kmn-converter.tests.ts
#	developer/src/kmc-convert/test/kmn-file-writer.tests.ts

@ermshiperete ermshiperete left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks pretty good despite the many comments.

Comment on lines +21 to +23
* @brief helper function to check if a specific keyMap index exists in a keyMapSet
* neccessary because the amount of <keyMap index> must correspond to
* the amount of <keyMapSelect mapIndex>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better to make this full sentences. If this gets processed by a tool the three lines might get put in the same paragraph removing the line breaks, which then would make it hard to understand.

Suggested change
* @brief helper function to check if a specific keyMap index exists in a keyMapSet
* neccessary because the amount of <keyMap index> must correspond to
* the amount of <keyMapSelect mapIndex>
* @brief Helper function to check if a specific keyMap index exists in a keyMapSet.
* This is neccessary because the amount of <keyMap index> must correspond to
* the amount of <keyMapSelect mapIndex>.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines +40 to +42
* @brief helper function to check if a specific keyMapSelect index exists in a modifierMap
* neccessary because the amount of <keyMap index> must correspond to
* the amount of <keyMapSelect mapIndex>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @brief helper function to check if a specific keyMapSelect index exists in a modifierMap
* neccessary because the amount of <keyMap index> must correspond to
* the amount of <keyMapSelect mapIndex>
* @brief Helper function to check if a specific keyMapSelect index exists in a modifierMap.
* This is neccessary because the amount of <keyMap index> must correspond to
* the amount of <keyMapSelect mapIndex>.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

behavior: string;
modifier?: string;
outchar?: string;
outchar?: string | undefined;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be necessary. outchar? makes the property optional which implies undefined. So if you write outchar?: string; the type of outchar is implicitly string|undefined.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines +318 to +320
// in case writeCharacterOrUnicode() returns null, the fallback is empty strings for characterMessage.character
// and characterMessage.message. Then versionOutputCharacter could be "" and would be written into the kmn file
// as ... > '', producing an invalid kmn rule.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this comment. It describes what the code does and that this could result in an invalid kmn rule. Wouldn't we want to do something so that we don't get invalid kmn rules? (in which case I'd expect a TODO or something).

Or does the comment try to explain why we have the if ((outputCharacter !== undefined) && (outputCharacter !== "")) check? Then it should mention outputCharacter which could lead to writeCharacterOrUnicode returning false...

(Additional occurrences of the same code comment above)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The explanation would have been:

// in case writeCharacterOrUnicode() returns null, the fallback is empty strings for characterMessage.character
// and characterMessage.message. Then versionOutputCharacter will be "" and will be written into the kmn file
// as ... > '', preventing an invalid kmn rule like + [NCAPS K_A]  >  'undefined'

but this code is removed since writeCharacterOrUnicode(outputCharacter ..) now returns "" in case of null/undefined as input

* null in case of an empty string or null or undefined input
*/
public writeCharacterOrUnicode(ctr: string, msg: string = ""): MessageCharacter {
public writeCharacterOrUnicode(ctr: string, msg: string = ""): MessageCharacter | null {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better if writeCharacterToUnicode would always returns MessageCharacter. In the error case it could return { character: '', message: '' }. IMO this would simplify things quite a bit.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

].forEach(function (values) {
it(('should convert "' + values[0] + '"').padEnd(25, " ") + 'to "' + values[2] + '"', async function () {
const result = sutW.writeCharacterOrUnicode(values[0] as string, values[1] as string);
if (result) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better to split this into two tests: one set that tests the cases where we get a result, and the other for the cases that return null.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

});
it(('null should create empty string '), async function () {
const result1 = sutW.writeDataRules(null);
assert.isTrue(result1 === '');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd use

Suggested change
assert.isTrue(result1 === '');
assert.equal(result1, '');

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done and in other occurrences also

it('run() should throw on unavailable input file name and null output file name', async function () {
const inputFilename = makePathToFixture('../data/Unavailable.keylayout');
const result = sut.run(inputFilename, null);
const result = sut.run(inputFilename, undefined);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since run is async, we'll have to await it:

Suggested change
const result = sut.run(inputFilename, undefined);
const result = await sut.run(inputFilename, undefined);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done here and at other places

this.callbacks.reportMessage(ConverterMessages.Error_UnsupportedCharactersDetected({
keymapIndex: jsonObj.keyboard.keyMapSet[0].keyMap[i]['index'],
output: jsonObj.keyboard.keyMapSet[0].keyMap[i].key[j]['output'] ?? '',
output: jsonObj.keyboard.keyMapSet[0].keyMap[i].key[j]['output'] ?? '' as string,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as string is unnecessary since it's already a string.

Suggested change
output: jsonObj.keyboard.keyMapSet[0].keyMap[i].key[j]['output'] ?? '' as string,
output: jsonObj.keyboard.keyMapSet[0].keyMap[i].key[j]['output'] ?? '',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be better to initialize this to the empty string, otherwise we could theoretically end up with an uninitialized variable if the if in line 163 evaluates to false.

Suggested change
let versionOutputCharacter = '';

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done unused now; use const later

@keyman-server keyman-server modified the milestones: A19S29, A19S30 May 23, 2026

@SabineSIL SabineSIL left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all comments have been addressed

Comment on lines +21 to +23
* @brief helper function to check if a specific keyMap index exists in a keyMapSet
* neccessary because the amount of <keyMap index> must correspond to
* the amount of <keyMapSelect mapIndex>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines +40 to +42
* @brief helper function to check if a specific keyMapSelect index exists in a modifierMap
* neccessary because the amount of <keyMap index> must correspond to
* the amount of <keyMapSelect mapIndex>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

behavior: string;
modifier?: string;
outchar?: string;
outchar?: string | undefined;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines +318 to +320
// in case writeCharacterOrUnicode() returns null, the fallback is empty strings for characterMessage.character
// and characterMessage.message. Then versionOutputCharacter could be "" and would be written into the kmn file
// as ... > '', producing an invalid kmn rule.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The explanation would have been:

// in case writeCharacterOrUnicode() returns null, the fallback is empty strings for characterMessage.character
// and characterMessage.message. Then versionOutputCharacter will be "" and will be written into the kmn file
// as ... > '', preventing an invalid kmn rule like + [NCAPS K_A]  >  'undefined'

but this code is removed since writeCharacterOrUnicode(outputCharacter ..) now returns "" in case of null/undefined as input

Comment on lines +1656 to +1657
const ctr_val = ((m_uni || m_hex || m_dec) ?
m_uni ? parseInt(m_uni[1], 16) : m_hex ? parseInt(m_hex[1], 16) : parseInt(m_dec[1], 10) : KeylayoutToKmnConverter.MAX_CTRL_CHARACTER
m_uni ? parseInt(m_uni[1], 16) : m_hex ? parseInt(m_hex[1], 16) : m_dec ? parseInt(m_dec[1], 10) : KeylayoutToKmnConverter.MAX_CTRL_CHARACTER : KeylayoutToKmnConverter.MAX_CTRL_CHARACTER

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

].forEach(function (values) {
it(('should convert "' + values[0] + '"').padEnd(25, " ") + 'to "' + values[2] + '"', async function () {
const result = sutW.writeCharacterOrUnicode(values[0] as string, values[1] as string);
if (result) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

});
it(('null should create empty string '), async function () {
const result1 = sutW.writeDataRules(null);
assert.isTrue(result1 === '');

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done and in other occurrences also

it('run() should throw on unavailable input file name and null output file name', async function () {
const inputFilename = makePathToFixture('../data/Unavailable.keylayout');
const result = sut.run(inputFilename, null);
const result = sut.run(inputFilename, undefined);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done here and at other places

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done unused now; use const later

* null in case of an empty string or null or undefined input
*/
public writeCharacterOrUnicode(ctr: string, msg: string = ""): MessageCharacter {
public writeCharacterOrUnicode(ctr: string, msg: string = ""): MessageCharacter | null {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@keyman-server keyman-server modified the milestones: A19S30, A19S31 Jun 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Code Review

Development

Successfully merging this pull request may close these issues.

3 participants