From c17fde7c45dc2e10473e10c1e3fb06be9d48d9db Mon Sep 17 00:00:00 2001 From: David Paz Date: Wed, 3 Dec 2025 15:05:32 +0100 Subject: [PATCH 1/5] Remove deprecated warning scripts-prepend-node-path This settings is not required anymore. Remove deprecated settings on .npmrc causing warning: ``` npm warn Unknown project config "scripts-prepend-node-path". This will stop working in the next major version of npm. ``` --- .npmrc | 1 - 1 file changed, 1 deletion(-) diff --git a/.npmrc b/.npmrc index 11a5949..e69de29 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +0,0 @@ -scripts-prepend-node-path=auto From a44540f61d4036f56feb8b61831325a0ad763c67 Mon Sep 17 00:00:00 2001 From: David Paz Date: Wed, 3 Dec 2025 15:08:20 +0100 Subject: [PATCH 2/5] Add serilization functionality to raw fix format Add possibility to serialize pretty printed FIX messages back to its raw FIX format. It does it by adding functions to parse already pretty printed messages and to serialize parsed Messages to FIX format. for more details, look at the doc/RAW_FIX_CONVERTER.md file --- doc/RAW_FIX_CONVERTER.md | 167 ++++++++++++++++ package.json | 20 ++ src/extension.ts | 77 +++++++- src/fixProtocol.ts | 103 +++++++++- src/test/suite/fixProtocol.test.ts | 306 ++++++++++++++++++++++++++++- 5 files changed, 667 insertions(+), 6 deletions(-) create mode 100644 doc/RAW_FIX_CONVERTER.md diff --git a/doc/RAW_FIX_CONVERTER.md b/doc/RAW_FIX_CONVERTER.md new file mode 100644 index 0000000..65ec3a7 --- /dev/null +++ b/doc/RAW_FIX_CONVERTER.md @@ -0,0 +1,167 @@ +# Convert Pretty-Printed FIX to Raw FIX Format + +This guide shows you how to use the new "Convert to Raw FIX" commands to transform pretty-printed FIX messages back into their raw protocol format. + +## What's New + +Two new commands have been added: +- **FIX Master - Convert to Raw FIX**: Converts entire document from pretty-printed to raw FIX +- **FIX Master - Convert Selection to Raw FIX**: Converts only selected text + +## How It Works + +The commands parse pretty-printed FIX messages like this: + +``` +NewOrderSingle +{ + BeginString (8) FIX.4.4 + BodyLength (9) 140 + MsgType (35) D - NewOrderSingle + SenderCompID (49) INITIATOR + TargetCompID (56) ACCEPTOR + MsgSeqNum (34) 2282 + SendingTime (52) 20190929-04:51:00.849 + ClOrdID (11) 50 + AllocAccount (70) 49 + ExDestination (100) AUTO + Symbol (55) WTF + Side (54) 1 - Buy + TransactTime (60) 20190929-04:35:33.562 + OrderQty (38) 10000 + OrdType (40) 1 - Market + TimeInForce (59) 1 - GoodTillCancel + CheckSum (10) 129 +} +``` + +And converts them to raw FIX format: + +``` +8=FIX.4.4�9=140�35=D�49=INITIATOR�56=ACCEPTOR�34=2282�52=20190929-04:51:00.849�11=50�70=49�100=AUTO�55=WTF�54=1�60=20190929-04:35:33.562�38=10000�40=1�59=1�10=129� +``` + +(Note: `�` represents the SOH character `0x01` which is the standard FIX field delimiter) + +## Usage + +### Method 1: Command Palette + +1. Open a file with pretty-printed FIX messages (usually a `.fix` file) +2. Press `Cmd+Shift+P` (Mac) or `Ctrl+Shift+P` (Windows/Linux) +3. Type "FIX Master - Convert to Raw FIX" +4. Press Enter +5. A new document opens in a side-by-side view with the raw FIX output + +### Method 2: Convert Selection Only + +1. Select one or more pretty-printed messages +2. Press `Cmd+Shift+P` (Mac) or `Ctrl+Shift+P` (Windows/Linux) +3. Type "FIX Master - Convert Selection to Raw FIX" +4. Press Enter +5. A new document opens with the converted selection + +### Method 3: Right-Click Context Menu + +1. Select the text you want to convert +2. Right-click to open the context menu +3. Choose "FIX Master - Convert Selection to Raw FIX" + +## Testing the Feature + +### Step 1: Create a Test File + +1. In VS Code, press `F5` to launch the Extension Development Host +2. Create a new file with this content: + +``` +NewOrderSingle +{ + BeginString (8) FIX.4.4 + MsgType (35) D - NewOrderSingle + SenderCompID (49) INITIATOR + TargetCompID (56) ACCEPTOR + ClOrdID (11) ORDER123 + Side (54) 1 - Buy + Symbol (55) AAPL + OrderQty (38) 100 + OrdType (40) 2 - Limit + Price (44) 150.50 +} +``` + +### Step 2: Run the Command + +1. Press `Cmd+Shift+P` / `Ctrl+Shift+P` +2. Run "FIX Master - Convert to Raw FIX" +3. You should see a new document with raw FIX output + +### Step 3: Verify Round-Trip + +To verify the conversion works correctly: + +1. Copy the raw FIX output from the new document +2. Create another new file and paste the raw FIX +3. Run "FIX Master - Pretty Print" +4. Compare with the original pretty-printed message - they should match! + +## Features + +### Multiple Messages + +The command handles multiple messages in the same document: + +``` +Logon +{ + BeginString (8) FIX.4.4 + ... +} + +NewOrderSingle +{ + BeginString (8) FIX.4.4 + ... +} +``` + +Both messages will be converted to raw FIX format. + +### Preserves Non-Message Lines + +Lines outside message blocks (like headers, timestamps, or notes) are preserved: + +``` +=== Trading Session 2024-01-15 === + +NewOrderSingle +{ + ... +} + +=== End of Session === +``` + +### Data Fields + +Base64-encoded data fields (like Signature, SecureData) are automatically decoded back to their binary representation. + +## Use Cases + +1. **Message Replay**: Convert formatted messages back to raw FIX for replaying to a test environment +2. **Debugging**: Compare raw messages before and after formatting +3. **Log Analysis**: Extract specific messages from logs and convert them for injection into test systems +4. **Message Modification**: Edit pretty-printed messages and convert back to raw FIX +5. **Integration Testing**: Generate raw FIX messages from human-readable format + +## Implementation Details + +- Parser: `parsePrettyPrintedMessage()` in `src/fixProtocol.ts` +- Formatter: `fixFormatPrintMessage()` in `src/fixProtocol.ts` +- Command Handler: `formatRawFix()` in `src/extension.ts` + +## Limitations + +- The converter relies on the tag numbers in parentheses to extract field values +- Enumerated values (e.g., "1 - Buy") are correctly parsed to extract just the value ("1") +- The output is always in a new document (doesn't support in-place replacement) diff --git a/package.json b/package.json index af5bdb8..827cf72 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,14 @@ "command": "extension.format-csv-selection", "title": "FIX Master - CSV Format Selection" }, + { + "command": "extension.format-raw-fix", + "title": "FIX Master - Convert to Raw FIX" + }, + { + "command": "extension.format-raw-fix-selection", + "title": "FIX Master - Convert Selection to Raw FIX" + }, { "command": "extension.show-field", "title": "FIX Master - Show the definition of a field" @@ -129,6 +137,13 @@ "when": "editorHasSelection", "command": "extension.format-csv-selection" }, + { + "command": "extension.format-raw-fix" + }, + { + "when": "editorHasSelection", + "command": "extension.format-raw-fix-selection" + }, { "command": "extension.show-field" } @@ -148,6 +163,11 @@ "when": "editorHasSelection", "command": "extension.format-csv-selection", "group": "1_modification" + }, + { + "when": "editorHasSelection", + "command": "extension.format-raw-fix-selection", + "group": "1_modification" } ] }, diff --git a/src/extension.ts b/src/extension.ts index 4549af4..aecd6dd 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import * as fs from 'fs'; import { Orchestra } from './fixOrchestra'; import { DataDictionary } from './quickFixDataDictionary'; -import { fixMessagePrefix, parseMessage, prettyPrintMessage, msgTypeHeartbeat, msgTypeTestRequest, csvPrintMessage, Message } from './fixProtocol'; +import { fixMessagePrefix, parseMessage, prettyPrintMessage, msgTypeHeartbeat, msgTypeTestRequest, csvPrintMessage, fixFormatPrintMessage, parsePrettyPrintedMessage, Message } from './fixProtocol'; import { AdministrativeMessageBehaviour, CommandScope, EditorReuse, NameLookup } from './options'; import { definitionHtmlForField } from './html'; import { OrderBook } from './orderBook'; @@ -421,7 +421,80 @@ export function activate(context: ExtensionContext) { commands.registerCommand('extension.format-csv-selection', async () => { await format(csvPrintMessage, CommandScope.Selection); }); - + + commands.registerCommand('extension.format-raw-fix', async () => { + await formatRawFix(CommandScope.Document); + }); + + commands.registerCommand('extension.format-raw-fix-selection', async () => { + await formatRawFix(CommandScope.Selection); + }); + + let formatRawFix = async (scope: CommandScope) => { + if (!orchestra) { + window.showErrorMessage('The orchestra has not been loaded - check the orchestraPath setting.'); + return; + } + + const {activeTextEditor} = window; + + if (!activeTextEditor) { + window.showErrorMessage('No document is open or the file is too large.'); + return; + } + + const sourceDocument = activeTextEditor.document; + let text: string; + + if (scope === CommandScope.Document) { + text = sourceDocument.getText(); + } else { + const selection = activeTextEditor.selection; + if (!selection) { + window.showErrorMessage('No text selected.'); + return; + } + const range = new Range(selection.start, selection.end); + text = sourceDocument.getText(range); + } + + // Parse pretty-printed messages and convert to raw FIX + const lines = text.split('\n'); + let output = ''; + let currentMessageLines: string[] = []; + let inMessage = false; + + for (const line of lines) { + if (line.trim() === '{') { + inMessage = true; + currentMessageLines = [line]; + } else if (line.trim() === '}') { + currentMessageLines.push(line); + inMessage = false; + + // Parse this message block + const messageText = currentMessageLines.join('\n'); + const message = parsePrettyPrintedMessage(messageText); + + if (message) { + const rawFix = fixFormatPrintMessage('', message, orchestra, dataDictionary, 0); + output += rawFix; + } + + currentMessageLines = []; + } else if (inMessage) { + currentMessageLines.push(line); + } else if (!inMessage && line.trim().length > 0) { + // Preserve non-message lines (like headers, timestamps, etc.) + output += line + '\n'; + } + } + + // Create a new document with the raw FIX output + const document = await workspace.openTextDocument({ content: output, language: 'plaintext' }); + await window.showTextDocument(document, ViewColumn.Beside); + }; + commands.registerCommand('extension.show-field', async () => { if (!orchestra) { diff --git a/src/fixProtocol.ts b/src/fixProtocol.ts index 3a24d95..35a3f31 100644 --- a/src/fixProtocol.ts +++ b/src/fixProtocol.ts @@ -316,14 +316,14 @@ export function csvPrintMessage(context: string, message: Message, orchestra: FI var buffer: string = ""; const description = message.describe(orchestra, quickFix); - + if (context && context.length > 0) { buffer += context + " "; } if (description.messageName && description.messageName.length > 0) { buffer += description.messageName + "\n"; - } + } buffer += "{\n"; @@ -342,3 +342,102 @@ export function csvPrintMessage(context: string, message: Message, orchestra: FI return buffer; } + +export function fixFormatPrintMessage(context: string, message: Message, orchestra: FIX.Orchestra, quickFix: DataDictionary | null, _: number) { + + var buffer: string = ""; + const description = message.describe(orchestra, quickFix); + + if (context && context.length > 0) { + buffer += context + " "; + } + + // Get the BeginString field to determine FIX version for data field handling + const beginString = message.fields.find(field => field.tag === beginStringTag); + var version: xml.Orchestration | undefined; + + if (beginString) { + version = orchestra.orchestrations.find(v => v.version === beginString.value); + } + + // Build the raw FIX message + message.fields.forEach((field, index) => { + buffer += `${field.tag}${fieldValueSeparator}`; + + // Check if this is a data field (base64 encoded) that needs decoding + let definition = orchestra.definitionOfField(field.tag, version, undefined); + if (definition?.field.type === 'data') { + // Decode base64 encoded data fields back to their original form + try { + const decoded = base64.decode(field.value); + buffer += decoded; + } catch (err) { + // If decoding fails, use the value as-is + buffer += field.value; + } + } else { + buffer += field.value; + } + + buffer += fieldDelimiter; + }); + + buffer += "\n"; + + return buffer; +} + +export function parsePrettyPrintedMessage(text: string): Message | null { + // Pattern to match pretty-printed fields: FieldName (tag) value + // or: FieldName (tag) value - description (for enumerated values) + const fieldPattern = /^\s*\w+\s+\((\d+)\)\s+(.+?)(?:\s+-\s+.+)?$/; + + const lines = text.split('\n'); + const fields: Field[] = []; + var msgType: string | null = null; + var inMessage = false; + + for (const line of lines) { + // Check for message start + if (line.trim() === '{') { + inMessage = true; + continue; + } + + // Check for message end + if (line.trim() === '}') { + break; + } + + // Skip message name line (doesn't have parentheses) + if (!inMessage || !line.includes('(')) { + continue; + } + + const match = fieldPattern.exec(line); + if (match) { + const tag = parseInt(match[1]); + let value = match[2].trim(); + + // For enumerated values, extract just the value part before the dash + // Example: "1 - Buy" -> "1" + const dashIndex = value.indexOf(' - '); + if (dashIndex > 0) { + value = value.substring(0, dashIndex).trim(); + } + + const field = new Field(tag, value); + fields.push(field); + + if (tag === msgTypeTag) { + msgType = value; + } + } + } + + if (fields.length === 0) { + return null; + } + + return new Message(msgType, fields); +} diff --git a/src/test/suite/fixProtocol.test.ts b/src/test/suite/fixProtocol.test.ts index 3d058f9..6e48605 100644 --- a/src/test/suite/fixProtocol.test.ts +++ b/src/test/suite/fixProtocol.test.ts @@ -8,7 +8,7 @@ import { Orchestra } from '../../fixOrchestra'; import * as vscode from 'vscode'; // import * as myExtension from '../extension'; -import { parseMessage } from '../../fixProtocol'; +import { parseMessage, fixFormatPrintMessage, parsePrettyPrintedMessage, prettyPrintMessage } from '../../fixProtocol'; suite('FIX Protocol Test Suite', () => { @@ -80,7 +80,7 @@ suite('FIX Protocol Test Suite', () => { test('Parse message with a data field', () => { let orchestra = new Orchestra(path.join(__dirname, "../../../orchestrations")); - let text = "8=FIX.4.4\u00019=167\u000135=D\u000149=INITIATOR\u000156=ACCEPTOR\u000134=2752\u000152=20200114-08:13:20.041\u000111=61\u000170=60\u0001100=AUTO\u000155=BHP.AX\u000154=1\u000160=20200114-08:12:59.397\u000138=10000\u000140=2\u000144=20\u000159=1\u000193=20\u000189=ABCDEF\u0001ABCDEFABC\u0001DEF\u000110=220\u0001"; + let text = "8=FIX.4.4\u00019=167\u000135=D\u000149=INITIATOR\u000156=ACCEPTOR\u000134=2752\u000152=20200114-08:13:20.041\u000111=61\u000170=60\u0001100=AUTO\u000155=BHP.AX\u000154=1\u000160=20200114-08:12:59.397\u000138=10000\u000140=2\u000144=20\u000159=1\u000193=20\u000189=ABCDEF\u0001ABCDEFABC\u0001DEF\u000110=220\u0001"; let message = parseMessage(text, orchestra); if (!message) { assert.fail("message failed to parse"); @@ -90,4 +90,306 @@ suite('FIX Protocol Test Suite', () => { let signature = message.fields[18]; assert.equal('QUJDREVGAUFCQ0RFRkFCQwFERUY=', signature.value); }); + + test('fixFormatPrintMessage outputs raw FIX format', () => { + let orchestra = new Orchestra(path.join(__dirname, "../../../orchestrations")); + let text = "8=FIX.4.4\u00019=72\u000135=A\u000149=ACCEPTOR\u000156=INITIATOR\u000134=1\u000152=20190816-10:34:27.742\u000198=0\u0001108=30\u000110=012\u0001"; + let message = parseMessage(text); + if (!message) { + assert.fail("message failed to parse"); + return; + } + let output = fixFormatPrintMessage("", message, orchestra, null, 0); + // Remove the trailing newline for comparison + output = output.trimEnd(); + assert.equal(text, output); + }); + + test('fixFormatPrintMessage with context prefix', () => { + let orchestra = new Orchestra(path.join(__dirname, "../../../orchestrations")); + let text = "8=FIX.4.4\u00019=72\u000135=A\u000149=ACCEPTOR\u000156=INITIATOR\u000134=1\u000152=20190816-10:34:27.742\u000198=0\u0001108=30\u000110=012\u0001"; + let message = parseMessage(text); + if (!message) { + assert.fail("message failed to parse"); + return; + } + let context = "2024-01-15 10:30:45.123"; + let output = fixFormatPrintMessage(context, message, orchestra, null, 0); + // Check that context appears at the beginning + assert.ok(output.startsWith(context + " ")); + // Check that the FIX message follows + assert.ok(output.includes("8=FIX.4.4")); + }); + + test('fixFormatPrintMessage with NewOrderSingle', () => { + let orchestra = new Orchestra(path.join(__dirname, "../../../orchestrations")); + let text = "8=FIX.4.4\u00019=140\u000135=D\u000149=INITIATOR\u000156=ACCEPTOR\u000134=2282\u000152=20190929-04:51:00.849\u000111=50\u000170=49\u0001100=AUTO\u000155=WTF\u000154=1\u000160=20190929-04:35:33.562\u000138=10000\u000140=1\u000159=1\u000110=129\u0001"; + let message = parseMessage(text); + if (!message) { + assert.fail("message failed to parse"); + return; + } + let output = fixFormatPrintMessage("", message, orchestra, null, 0); + output = output.trimEnd(); + assert.equal(text, output); + }); + + test('fixFormatPrintMessage with data field decodes base64', () => { + let orchestra = new Orchestra(path.join(__dirname, "../../../orchestrations")); + let originalText = "8=FIX.4.4\u00019=167\u000135=D\u000149=INITIATOR\u000156=ACCEPTOR\u000134=2752\u000152=20200114-08:13:20.041\u000111=61\u000170=60\u0001100=AUTO\u000155=BHP.AX\u000154=1\u000160=20200114-08:12:59.397\u000138=10000\u000140=2\u000144=20\u000159=1\u000193=20\u000189=ABCDEF\u0001ABCDEFABC\u0001DEF\u000110=220\u0001"; + let message = parseMessage(originalText, orchestra); + if (!message) { + assert.fail("message failed to parse"); + return; + } + let output = fixFormatPrintMessage("", message, orchestra, null, 0); + output = output.trimEnd(); + // The output should match the original since data fields are decoded from base64 + assert.equal(originalText, output); + }); + + test('fixFormatPrintMessage round-trip preserves message', () => { + let orchestra = new Orchestra(path.join(__dirname, "../../../orchestrations")); + let originalText = "8=FIX.4.4\u00019=72\u000135=A\u000149=ACCEPTOR\u000156=INITIATOR\u000134=1\u000152=20190816-10:34:27.742\u000198=0\u0001108=30\u000110=012\u0001"; + + // Parse the message + let message = parseMessage(originalText); + if (!message) { + assert.fail("message failed to parse"); + return; + } + + // Convert back to FIX format + let output = fixFormatPrintMessage("", message, orchestra, null, 0); + output = output.trimEnd(); + + // Parse again + let reparsed = parseMessage(output); + if (!reparsed) { + assert.fail("reparsed message failed to parse"); + return; + } + + // Check that field counts match + assert.equal(message.fields.length, reparsed.fields.length); + + // Check that all field values match + for (let i = 0; i < message.fields.length; i++) { + assert.equal(message.fields[i].tag, reparsed.fields[i].tag); + assert.equal(message.fields[i].value, reparsed.fields[i].value); + } + }); + + test('parsePrettyPrintedMessage parses basic message', () => { + let prettyText = `Logon +{ + BeginString (8) FIX.4.4 + BodyLength (9) 72 + MsgType (35) A + SenderCompID (49) ACCEPTOR + TargetCompID (56) INITIATOR + MsgSeqNum (34) 1 + SendingTime (52) 20190816-10:34:27.742 + EncryptMethod (98) 0 + HeartBtInt (108) 30 + CheckSum (10) 012 +}`; + let message = parsePrettyPrintedMessage(prettyText); + if (!message) { + assert.fail("message failed to parse"); + return; + } + assert.equal(10, message.fields.length); + assert.equal(8, message.fields[0].tag); + assert.equal("FIX.4.4", message.fields[0].value); + assert.equal(35, message.fields[2].tag); + assert.equal("A", message.fields[2].value); + assert.equal("A", message.msgType); + }); + + test('parsePrettyPrintedMessage handles enumerated values', () => { + let prettyText = `NewOrderSingle +{ + BeginString (8) FIX.4.4 + MsgType (35) D - NewOrderSingle + Side (54) 1 - Buy + OrdType (40) 2 - Limit +}`; + let message = parsePrettyPrintedMessage(prettyText); + if (!message) { + assert.fail("message failed to parse"); + return; + } + assert.equal(4, message.fields.length); + // Check that enumerated values are parsed correctly (should extract "1" from "1 - Buy") + let sideField = message.fields.find(f => f.tag === 54); + assert.ok(sideField); + assert.equal("1", sideField.value); + + let ordTypeField = message.fields.find(f => f.tag === 40); + assert.ok(ordTypeField); + assert.equal("2", ordTypeField.value); + }); + + test('parsePrettyPrintedMessage handles NewOrderSingle', () => { + let prettyText = `NewOrderSingle +{ + BeginString (8) FIX.4.4 + MsgType (35) D - NewOrderSingle + SenderCompID (49) INITIATOR + TargetCompID (56) ACCEPTOR + ClOrdID (11) ORDER123 + Side (54) 1 - Buy + Symbol (55) AAPL + OrderQty (38) 100 + OrdType (40) 2 - Limit + Price (44) 150.50 +}`; + let message = parsePrettyPrintedMessage(prettyText); + if (!message) { + assert.fail("message failed to parse"); + return; + } + assert.equal(10, message.fields.length); + assert.equal("D", message.msgType); + + let clOrdIdField = message.fields.find(f => f.tag === 11); + assert.ok(clOrdIdField); + assert.equal("ORDER123", clOrdIdField.value); + + let priceField = message.fields.find(f => f.tag === 44); + assert.ok(priceField); + assert.equal("150.50", priceField.value); + }); + + test('parsePrettyPrintedMessage returns null for empty input', () => { + let prettyText = ``; + let message = parsePrettyPrintedMessage(prettyText); + assert.equal(null, message); + }); + + test('parsePrettyPrintedMessage returns null for no fields', () => { + let prettyText = `NewOrderSingle +{ +}`; + let message = parsePrettyPrintedMessage(prettyText); + assert.equal(null, message); + }); + + test('parsePrettyPrintedMessage handles message without header', () => { + let prettyText = `{ + BeginString (8) FIX.4.4 + MsgType (35) A +}`; + let message = parsePrettyPrintedMessage(prettyText); + if (!message) { + assert.fail("message failed to parse"); + return; + } + assert.equal(2, message.fields.length); + assert.equal("A", message.msgType); + }); + + test('parsePrettyPrintedMessage ignores non-field lines', () => { + let prettyText = `NewOrderSingle +{ + This is a comment line + BeginString (8) FIX.4.4 + Another comment + MsgType (35) D - NewOrderSingle + More comments here + Side (54) 1 - Buy +}`; + let message = parsePrettyPrintedMessage(prettyText); + if (!message) { + assert.fail("message failed to parse"); + return; + } + // Should only parse lines with (tag) pattern + assert.equal(3, message.fields.length); + assert.equal("D", message.msgType); + }); + + test('parsePrettyPrintedMessage round-trip with prettyPrintMessage', () => { + let orchestra = new Orchestra(path.join(__dirname, "../../../orchestrations")); + let originalRaw = "8=FIX.4.4\u00019=72\u000135=A\u000149=ACCEPTOR\u000156=INITIATOR\u000134=1\u000152=20190816-10:34:27.742\u000198=0\u0001108=30\u000110=012\u0001"; + + // Parse raw message + let originalMessage = parseMessage(originalRaw); + if (!originalMessage) { + assert.fail("original message failed to parse"); + return; + } + + // Convert to pretty print + let prettyText = prettyPrintMessage("", originalMessage, orchestra, null, 0); + + // Parse pretty printed back to message + let reparsedMessage = parsePrettyPrintedMessage(prettyText); + if (!reparsedMessage) { + assert.fail("reparsed message failed to parse"); + return; + } + + // Check that field counts match + assert.equal(originalMessage.fields.length, reparsedMessage.fields.length); + + // Check that all field values match + for (let i = 0; i < originalMessage.fields.length; i++) { + assert.equal(originalMessage.fields[i].tag, reparsedMessage.fields[i].tag); + assert.equal(originalMessage.fields[i].value, reparsedMessage.fields[i].value); + } + }); + + test('parsePrettyPrintedMessage full round-trip: raw -> pretty -> parse -> raw', () => { + let orchestra = new Orchestra(path.join(__dirname, "../../../orchestrations")); + let originalRaw = "8=FIX.4.4\u00019=140\u000135=D\u000149=INITIATOR\u000156=ACCEPTOR\u000134=2282\u000152=20190929-04:51:00.849\u000111=50\u000170=49\u0001100=AUTO\u000155=WTF\u000154=1\u000160=20190929-04:35:33.562\u000138=10000\u000140=1\u000159=1\u000110=129\u0001"; + + // Step 1: Parse raw FIX + let step1 = parseMessage(originalRaw); + if (!step1) { + assert.fail("step 1 failed"); + return; + } + + // Step 2: Convert to pretty print + let prettyText = prettyPrintMessage("", step1, orchestra, null, 0); + + // Step 3: Parse pretty printed + let step3 = parsePrettyPrintedMessage(prettyText); + if (!step3) { + assert.fail("step 3 failed"); + return; + } + + // Step 4: Convert back to raw FIX + let finalRaw = fixFormatPrintMessage("", step3, orchestra, null, 0); + finalRaw = finalRaw.trimEnd(); + + // Verify: original raw should equal final raw + assert.equal(originalRaw, finalRaw); + }); + + test('parsePrettyPrintedMessage handles fields with special characters', () => { + let prettyText = `NewOrderSingle +{ + BeginString (8) FIX.4.4 + MsgType (35) D - NewOrderSingle + Symbol (55) ABC.XYZ + Text (58) Test order with spaces and symbols! +}`; + let message = parsePrettyPrintedMessage(prettyText); + if (!message) { + assert.fail("message failed to parse"); + return; + } + + let symbolField = message.fields.find(f => f.tag === 55); + assert.ok(symbolField); + assert.equal("ABC.XYZ", symbolField.value); + + let textField = message.fields.find(f => f.tag === 58); + assert.ok(textField); + assert.equal("Test order with spaces and symbols!", textField.value); + }); }); From c3b1e9cd088b79af2078b15c68cfa91c73ded0a5 Mon Sep 17 00:00:00 2001 From: David Paz Date: Wed, 3 Dec 2025 16:14:49 +0100 Subject: [PATCH 3/5] Fix linter tooling Fix eslint configuration and add rules to mimic earlier configuration. --- .eslintrc.js | 58 ------- .prettierignore | 7 + .prettierrc | 9 ++ eslint.config.js | 50 ++++++ package-lock.json | 380 +++++++++++++++++++++++++++++++++++++++++++- package.json | 8 +- src/definitions.ts | 2 +- src/extension.ts | 32 ++-- src/fixOrchestra.ts | 2 +- 9 files changed, 464 insertions(+), 84 deletions(-) delete mode 100644 .eslintrc.js create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 eslint.config.js diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 4136590..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,58 +0,0 @@ -/* -👋 Hi! This file was autogenerated by tslint-to-eslint-config. -https://github.com/typescript-eslint/tslint-to-eslint-config - -It represents the closest reasonable ESLint configuration to this -project's original TSLint configuration. - -We recommend eventually switching this configuration to extend from -the recommended rulesets in typescript-eslint. -https://github.com/typescript-eslint/tslint-to-eslint-config/blob/master/docs/FAQs.md - -Happy linting! 💖 -*/ -module.exports = { - "env": { - "es6": true, - "node": true - }, - "extends": [ - "prettier", - "prettier/@typescript-eslint" - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "project": "tsconfig.json", - "sourceType": "module" - }, - "plugins": [ - "@typescript-eslint" - ], - "rules": { - "@typescript-eslint/member-delimiter-style": [ - "warn", - { - "multiline": { - "delimiter": "semi", - "requireLast": true - }, - "singleline": { - "delimiter": "semi", - "requireLast": false - } - } - ], - "@typescript-eslint/no-unused-expressions": "warn", - "@typescript-eslint/semi": [ - "warn", - "always" - ], - "curly": "warn", - "eqeqeq": [ - "warn", - "always" - ], - "no-redeclare": "warn", - "no-throw-literal": "warn" - } -}; diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..abc1cda --- /dev/null +++ b/.prettierignore @@ -0,0 +1,7 @@ +node_modules +out +.vscode-test +*.min.js +package-lock.json +orchestrations +images diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..9a8f729 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "semi": true, + "singleQuote": true, + "tabWidth": 4, + "useTabs": true, + "trailingComma": "es5", + "printWidth": 120, + "arrowParens": "avoid" +} diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..b47ac2a --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,50 @@ +const tsPlugin = require('@typescript-eslint/eslint-plugin'); +const tsParser = require('@typescript-eslint/parser'); +const prettierConfig = require('eslint-config-prettier'); + +module.exports = [ + { + files: ['src/**/*.ts'], + languageOptions: { + parser: tsParser, + parserOptions: { + project: './tsconfig.json', + sourceType: 'module', + }, + globals: { + console: 'readonly', + process: 'readonly', + __dirname: 'readonly', + module: 'readonly', + require: 'readonly', + exports: 'readonly', + }, + }, + plugins: { + '@typescript-eslint': tsPlugin, + }, + rules: { + // TypeScript specific rules + '@typescript-eslint/no-unused-expressions': 'warn', + + // General JavaScript/TypeScript rules + // Note: semi, quotes, indent rules are handled by Prettier + 'curly': 'warn', + 'eqeqeq': ['warn', 'always'], + 'no-redeclare': 'warn', + 'no-throw-literal': 'warn', + + // Disable rules that conflict with Prettier + ...prettierConfig.rules, + }, + }, + { + ignores: [ + 'node_modules/**', + 'out/**', + '.vscode-test/**', + '*.js', + '!eslint.config.js', + ], + }, +]; diff --git a/package-lock.json b/package-lock.json index 1533dda..8d0a851 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,10 +21,14 @@ "@types/mocha": "^10.0.1", "@types/node": "^20.12.7", "@types/vscode": "^1.88.0", + "@typescript-eslint/eslint-plugin": "^8.48.1", + "@typescript-eslint/parser": "^8.48.1", "@vscode/test-electron": "^2.3.4", "eslint": "^9.1.0", + "eslint-config-prettier": "^10.1.8", "glob": "^11.1.0", "mocha": "^10.8.2", + "prettier": "^3.7.4", "ts-loader": "^9.4.4", "typescript": "^5.1.6", "webpack": "^5.94.0", @@ -53,16 +57,20 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, + "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, + "funding": { + "url": "https://opencollective.com/eslint" + }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } @@ -388,6 +396,263 @@ "@types/node": "*" } }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.1.tgz", + "integrity": "sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.48.1", + "@typescript-eslint/type-utils": "8.48.1", + "@typescript-eslint/utils": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.48.1", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.1.tgz", + "integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.48.1", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/typescript-estree": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.1.tgz", + "integrity": "sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.48.1", + "@typescript-eslint/types": "^8.48.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.1.tgz", + "integrity": "sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.1.tgz", + "integrity": "sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.1.tgz", + "integrity": "sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/typescript-estree": "8.48.1", + "@typescript-eslint/utils": "8.48.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.1.tgz", + "integrity": "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.1.tgz", + "integrity": "sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.48.1", + "@typescript-eslint/tsconfig-utils": "8.48.1", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1", + "debug": "^4.3.4", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.1.tgz", + "integrity": "sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.48.1", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/typescript-estree": "8.48.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.1.tgz", + "integrity": "sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.48.1", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@vscode/test-electron": { "version": "2.3.9", "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.3.9.tgz", @@ -1220,6 +1485,22 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, "node_modules/eslint-scope": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.1.tgz", @@ -1237,10 +1518,11 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", - "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -1551,6 +1833,13 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2418,6 +2707,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", + "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -2944,6 +3249,54 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -2956,6 +3309,19 @@ "node": ">=8.0" } }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/ts-loader": { "version": "9.5.1", "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz", diff --git a/package.json b/package.json index 827cf72..766e1c4 100644 --- a/package.json +++ b/package.json @@ -267,16 +267,22 @@ "watch": "tsc -watch -p ./", "pretest": "npm run compile", "test": "node ./out/test/runTest.js", - "lint": "eslint -c .eslintrc.js --ext .ts ./" + "lint": "eslint .", + "format": "prettier --write \"src/**/*.ts\"", + "format:check": "prettier --check \"src/**/*.ts\"" }, "devDependencies": { "@types/mocha": "^10.0.1", "@types/node": "^20.12.7", "@types/vscode": "^1.88.0", + "@typescript-eslint/eslint-plugin": "^8.48.1", + "@typescript-eslint/parser": "^8.48.1", "@vscode/test-electron": "^2.3.4", "eslint": "^9.1.0", + "eslint-config-prettier": "^10.1.8", "glob": "^11.1.0", "mocha": "^10.8.2", + "prettier": "^3.7.4", "ts-loader": "^9.4.4", "typescript": "^5.1.6", "webpack": "^5.94.0", diff --git a/src/definitions.ts b/src/definitions.ts index ff6ae79..978be30 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -67,7 +67,7 @@ export class Field { public toString = () : string => { return `Field(${this.name}=${this.tag})`; - } + }; } // diff --git a/src/extension.ts b/src/extension.ts index aecd6dd..160d102 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -25,23 +25,23 @@ export function activate(context: ExtensionContext) { } return workspace.workspaceFolders[0].uri.path; - } + }; const resolvePathVariables = (path: string, context: string) : string | undefined => { - if (path.search(/\${workspaceFolder}/g) == -1) { + if (path.search(/\${workspaceFolder}/g) === -1) { return path; } let workspaceFolder = getWorkspaceFolder(); - if (workspaceFolder == undefined) { + if (workspaceFolder === undefined) { window.showErrorMessage("Unable to determine ${workspaceFolder} when resolving the " + context + " path."); return undefined; } - return path.replace(/\${workspaceFolder}/g, workspaceFolder) - } + return path.replace(/\${workspaceFolder}/g, workspaceFolder); + }; const loadOrchestra = () => { @@ -55,7 +55,7 @@ export function activate(context: ExtensionContext) { orchestraPath = resolvePathVariables(orchestraPath, "orchestra"); - if (orchestraPath == undefined) { + if (orchestraPath === undefined) { return; } @@ -65,7 +65,7 @@ export function activate(context: ExtensionContext) { if (!fs.existsSync(orchestraPath)) { window.showErrorMessage("The orchestra path '" + orchestraPath + "' cannot be found."); - return + return; } window.withProgress({ @@ -99,7 +99,7 @@ export function activate(context: ExtensionContext) { path = resolvePathVariables(path, "QuickFix data dictionary"); - if (path == undefined) { + if (path === undefined) { return; } @@ -202,19 +202,19 @@ export function activate(context: ExtensionContext) { let getDocument = async (editorReuse:EditorReuse) : Promise => { - if (editorReuse != EditorReuse.New) { - const document = workspace.textDocuments.find(document => { return document.languageId === 'FIX' }) + if (editorReuse !== EditorReuse.New) { + const document = workspace.textDocuments.find(document => { return document.languageId === 'FIX'; }); if (document) { if (editorReuse === EditorReuse.Replace) { let edit = new WorkspaceEdit(); var firstLine = document.lineAt(0); var lastLine = document.lineAt(document.lineCount - 1); var textRange = new Range(firstLine.range.start, lastLine.range.end); - edit.delete(document.uri, textRange) + edit.delete(document.uri, textRange); await workspace.applyEdit(edit); } - await window.showTextDocument(document) - return document + await window.showTextDocument(document); + return document; } } @@ -248,17 +248,17 @@ export function activate(context: ExtensionContext) { const configuration = workspace.getConfiguration(); const editorReuse = EditorReuse[configuration.get("fixmaster.editorReuse") as keyof typeof EditorReuse]; - let document = await getDocument(editorReuse) + let document = await getDocument(editorReuse); let edit = new WorkspaceEdit(); var index = 0; if (editorReuse === EditorReuse.Append) { - index = document.lineCount - 1 + index = document.lineCount - 1; } - if (scope == CommandScope.Document) { + if (scope === CommandScope.Document) { edit.insert(document.uri, new Position(index, 0), sourceDocument.getText()); } else { diff --git a/src/fixOrchestra.ts b/src/fixOrchestra.ts index 51d3c90..5441bfc 100644 --- a/src/fixOrchestra.ts +++ b/src/fixOrchestra.ts @@ -15,7 +15,7 @@ export class Orchestra this.orchestrations = filenames.map(entry => { const orchestraPath = path.join(root, entry); try { - return new xml.Orchestration(orchestraPath) + return new xml.Orchestration(orchestraPath); } catch (err) { throw new Error(`failed to load orchestra file '${orchestraPath}' - ${err}`); } From 7533407f663386463e84b0e9f518ac43b55a7dec Mon Sep 17 00:00:00 2001 From: David Paz Date: Wed, 3 Dec 2025 16:18:00 +0100 Subject: [PATCH 4/5] Add tests tasks to the github workflow file Run tests on the workflow file --- .github/workflows/nodejs.yml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 066e901..e52cd21 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -26,6 +26,14 @@ jobs: node-version: ${{ matrix.node-version }} - run: npm ci - run: npm run build --if-present -# - run: npm test -# env: -# CI: true + - run: sudo apt-get update && sudo apt-get install -y xvfb + - name: Start Xvfb + run: | + Xvfb :99 -screen 0 1280x1024x24 -ac &>/dev/null & + sleep 1 + shell: bash + - name: Run tests + run: npm test + env: + DISPLAY: :99 + CI: true From a5b71e211906f63c680e9df4280d78a4eea685a7 Mon Sep 17 00:00:00 2001 From: David Paz Date: Thu, 4 Dec 2025 13:39:42 +0100 Subject: [PATCH 5/5] Parse printed mesages with normalized line endings Parse pretty printed messages while normalizing line endings to it is agnostic to whatever OS is the user using. Not using VSCode api for determining the line ending since the function uses a string as parameter and there is not Window or Document object involved in current functionality. Just good old search and replace to assume consistent line endings. --- src/fixProtocol.ts | 3 +- src/test/suite/fixProtocol.test.ts | 70 ++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/src/fixProtocol.ts b/src/fixProtocol.ts index 35a3f31..5b832be 100644 --- a/src/fixProtocol.ts +++ b/src/fixProtocol.ts @@ -392,7 +392,8 @@ export function parsePrettyPrintedMessage(text: string): Message | null { // or: FieldName (tag) value - description (for enumerated values) const fieldPattern = /^\s*\w+\s+\((\d+)\)\s+(.+?)(?:\s+-\s+.+)?$/; - const lines = text.split('\n'); + // Normalize line endings to handle both Unix (\n) and Windows (\r\n) + const lines = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n'); const fields: Field[] = []; var msgType: string | null = null; var inMessage = false; diff --git a/src/test/suite/fixProtocol.test.ts b/src/test/suite/fixProtocol.test.ts index 6e48605..67b82a7 100644 --- a/src/test/suite/fixProtocol.test.ts +++ b/src/test/suite/fixProtocol.test.ts @@ -392,4 +392,74 @@ suite('FIX Protocol Test Suite', () => { assert.ok(textField); assert.equal("Test order with spaces and symbols!", textField.value); }); + + test('parsePrettyPrintedMessage normalizes Windows line endings (CRLF)', () => { + // Use \r\n (Windows) line endings + let prettyText = "NewOrderSingle\r\n{\r\n BeginString (8) FIX.4.4\r\n MsgType (35) D - NewOrderSingle\r\n Side (54) 1 - Buy\r\n}"; + let message = parsePrettyPrintedMessage(prettyText); + if (!message) { + assert.fail("message failed to parse with Windows line endings"); + return; + } + assert.equal(3, message.fields.length); + assert.equal(8, message.fields[0].tag); + assert.equal("FIX.4.4", message.fields[0].value); + assert.equal(35, message.fields[1].tag); + assert.equal("D", message.fields[1].value); + assert.equal(54, message.fields[2].tag); + assert.equal("1", message.fields[2].value); + assert.equal("D", message.msgType); + }); + + test('parsePrettyPrintedMessage normalizes old Mac line endings (CR)', () => { + // Use \r (old Mac) line endings + let prettyText = "NewOrderSingle\r{\r BeginString (8) FIX.4.4\r MsgType (35) D - NewOrderSingle\r Side (54) 1 - Buy\r}"; + let message = parsePrettyPrintedMessage(prettyText); + if (!message) { + assert.fail("message failed to parse with old Mac line endings"); + return; + } + assert.equal(3, message.fields.length); + assert.equal(8, message.fields[0].tag); + assert.equal("FIX.4.4", message.fields[0].value); + assert.equal(35, message.fields[1].tag); + assert.equal("D", message.fields[1].value); + assert.equal(54, message.fields[2].tag); + assert.equal("1", message.fields[2].value); + assert.equal("D", message.msgType); + }); + + test('parsePrettyPrintedMessage normalizes mixed line endings', () => { + // Mix of \r\n (Windows), \r (old Mac), and \n (Unix) + let prettyText = "NewOrderSingle\r\n{\r BeginString (8) FIX.4.4\n MsgType (35) D - NewOrderSingle\r\n Side (54) 1 - Buy\r}"; + let message = parsePrettyPrintedMessage(prettyText); + if (!message) { + assert.fail("message failed to parse with mixed line endings"); + return; + } + assert.equal(3, message.fields.length); + assert.equal(8, message.fields[0].tag); + assert.equal("FIX.4.4", message.fields[0].value); + assert.equal(35, message.fields[1].tag); + assert.equal("D", message.fields[1].value); + assert.equal(54, message.fields[2].tag); + assert.equal("1", message.fields[2].value); + assert.equal("D", message.msgType); + }); + + test('parsePrettyPrintedMessage with Windows line endings round-trip', () => { + // Verify Windows line endings work with full message parsing + let prettyText = "Logon\r\n{\r\n BeginString (8) FIX.4.4\r\n BodyLength (9) 72\r\n MsgType (35) A\r\n SenderCompID (49) ACCEPTOR\r\n TargetCompID (56) INITIATOR\r\n MsgSeqNum (34) 1\r\n SendingTime (52) 20190816-10:34:27.742\r\n EncryptMethod (98) 0\r\n HeartBtInt (108) 30\r\n CheckSum (10) 012\r\n}"; + let message = parsePrettyPrintedMessage(prettyText); + if (!message) { + assert.fail("message failed to parse with Windows line endings"); + return; + } + assert.equal(10, message.fields.length); + assert.equal(8, message.fields[0].tag); + assert.equal("FIX.4.4", message.fields[0].value); + assert.equal(35, message.fields[2].tag); + assert.equal("A", message.fields[2].value); + assert.equal("A", message.msgType); + }); });