Now flags only .pipe calls which have an error somewhere down the stream, but not on the source stream.

This commit is contained in:
Napalys Klicius
2025-05-28 17:17:43 +02:00
parent 5214cc0407
commit 5bb29b6e33
4 changed files with 183 additions and 27 deletions

View File

@@ -110,8 +110,11 @@ string getStreamMethodName() {
* A call to register an event handler on a Node.js stream.
* This includes methods like `on`, `once`, and `addListener`.
*/
class StreamEventRegistration extends DataFlow::MethodCallNode {
StreamEventRegistration() { this.getMethodName() = getEventHandlerMethodName() }
class ErrorHandlerRegistration extends DataFlow::MethodCallNode {
ErrorHandlerRegistration() {
this.getMethodName() = getEventHandlerMethodName() and
this.getArgument(0).getStringValue() = "error"
}
}
/**
@@ -136,7 +139,12 @@ predicate streamFlowStep(DataFlow::Node streamNode, DataFlow::Node relatedNode)
* Tracks the result of a pipe call as it flows through the program.
*/
private DataFlow::SourceNode pipeResultTracker(DataFlow::TypeTracker t, PipeCall pipe) {
t.start() and result = pipe
t.start() and result = pipe.getALocalSource()
or
exists(DataFlow::SourceNode prev |
prev = pipeResultTracker(t.continue(), pipe) and
streamFlowStep(result.getALocalUse(), prev)
)
or
exists(DataFlow::TypeTracker t2 | result = pipeResultTracker(t2, pipe).track(t2, t))
}
@@ -166,7 +174,7 @@ predicate isPipeFollowedByNonStreamMethod(PipeCall pipeCall) {
predicate isPipeFollowedByNonStreamProperty(PipeCall pipeCall) {
exists(DataFlow::PropRef propRef |
propRef = pipeResultRef(pipeCall).getAPropertyRead() and
not propRef.getPropertyName() = getStreamPropertyName()
not propRef.getPropertyName() = [getStreamPropertyName(), getStreamMethodName()]
)
}
@@ -206,9 +214,8 @@ private DataFlow::SourceNode streamRef(PipeCall pipeCall) {
* Holds if the source stream of the given pipe call has an `error` handler registered.
*/
predicate hasErrorHandlerRegistered(PipeCall pipeCall) {
exists(StreamEventRegistration handler |
handler = streamRef(pipeCall).getAMethodCall(getEventHandlerMethodName()) and
handler.getArgument(0).getStringValue() = "error"
exists(ErrorHandlerRegistration handler |
handler = streamRef(pipeCall).getAMethodCall(getEventHandlerMethodName())
)
or
hasPlumber(pipeCall)
@@ -218,6 +225,8 @@ predicate hasErrorHandlerRegistered(PipeCall pipeCall) {
* Holds if one of the arguments of the pipe call is a `gulp-plumber` monkey patch.
*/
predicate hasPlumber(PipeCall pipeCall) {
pipeCall.getDestinationStream().getALocalSource() = API::moduleImport("gulp-plumber").getACall()
or
streamRef+(pipeCall) = API::moduleImport("gulp-plumber").getACall()
}
@@ -246,9 +255,19 @@ private predicate hasNonStreamSourceLikeUsage(PipeCall pipeCall) {
)
}
/**
* Holds if the pipe call destination stream has an error handler registered.
*/
predicate hasErrorHandlerDownstream(PipeCall pipeCall) {
exists(ErrorHandlerRegistration handler |
handler.getReceiver().getALocalSource() = pipeResultRef(pipeCall)
)
}
from PipeCall pipeCall
where
not hasErrorHandlerRegistered(pipeCall) and
hasErrorHandlerDownstream(pipeCall) and
not isPipeFollowedByNonStreamAccess(pipeCall) and
not hasNonStreamSourceLikeUsage(pipeCall) and
not hasNonNodeJsStreamSource(pipeCall)

View File

@@ -5,9 +5,16 @@
| test.js:66:5:66:21 | stream.pipe(dest) | Stream pipe without error handling on the source stream. Errors won't propagate downstream and may be silently dropped. |
| test.js:79:5:79:25 | s2.pipe ... ation2) | Stream pipe without error handling on the source stream. Errors won't propagate downstream and may be silently dropped. |
| test.js:94:5:94:21 | stream.pipe(dest) | Stream pipe without error handling on the source stream. Errors won't propagate downstream and may be silently dropped. |
| test.js:109:26:109:37 | s.pipe(dest) | Stream pipe without error handling on the source stream. Errors won't propagate downstream and may be silently dropped. |
| test.js:116:5:116:21 | stream.pipe(dest) | Stream pipe without error handling on the source stream. Errors won't propagate downstream and may be silently dropped. |
| test.js:125:5:125:26 | getStre ... e(dest) | Stream pipe without error handling on the source stream. Errors won't propagate downstream and may be silently dropped. |
| test.js:143:5:143:62 | stream. ... itable) | Stream pipe without error handling on the source stream. Errors won't propagate downstream and may be silently dropped. |
| test.js:175:17:175:40 | notStre ... itable) | Stream pipe without error handling on the source stream. Errors won't propagate downstream and may be silently dropped. |
| test.js:185:5:185:32 | copyStr ... nation) | Stream pipe without error handling on the source stream. Errors won't propagate downstream and may be silently dropped. |
| test.js:110:11:110:22 | s.pipe(dest) | Stream pipe without error handling on the source stream. Errors won't propagate downstream and may be silently dropped. |
| test.js:119:5:119:21 | stream.pipe(dest) | Stream pipe without error handling on the source stream. Errors won't propagate downstream and may be silently dropped. |
| test.js:128:5:128:26 | getStre ... e(dest) | Stream pipe without error handling on the source stream. Errors won't propagate downstream and may be silently dropped. |
| test.js:146:5:146:62 | stream. ... itable) | Stream pipe without error handling on the source stream. Errors won't propagate downstream and may be silently dropped. |
| test.js:182:17:182:40 | notStre ... itable) | Stream pipe without error handling on the source stream. Errors won't propagate downstream and may be silently dropped. |
| test.js:192:5:192:32 | copyStr ... nation) | Stream pipe without error handling on the source stream. Errors won't propagate downstream and may be silently dropped. |
| tst.js:8:5:8:21 | source.pipe(gzip) | Stream pipe without error handling on the source stream. Errors won't propagate downstream and may be silently dropped. |
| tst.js:29:5:29:40 | wrapper ... Stream) | Stream pipe without error handling on the source stream. Errors won't propagate downstream and may be silently dropped. |
| tst.js:37:21:37:56 | wrapper ... Stream) | Stream pipe without error handling on the source stream. Errors won't propagate downstream and may be silently dropped. |
| tst.js:44:5:44:40 | wrapper ... Stream) | Stream pipe without error handling on the source stream. Errors won't propagate downstream and may be silently dropped. |
| tst.js:59:18:59:39 | stream. ... Stream) | Stream pipe without error handling on the source stream. Errors won't propagate downstream and may be silently dropped. |
| tst.js:73:5:73:40 | wrapper ... Stream) | Stream pipe without error handling on the source stream. Errors won't propagate downstream and may be silently dropped. |
| tst.js:111:5:111:26 | stream. ... Stream) | Stream pipe without error handling on the source stream. Errors won't propagate downstream and may be silently dropped. |

View File

@@ -1,7 +1,7 @@
function test() {
{
const stream = getStream();
stream.pipe(destination); // $Alert
stream.pipe(destination).on("error", e); // $Alert
}
{
const stream = getStream();
@@ -16,7 +16,7 @@ function test() {
{
const stream = getStream();
const s2 = stream;
s2.pipe(dest); // $Alert
s2.pipe(dest).on("error", e); // $Alert
}
{
const stream = getStream();
@@ -42,7 +42,7 @@ function test() {
const stream = getStream();
stream.on('error', handleError);
const stream2 = stream.pipe(destination);
stream2.pipe(destination2); // $Alert
stream2.pipe(destination2).on("error", e); // $Alert
}
{
const stream = getStream();
@@ -57,13 +57,13 @@ function test() {
const stream = getStream();
stream.on('error', handleError);
const stream2 = stream.pipe(destination);
stream2.pipe(destination2); // $Alert
stream2.pipe(destination2).on("error", e); // $Alert
}
{ // Error handler on destination instead of source
const stream = getStream();
const dest = getDest();
dest.on('error', handler);
stream.pipe(dest); // $Alert
stream.pipe(dest).on("error", e); // $Alert
}
{ // Multiple aliases, error handler on one
const stream = getStream();
@@ -76,7 +76,7 @@ function test() {
const stream = getStream();
const s2 = stream.pipe(destination1);
stream.on('error', handleError);
s2.pipe(destination2); // $Alert
s2.pipe(destination2).on("error", e); // $Alert
}
{ // Handler registered via .once
const stream = getStream();
@@ -91,7 +91,7 @@ function test() {
{ // Handler registered for unrelated event
const stream = getStream();
stream.on('close', handleClose);
stream.pipe(dest); // $Alert
stream.pipe(dest).on("error", e); // $Alert
}
{ // Error handler registered after pipe, but before error
const stream = getStream();
@@ -106,14 +106,17 @@ function test() {
}
{ // Pipe in a function, error handler not set
const stream = getStream();
function doPipe(s) { s.pipe(dest); } // $Alert
function doPipe(s) {
f = s.pipe(dest); // $Alert
f.on("error", e);
}
doPipe(stream);
}
{ // Dynamic event assignment
const stream = getStream();
const event = 'error';
stream.on(event, handleError);
stream.pipe(dest); // $SPURIOUS:Alert
stream.pipe(dest).on("error", e); // $SPURIOUS:Alert
}
{ // Handler assigned via variable property
const stream = getStream();
@@ -122,12 +125,12 @@ function test() {
stream.pipe(dest);
}
{ // Pipe with no intermediate variable, no error handler
getStream().pipe(dest); // $Alert
getStream().pipe(dest).on("error", e); // $Alert
}
{ // Handler set via .addListener synonym
const stream = getStream();
stream.addListener('error', handleError);
stream.pipe(dest);
stream.pipe(dest).on("error", e);
}
{ // Handler set via .once after .pipe
const stream = getStream();
@@ -140,7 +143,11 @@ function test() {
}
{ // Long chained pipe without error handler
const stream = getStream();
stream.pause().setEncoding('utf8').resume().pipe(writable); // $Alert
stream.pause().setEncoding('utf8').resume().pipe(writable).on("error", e); // $Alert
}
{ // Long chained pipe without error handler
const stream = getStream();
stream.pause().setEncoding('utf8').on("error", e).resume().pipe(writable).on("error", e);
}
{ // Non-stream with pipe method that returns subscribable object (Streams do not have subscribe method)
const notStream = getNotAStream();
@@ -172,7 +179,7 @@ function test() {
}
{ // Member access on a stream after pipe
const notStream = getNotAStream();
const val = notStream.pipe(writable).readable; // $Alert
const val = notStream.pipe(writable).on("error", e).readable; // $Alert
}
{ // Method access on a non-stream after pipe
const notStream = getNotAStream();
@@ -182,7 +189,7 @@ function test() {
const fs = require('fs');
const stream = fs.createReadStream('file.txt');
const copyStream = stream;
copyStream.pipe(destination); // $Alert
copyStream.pipe(destination).on("error", e); // $Alert
}
{
const notStream = getNotAStream();
@@ -211,6 +218,16 @@ function test() {
const p = plumber();
getStream().pipe(p).pipe(dest).pipe(dest).pipe(dest);
}
{
const plumber = require('gulp-plumber');
const p = plumber();
getStream().pipe(p);
}
{
const plumber = require('gulp-plumber');
const p = plumber();
getStream().pipe(p).pipe(dest);
}
{
const notStream = getNotAStream();
notStream.pipe(getStream(),()=>{});

View File

@@ -0,0 +1,113 @@
const fs = require('fs');
const zlib = require('zlib');
function foo(){
const source = fs.createReadStream('input.txt');
const gzip = zlib.createGzip();
const destination = fs.createWriteStream('output.txt.gz');
source.pipe(gzip).pipe(destination); // $Alert
gzip.on('error', e);
}
class StreamWrapper {
constructor() {
this.outputStream = getStream();
}
}
function zip() {
const zipStream = createWriteStream(zipPath);
let wrapper = new StreamWrapper();
let stream = wrapper.outputStream;
stream.on('error', e);
stream.pipe(zipStream);
zipStream.on('error', e);
}
function zip1() {
const zipStream = createWriteStream(zipPath);
let wrapper = new StreamWrapper();
wrapper.outputStream.pipe(zipStream); // $SPURIOUS:Alert
wrapper.outputStream.on('error', e);
zipStream.on('error', e);
}
function zip2() {
const zipStream = createWriteStream(zipPath);
let wrapper = new StreamWrapper();
let outStream = wrapper.outputStream.pipe(zipStream); // $Alert
outStream.on('error', e);
}
function zip3() {
const zipStream = createWriteStream(zipPath);
let wrapper = new StreamWrapper();
wrapper.outputStream.pipe(zipStream); // $Alert
zipStream.on('error', e);
}
function zip3() {
const zipStream = createWriteStream(zipPath);
let wrapper = new StreamWrapper();
let source = getStream();
source.pipe(wrapper.outputStream); // $MISSING:Alert
wrapper.outputStream.on('error', e);
}
function zip4() {
const zipStream = createWriteStream(zipPath);
let stream = getStream();
let output = stream.pipe(zipStream); // $Alert
output.on('error', e);
}
class StreamWrapper2 {
constructor() {
this.outputStream = getStream();
this.outputStream.on('error', e);
}
}
function zip5() {
const zipStream = createWriteStream(zipPath);
let wrapper = new StreamWrapper2();
wrapper.outputStream.pipe(zipStream); // $SPURIOUS:Alert
zipStream.on('error', e);
}
class StreamWrapper3 {
constructor() {
this.stream = getStream();
}
pipeIt(dest) {
return this.stream.pipe(dest);
}
register_error_handler(listener) {
return this.stream.on('error', listener);
}
}
function zip5() {
const zipStream = createWriteStream(zipPath);
let wrapper = new StreamWrapper3();
wrapper.pipeIt(zipStream); // $MISSING:Alert
zipStream.on('error', e);
}
function zip6() {
const zipStream = createWriteStream(zipPath);
let wrapper = new StreamWrapper3();
wrapper.pipeIt(zipStream);
wrapper.register_error_handler(e);
zipStream.on('error', e);
}
function registerErr(stream, listerner) {
stream.on('error', listerner);
}
function zip7() {
const zipStream = createWriteStream(zipPath);
let stream = getStream();
registerErr(stream, e);
stream.pipe(zipStream); // $SPURIOUS:Alert
zipStream.on('error', e);
}