rodrigoavf.dev
← All writing

Removing All DAX Comments From a Model in One Pass

A Tabular Editor 2 script that strips every comment from a model's measures without touching text inside DAX strings.

6 min read

The problem

Every measure I write picks up comments as I go. A // note about why a filter is there, a /* */ block I used to disable half a CALCULATE() while debugging, a leftover reminder to fix something later. That's fine during development, it's how DAX thinking usually happens for me.

The problem shows up later, when the model is done and I want to hand it off, publish it, or just look at it with fresh eyes. Dozens of measures, each with a few lines of scratch notes mixed into the actual logic. Going through them one at a time in Tabular Editor's expression editor to delete comments by hand is slow, and it's exactly the kind of repetitive task I'd rather not do manually.

Tabular Editor doesn't have a built-in "strip comments" action. There's no menu item for this, no DAX function. The only option was doing it by hand, or writing something to do it for me.

What I built

Tabular Editor has an Advanced Scripting window that runs plain C# against the model, with Model already in scope. That's the natural place for a one-off cleanup script like this: paste it in, run it once, done.

The naive version is a couple of regex replacements for //.*$ and /\*.*?\*/. That breaks the moment a measure has a text string containing something that looks like a comment, which happens more often than you'd think. A measure returning a status label like "In Progress // Review" would get silently mangled, and that's worse than not touching it at all.

So instead of regex, the script walks each expression character by character and only strips comments when it's outside of a double-quoted string:

// Removes comments from the DAX expressions of SELECTED measures, 
// SELECTED calculated columns, AND all measures/columns inside SELECTED TABLES.
//
// Handles //, -- and /* */ comments.
// Preserves text inside double-quoted DAX strings (including escaped "" quotes).
//
// Comments that start with the string in `keepCommentPrefix` (after trimming
// leading whitespace inside the comment) are LEFT IN PLACE instead of removed.
// Leave `keepCommentPrefix` empty ("") to remove ALL comments.
 
using System;
using System.Text;
using System.Linq;
using System.Collections.Generic;
using TabularEditor.TOMWrapper;
 
// ==================== CONFIGURATION ====================
// Comments starting with this string are preserved. Case-sensitive.
// Example: "KEEP:" will preserve "// KEEP: don't touch this"
// Leave as "" to delete every comment.
string keepCommentPrefix = "KEEP:";
// =========================================================
 
int changed = 0;
 
Func<string, string, string> RemoveComments = (input, keepPrefix) =>
{
    var sb = new StringBuilder(input.Length);
    int i = 0;
    int n = input.Length;
    bool inString = false;
    bool commentRemoved = false; // Tracks if we actually deleted anything
 
    while (i < n)
    {
        char c = input[i];
 
        if (inString)
        {
            sb.Append(c);
            if (c == '"')
            {
                // Escaped double quote inside DAX string ("")
                if (i + 1 < n && input[i + 1] == '"')
                {
                    sb.Append('"');
                    i += 2;
                    continue;
                }
                inString = false;
                i++;
                continue;
            }
            i++;
            continue;
        }
 
        // Start of string?
        if (c == '"')
        {
            inString = true;
            sb.Append(c);
            i++;
            continue;
        }
 
        // Line comment: // ... or -- ... (until newline)
        bool isSlashComment = (c == '/' && i + 1 < n && input[i + 1] == '/');
        bool isDashComment = (c == '-' && i + 1 < n && input[i + 1] == '-');
 
        if (isSlashComment || isDashComment)
        {
            int start = i;
            i += 2;
            int contentStart = i;
            while (i < n && input[i] != '\n' && input[i] != '\r') i++;
            int contentEnd = i;
 
            string commentBody = input.Substring(contentStart, contentEnd - contentStart);
            bool keep = !string.IsNullOrEmpty(keepPrefix) &&
                        commentBody.TrimStart().StartsWith(keepPrefix, StringComparison.Ordinal);
 
            if (keep) 
            {
                sb.Append(input.Substring(start, i - start));
            } 
            else 
            {
                commentRemoved = true; // We successfully stripped a comment
            }
 
            continue;
        }
 
        // Block comment: /* ... */
        if (c == '/' && i + 1 < n && input[i + 1] == '*')
        {
            int start = i;
            i += 2;
            int contentStart = i;
            while (i + 1 < n && !(input[i] == '*' && input[i + 1] == '/')) i++;
            int contentEnd = i; // exclusive of closing */
            bool hasClosing = (i + 1 < n);
            if (hasClosing) i += 2; // skip closing */
            else i = n;
 
            string commentBody = input.Substring(contentStart, contentEnd - contentStart);
            bool keep = !string.IsNullOrEmpty(keepPrefix) &&
                        commentBody.TrimStart().StartsWith(keepPrefix, StringComparison.Ordinal);
 
            if (keep) 
            {
                sb.Append(input.Substring(start, i - start));
            }
            else 
            {
                commentRemoved = true; // We successfully stripped a comment
            }
 
            continue;
        }
 
        // Regular char
        sb.Append(c);
        i++;
    }
    
    // If no comments were stripped, return the exact original DAX (trailing spaces and all)
    if (!commentRemoved) 
    {
        return input;
    }
 
    // Tidy up ONLY if comments were removed: trim trailing spaces per line & collapse consecutive blank lines
    var withoutComments = sb.ToString();
    var lines = withoutComments.Replace("\r\n", "\n").Replace("\r", "\n").Split('\n');
    var outSb = new StringBuilder(withoutComments.Length);
    bool lastBlank = false;
    bool firstLine = true;
 
    for (int k = 0; k < lines.Length; k++)
    {
        string line = lines[k].TrimEnd();
        bool isBlank = (line.Length == 0);
 
        if (isBlank)
        {
            if (!lastBlank)
            {
                if (!firstLine) outSb.Append('\n');
                lastBlank = true;
                firstLine = false;
            }
        }
        else
        {
            if (!firstLine) outSb.Append('\n');
            outSb.Append(line);
            lastBlank = false;
            firstLine = false;
        }
    }
 
    return outSb.ToString();
};
 
// ======================================================================
// GATHER TARGET OBJECTS
// We use a HashSet to avoid processing the same object twice.
// ======================================================================
 
var targetMeasures = new HashSet<Measure>();
var targetColumns = new HashSet<CalculatedColumn>();
 
// 1. Add explicitly selected measures & columns
foreach (var m in Selected.Measures) targetMeasures.Add(m);
foreach (var col in Selected.Columns.OfType<CalculatedColumn>()) targetColumns.Add(col);
 
// 2. Add all measures & columns from explicitly selected tables
foreach (var table in Selected.Tables)
{
    foreach (var m in table.Measures) targetMeasures.Add(m);
    foreach (var col in table.Columns.OfType<CalculatedColumn>()) targetColumns.Add(col);
}
 
// ======================================================================
// PROCESS TARGET OBJECTS
// ======================================================================
 
// ---- Process measures ----
foreach (var m in targetMeasures)
{
    var input = m.Expression;
    if (string.IsNullOrEmpty(input)) continue;
 
    var cleaned = RemoveComments(input, keepCommentPrefix);
    if (input != cleaned)
    {
        m.Expression = cleaned;
        changed++;
    }
}
 
// ---- Process calculated columns ----
foreach (var col in targetColumns)
{
    var input = col.Expression;
    if (string.IsNullOrEmpty(input)) continue;
 
    var cleaned = RemoveComments(input, keepCommentPrefix);
    if (input != cleaned)
    {
        col.Expression = cleaned;
        changed++;
    }
}
 
Info("Removed comments from " + changed + " object(s).");

The inString flag is the whole trick. Once the parser hits an opening ", everything gets copied through untouched, including a // or /* sitting inside the string, until it finds the matching closing ". It also checks for "", which is how DAX escapes a literal quote character inside a string, so it doesn't close the string one character too early on something like "She said ""hello""".

Stripping comments leaves gaps behind: blank lines where a // note used to sit on its own line, trailing spaces where a comment used to trail a code line. The second pass cleans that up, trimming trailing whitespace on every line and collapsing runs of blank lines down to one, so the result reads like a measure written clean rather than one that had text torn out of it.

It only rewrites m.Expression when the cleaned version actually differs from the original, and keeps a count of how many measures changed. Running it on a model prints something like:

Removed comments from 14 measure(s).

To use it: open the model in Tabular Editor 2, open the Advanced Scripting window, paste the script in, hit run. No selection needed, it walks every measure in the model on its own.

What's still rough

Since it writes to m.Expression directly, undo relies entirely on Tabular Editor's own undo history rather than anything the script itself provides. Worth keeping a saved copy of the model before running it on anything you can't easily revert.