#9510·stylelint

Fix `--fix` not writing a file whose only change is the removal of its byte order mark

Author: firefoxicCreated Sep 14, 2026Updated Sep 15, 2026
Labelsstatus: ready to implement

What minimal example or steps are needed to reproduce the bug?

A stylesheet that starts with a byte order mark, and a plugin rule whose fix removes the mark by clearing the flag PostCSS keeps it in:

css
a { color: red; }
javascript
// plugin.mjs
import stylelint from "stylelint";

const ruleName = "plugin/bom";

const rule = (primary) => (root, result) => {
	const { input } = root.source;

	if (input.hasBOM === (primary === "always")) return;

	stylelint.utils.report({
		result,
		ruleName,
		message: primary === "always" ? "Expected Unicode BOM" : "Unexpected Unicode BOM",
		node: root,
		fix: () => {
			input.hasBOM = primary === "always";
		},
	});
};

rule.ruleName = ruleName;
rule.meta = { fixable: true };

export default stylelint.createPlugin(ruleName, rule);

What minimal configuration is needed to reproduce the bug?

json
{
  "plugins": ["./plugin.mjs"],
  "rules": {
    "plugin/bom": "never"
  }
}

How did you run Stylelint?

bash
stylelint --fix a.css

Which Stylelint-related dependencies are you using?

json
{
  "stylelint": "17.14.1",
  "postcss": "8.5.26"
}

What did you expect to happen?

The file is written back without the mark, and a second run reports nothing.

What actually happened?

The run exits with 0 and reports nothing, but the file on disk still starts with the mark, and the next run reports Unexpected Unicode BOM again.

The cause is the check that decides whether a fixed file is written, in lib/standalone.mjs:

javascript
postcssResult.root.source.input.css !== fixedCss

PostCSS strips the mark from Input.css while building the input and records it as Input.hasBOM; the stringifier prints the mark back for a root whose input carries the flag. So after a fix that clears the flag, fixedCss is the stripped text and equals input.css, and nothing is written. The code path is not affected, since it compares against the code that was passed in, mark included: stylelint.lint({ code, fix: true }) returns the code without the mark, and so does the same rule in an editor extension.

The same comparison also misreports the opposite case: a file that starts with a mark and needs no fix is rewritten on every --fix run and reported as autofixed, because the printed text carries the mark and input.css does not.

Do you have a proposal to fix the bug?

Compare the fixed CSS against the source as it was read rather than against input.css: record input.hasBOM on the result before the rules run, and rebuild the source from that flag and input.css. Two lines in lib/lintSource.mjs and lib/standalone.mjs; every syntax Stylelint is commonly run with (postcss, postcss-scss, postcss-less, postcss-html, postcss-styled-syntax) prints a file-head mark back, measured, so the only files whose outcome changes are the two above.

I have the change with tests ready and can open a pull request.